React: Fetch Data on Mount

It's been a busy month! A few weeks ago I played a wonderful show with Future Museums, opening for Graham Reynolds at the beautiful Gaslight Baker Theatre in Lockhart, and we spent a few more days in the piney woods of Bastrop. Then last week I went back out to Lockhart to play with F.L.O.W. at Commerce Hall, opening for Highway Lights.

Joey warming up on the stage of the Gaslight Baker Theatre in Lockhart.

In my last few posts I've covered XHR requests, Fetch requests, and Axios requests. This month, I want to stay on the topic of asynchronous data fetching, but move that work from traditional vanilla JavaScript into React. The challenge I'm taking on in this blog post is to fetch some data in a React app on component mount without using any data fetching libraries. As a bonus challenge, I'll do some minimal styling with Tailwind.

Bootstrap the Vite/React app

Let's start with a very basic React app. I'm going to take this as an opportunity to try out Vite instead of Create React App.

yarn create vite

Here are the CLI options I went with:

  • ✔ Project name: > fetch-data-on-mount
  • ✔ Select a framework: › React
  • ✔ Select a variant: › JavaScript

I then run cd fetch-data-on-mount && yarn && yarn dev to install dependencies and get my app started.

Now I need to do a little bit of cleanup. First, I delete the entire public directory and I update index.html to the following:

<!-- index.html -->
<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>Fetch data on mount</title>
  </head>
  <body>
    <div id="root"></div>
    <script type="module" src="/src/main.jsx"></script>
  </body>
</html>

