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.
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:
idle
- used when the application is first opened and not doing anything at allloading
- used when the application is fetching dataerror
- used when the application has unsuccessfully fetched some datasuccess
- 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.jsximport { 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.jsximport { 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.jsximport { 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.jsximport { 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.jsximport { 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.jsexport 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.jsximport { 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.jsxfunction 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!