react-fetch-api

Fetch API in React

  • 5 min

Up to this point, all the data in our application (user lists, products, tasks) were “hardcoded” in arrays within the code.

But the real world doesn’t work like that. In the real world, data lives on a server (Backend), in a Database, and our React application must request it, wait for it to arrive, and display it.

To communicate with that server, the browser offers us a standard native tool: the Fetch API.

In this article, we will learn how to integrate fetch within the React lifecycle to make GET requests.

Where are requests made?

Can I put fetch directly in the component body? ❌ NO.

function BadExample() {
  // SERIOUS ERROR!
  // This will execute on EVERY render.
  fetch('https://api.com/data').then(...) 

  return <div>...</div>
}
Copied!

An HTTP request is a Side Effect. It is not part of the pure UI calculation. Therefore, the only correct place to make a request when mounting a component is inside useEffect.

The Loading, Error, Data Pattern

When we request data from a server, the response is not immediate. The application goes through three logical states that we must handle visually:

  1. Loading: “I am waiting for the data”. (Show spinner).
  2. Error: “Something went wrong”. (Show error message).
  3. Data: “Here is the data”. (Show the list).

To handle this, we need three states (or a state object that groups them).

const [data, setData] = useState(null);       // The final data
const [loading, setLoading] = useState(true); // Are we loading?
const [error, setError] = useState(null);     // Was there an error?
Copied!

Step-by-Step Implementation

Let’s create a component that requests a list of users from a public API (we’ll use JSONPlaceholder).

The useEffect Structure

Here’s an important technical trick. The function we pass to useEffect cannot be asynchronous (it cannot return a Promise).

Therefore, we must create an internal async function and call it immediately.

import { useState, useEffect } from "react";

export default function UserList() {
  const [users, setUsers] = useState(null);
  const [loading, setLoading] = useState(true);
  const [error, setError] = useState(null);

  useEffect(() => {
    // Define the async function inside the effect
    const fetchData = async () => {
      try {
        setLoading(true); // Start loading (optional if initial state is already true)

        const response = await fetch("https://jsonplaceholder.typicode.com/users");

        // NOTE: Fetch does not throw an error on 404 or 500, only on network failures.
        // We must check response.ok manually.
        if (!response.ok) {
          throw new Error(`HTTP Error: ${response.status}`);
        }

        const jsonData = await response.json();
        setUsers(jsonData);
        setError(null); // Clear previous errors if any
      } catch (err) {
        setError(err.message);
        setUsers(null);
      } finally {
        setLoading(false); // Whether successful or not, finish loading
      }
    };

    // Execute the function
    fetchData();

  }, []); // <--- Empty array: IMPORTANT so it only executes on mount
Copied!

Conditional Rendering

Now we use Early Return patterns to decide what to render.

  // Loading Block
  if (loading) {
    return <div className="spinner">Loading users...</div>;
  }

  // Error Block
  if (error) {
    return (
      <div className="alert-error">
        An error occurred: {error}
        <button onClick={() => window.location.reload()}>Retry</button>
      </div>
    );
  }

  // Success Block (Data)
  return (
    <ul>
      {users.map((user) => (
        <li key={user.id}>
          <strong>{user.name}</strong> ({user.email})
        </li>
      ))}
    </ul>
  );
}
Copied!

The Problem with Native Fetch

Although fetch works well, it has some inconveniences that developers often dislike:

  1. Does not throw errors on 404/500: You always have to write the if (!response.ok) check.
  2. Two steps: You need to call fetch and then .json().
  3. Boilerplate: You have to manually write all the headers (Content-Type, Auth Tokens, etc.) on each request.

For these reasons, in large applications it is often useful to wrap fetch in a small custom layer, or use a more convenient HTTP client.