In the src directory, I'll delete App.css and the entire assets directory. I'll delete everything in index.css but leave that file (I'll be adding to it later). I also update main.jsx to the following:

import React from "react";
import ReactDOM from "react-dom/client";
import App from "./App.jsx";
import "./index.css";

ReactDOM.createRoot(document.getElementById("root")).render(
  <React.StrictMode>
    <App />
  </React.StrictMode>
);

And finally I'll update App.jsx to the following:

function App() {
  return (
    <>
      <h1>Fetch data on mount</h1>
    </>
  );
}

export default App;

So we are down to the barest of brass tacks in our React app. We can close out Main.jsx. For the entire next section of the blog post I'll solely be working in src/App.jsx.

Set a status state

The first thing I need to do is convey what's going on with the data fetching. I can do this using a status state variable. This should have four available values:

  1. idle - used when the application is first opened and not doing anything at all
  2. loading - used when the application is fetching data
  3. error - used when the application has unsuccessfully fetched some data
  4. success - used when the application has successfully fetched some data

I'll kick things off using the useState hook, and pass the initial state value of "idle":

// src/App.jsx
import { useState } from "react";

function App() {
  /*
   * Initialize state variable for status of the fetch request. Possible statuses:
   * - idle
   * - loading
   * - error
   * - success
   */
  const [status, setStatus] = useState("idle");

  return (
    <>
      <p>{status}</p>
    </>
  );
}

export default App;

Not much going on at the moment, we're just rendering the "idle" string out to a paragraph element.

Fetch the data

In order to fetch some data when the component mounts, I need to use the useEffect hook with an empty dependency array:

// src/App.jsx
import { useState, useEffect } from "react";

function App() {
  /*
   * Initialize state variable for status of the fetch request. Possible statuses:
   * - idle
   * - loading
   * - error
   * - success
   */
  const [status, setStatus] = useState("idle");

  // Fetch data on component mount
  useEffect(() => {}, []);

  return (
    <>
      <p>{status}</p>
    </>
  );
}

export default App;

Now I need to start fetching the data. I'll use the fetch() method that I covered a couple of blog posts ago and I'll use a GET request endpoint from the https://reqres.in/ mock API that I used in that same blog post. In the previous blog post I fetched a list of users, but this time I want to fetch the information for a single user and display it.

// src/App.jsx
import { useState, useEffect } from "react";

function App() {
  /*
   * Initialize state variable for status of the fetch request. Possible statuses:
   * - idle
   * - loading
   * - error
   * - success
   */
  const [status, setStatus] = useState("idle");

  // Fetch data on component mount
  useEffect(() => {
    // Helper function to make the request and return error or success state
    function sendFetchRequest(method, url, data) {
      return fetch(url, {
        method,
        body: JSON.stringify(data),
        header: data
          ? {
              "Content-Type": "application/json",
            }
          : {},
      }).then((response) => {
        if (response.status >= 400) {
          return response.json().then((errorResponseData) => {
            // Error state
          });
        } else {
          // Success state
          return response.json();
        }
      });
    }
  }, []);

  return (
    <>
      <p>{status}</p>
    </>
  );
}

export default App;

I have my helper sendFetchRequest() function that builds my fetch request, throws an error if there's an error state returned from the request, or returns the response if there's a success state returned from the request. Following this format, I can update my state variables along the way and call this function with the endpoint to fetch the data.

To test the error state, I can use the https://reqres.in/api/users/23 endpoint, and to test the success state, I can use the https://reqres.in/api/users/2 endpoint.

Here is what my app looks like now:

// src/App.jsx
import { useState, useEffect } from "react";

function App() {
  /*
   * Initialize state variable for status of the fetch request. Possible statuses:
   * - idle
   * - loading
   * - error
   * - success
   */
  const [status, setStatus] = useState("idle");

  // Fetch data on component mount
  useEffect(() => {
    // Helper function to make the request and return error or success state
    function sendFetchRequest(method, url, data) {
      // Set status state variable to "loading" during fetch
      setStatus("loading");
      return fetch(url, {
        method,
        body: JSON.stringify(data),
        header: data
          ? {
              "Content-Type": "application/json",
            }
          : {},
      }).then((response) => {
        if (response.status >= 400) {
          return response.json().then((errorResponseData) => {
            // Error state - construct and throw our error
            const error = new Error("Something went wrong!");
            throw error;
          });
        } else {
          // Success state - return user data
          return response.json();
        }
      });
    }

    // Fetch the data
    // Error: https://reqres.in/api/users/23
    // Success: https://reqres.in/api/users/2
    sendFetchRequest("GET", "https://reqres.in/api/users/2")
      .then((res) => {
        // Set status state variable to "success"
        setStatus("success");
        console.log(res);
      })
      .catch((err) => {
        // Set status state variable to "error"
        setStatus("error");
        console.error(err);
      });
  }, []);

  return (
    <>
      <p>{status}</p>
    </>
  );
}

export default App;

At this point we have all four of our potential statuses available and tested. "idle" will show if we load the app but don't call sendFetchRequest(). "loading" will show if we call sendFetchRequest() but don't return anything, "error" will show if we call sendFetchRequest(https://reqres.in/api/users/23) (this endpoint intentionally returns an error), and "success" will show if we call sendFetchRequest(https://reqres.in/api/users/2).

The last thing I need to do is display either a loading message, an error message, or the user information, depending on what we get back. I will make two new state variables, error and user to hold these two potential pieces of information. If an error is returned from sendFetchRequest(), I'll set that error to the error state variable and display the error. If a success/user is returned from sendFetchRequest(), I'll set that data to the user state variable and display that user information. Lastly I will update the markup with some conditions to show each of these options:

// src/App.jsx
import { useState, useEffect } from "react";

function App() {
  /*
   * Initialize state variable for status of the fetch request. Possible statuses:
   * - idle
   * - loading
   * - error
   * - success
   */
  const [status, setStatus] = useState("idle");

  // Initialize state variable to hold a potential error
  const [error, setError] = useState({});

  // Initialize state variable to hold a potential success/user
  const [user, setUser] = useState({});

  // Fetch data on component mount
  useEffect(() => {
    // Helper function to make the request and return error or success state
    function sendFetchRequest(method, url, data) {
      // Set status state variable to "loading" during fetch
      setStatus("loading");
      return fetch(url, {
        method,
        body: JSON.stringify(data),
        header: data
          ? {
              "Content-Type": "application/json",
            }
          : {},
      }).then((response) => {
        if (response.status >= 400) {
          return response.json().then((errorResponseData) => {
            // Error state - construct and throw our error
            const error = new Error("Something went wrong!");
            throw error;
          });
        } else {
          // Success state - return user data
          return response.json();
        }
      });
    }

    // Fetch the data
    // Error: https://reqres.in/api/users/23
    // Success: https://reqres.in/api/users/2
    sendFetchRequest("GET", "https://reqres.in/api/users/2")
      .then((res) => {
        // Set status state variable to "success"
        setStatus("success");
        setUser(res);
      })
      .catch((err) => {
        // Set status state variable to "error"
        setStatus("error");
        setError(err);
      });
  }, []);

  return (
    <>
      {/* Loading status - show "Loading..." */}
      {status === "loading" && <p>Loading...</p>}

      {/* Error status - show some kind of error */}
      {status === "error" && <p>{error.message}</p>}

      {/* Success status - show the user */}
      {status === "success" && (
        <>
          <img
            src={user.data.avatar}
            alt={`${user.data.first_name} ${user.data.last_name}`}
          />
          <p>
            {user.data.first_name} {user.data.last_name}
          </p>
          <p>
            <a href={`mailto:${user.data.email}`}>{user.data.email}</a>
          </p>
        </>
      )}
    </>
  );
}

export default App;

Success! We are now fetching some user data and displaying it.

Adding Tailwind

As a bit of an added challenge or exploration, I'm going to see how hard it is to add Tailwind and let Vite do the bundling and purging of the CSS. To start, I need to add and initialize Tailwind:

yarn add -D tailwindcss postcss autoprefixer && npx tailwindcss init -p

Next I need to update the content property in the Tailwind Config file so it knows which files to look for utility CSS classes:

// tailwind.config.js
export default {
  content: ["./index.html", "./src/**/*.{js,ts,jsx,tsx}"],
  theme: {
    extend: {},
  },
  plugins: [],
};

Finally I need to add the basic Tailwind imports to index.css, which we import in main.jsx:

/* src/index.css */
@tailwind base;
@tailwind components;
@tailwind utilities;

With that I can utilize Tailwind utility classes. I'll now update App.jsx to center all of the page content.

// src/App.jsx
import { useState, useEffect } from "react";
import UserCard from "./components/UserCard";

function App() {
  /*
   * Initialize state variable for status of the fetch request. Possible statuses:
   * - idle
   * - loading
   * - error
   * - success
   */
  const [status, setStatus] = useState("idle");

  // Initialize state variable to hold a potential error
  const [error, setError] = useState({});

  // Initialize state variable to hold a potential success/user
  const [user, setUser] = useState({});

  // Fetch data on component mount
  useEffect(() => {
    // Helper function to make the request and return error or success state
    function sendFetchRequest(method, url, data) {
      // Set status state variable to "loading" during fetch
      setStatus("loading");
      return fetch(url, {
        method,
        body: JSON.stringify(data),
        header: data
          ? {
              "Content-Type": "application/json",
            }
          : {},
      }).then((response) => {
        if (response.status >= 400) {
          return response.json().then((errorResponseData) => {
            // Error state - construct and throw our error
            const error = new Error("Something went wrong!");
            throw error;
          });
        } else {
          // Success state - return user data
          return response.json();
        }
      });
    }

    // Fetch the data
    // Error: https://reqres.in/api/users/23
    // Success: https://reqres.in/api/users/2
    sendFetchRequest("GET", "https://reqres.in/api/users/2")
      .then((res) => {
        // Set status state variable to "success"
        setStatus("success");
        setUser(res);
      })
      .catch((err) => {
        // Set status state variable to "error"
        setStatus("error");
        setError(err);
      });
  }, []);

  return (
    <div className="flex flex-col items-center pt-10">
      {/* Loading status - show "Loading..." */}
      {status === "loading" && (
        <p className="text-3xl font-bold underline">Loading...</p>
      )}

      {/* Error status - show some kind of error */}
      {status === "error" && (
        <p className="text-3xl font-bold underline">{error.message}</p>
      )}

      {/* Success status - show the user */}
      {status === "success" && <UserCard user={user} />}
    </div>
  );
}

export default App;

I've also broken out the success state markup into a component called UserCard. This component lives in src/components/UserCard.jsx, and here are its contents:

// src/components/UserCard.jsx
function UserCard({ user }) {
  return (
    <div className="w-full max-w-sm bg-white border border-gray-200 rounded-lg shadow dark:bg-gray-800 dark:border-gray-700">
      <div className="flex flex-col items-center p-10">
        <img
          className="w-24 h-24 mb-3 rounded-full shadow-lg"
          src={user.data.avatar}
          alt={`${user.data.first_name} ${user.data.last_name}`}
        />
        <h1 className="mb-1 text-xl font-medium text-gray-900 dark:text-white">
          {user.data.first_name} {user.data.last_name}
        </h1>
        <div className="flex mt-4 md:mt-6">
          <a
            href={`mailto:${user.data.email}`}
            className="inline-flex items-center px-4 py-2 text-sm font-medium text-center text-white bg-blue-700 rounded-lg hover:bg-blue-800 focus:ring-4 focus:outline-none focus:ring-blue-300 dark:bg-blue-600 dark:hover:bg-blue-700 dark:focus:ring-blue-800"
          >
            Email me
          </a>
        </div>
      </div>
    </div>
  );
}

export default UserCard;

With that I have a Vite React app that fetches some API data on mount (without using any data fetching libraries, just the fetch API) and is styled using Tailwind. If I run yarn build, Vite handles all of the CSS purging for me. Pretty easy and nifty!