LearnContact
Lesson 5022 min read

Fetch API

Learn how to load data with the browser fetch API inside useEffect, track loading and error state, and avoid setting state after a component unmounts.

Introduction

Almost every real application needs to load data from somewhere other than its own local state — a list of products, a user's profile, search results. The browser's built-in fetch() function is the most basic tool for making these network requests, and it needs no extra installation.

This lesson focuses on the pattern for using fetch() correctly inside a React component: firing the request when the component mounts, tracking whether it is loading or has failed, and cleaning up properly so you never try to update an unmounted component.

What You Will Learn
  • What the Fetch API is and its basic usage.
  • How to call fetch() inside useEffect to load data on mount.
  • How to track loading and error state alongside the fetched data.
  • How a cleanup flag prevents setting state after the component has unmounted.

What is the Fetch API?

fetch() is a function built into every modern browser (no import needed) for making HTTP requests. It returns a Promise that resolves to a Response object once the server responds, and you typically call .json() on that response to parse the body as JSON — which itself returns another Promise.

Basic fetch() Usage
fetch("https://api.example.com/users/1")
  .then((res) => res.json())
  .then((data) => console.log(data))
  .catch((err) => console.error(err));

Built Into the Browser

No package to install — fetch() is a global function.

Promise-Based

fetch() returns a Promise, so it works naturally with .then() or async/await.

Two-Step Parsing

The response body must be parsed separately, usually with res.json().

Only Rejects on Network Failure

fetch() does not reject on HTTP error statuses like 404 — you must check res.ok yourself.

Fetching Data Inside useEffect

To load data when a component first appears on screen, you call fetch() inside a useEffect with an empty dependency array, so it runs exactly once when the component mounts. The fetched data is stored in state so the component re-renders once it arrives.

UserProfile.jsx
import { useState, useEffect } from "react";

function UserProfile({ userId }) {
  const [user, setUser] = useState(null);

  useEffect(() => {
    fetch(`https://api.example.com/users/${userId}`)
      .then((res) => res.json())
      .then((data) => setUser(data));
  }, [userId]);

  if (!user) return <p>Loading...</p>;

  return (
    <div>
      <h2>{user.name}</h2>
      <p>{user.email}</p>
    </div>
  );
}
Rendered After the Fetch Resolves
Jane Doe
jane@example.com
Why userId Is a Dependency

Including userId in the dependency array means the effect re-runs (and re-fetches) whenever a different user's profile needs to be shown, not just on the very first mount.

Tracking Loading and Error State

A single data variable is not enough for a good user experience — you also need to know whether the request is still in flight, and whether it failed, so you can show an appropriate spinner or error message.

UserProfile.jsx with Loading and Error State
function UserProfile({ userId }) {
  const [user, setUser] = useState(null);
  const [loading, setLoading] = useState(true);
  const [error, setError] = useState(null);

  useEffect(() => {
    setLoading(true);
    setError(null);

    fetch(`https://api.example.com/users/${userId}`)
      .then((res) => {
        if (!res.ok) throw new Error("Failed to fetch user");
        return res.json();
      })
      .then((data) => setUser(data))
      .catch((err) => setError(err.message))
      .finally(() => setLoading(false));
  }, [userId]);

  if (loading) return <p>Loading...</p>;
  if (error) return <p>Error: {error}</p>;

  return (
    <div>
      <h2>{user.name}</h2>
      <p>{user.email}</p>
    </div>
  );
}
Three Possible Renders
Loading...

(or, on failure)
Error: Failed to fetch user

(or, on success)
Jane Doe
jane@example.com
Remember res.ok

fetch() only rejects on true network failures (like being offline). A 404 or 500 response still resolves successfully, so you must check res.ok yourself and throw manually to trigger the .catch().

Avoiding State Updates After Unmount

A fetch request can still be in flight when a component unmounts — for example, if the user quickly navigates away. If the request resolves later and tries to call setState on a component that no longer exists, React logs a warning, and in more complex cases it can lead to subtle bugs.

The standard fix is a cleanup flag: a local boolean tracked with useEffect's cleanup function, checked before each state update.

UserProfile.jsx with a Cleanup Flag
function UserProfile({ userId }) {
  const [user, setUser] = useState(null);
  const [loading, setLoading] = useState(true);
  const [error, setError] = useState(null);

  useEffect(() => {
    let isActive = true; // cleanup flag

    setLoading(true);
    setError(null);

    fetch(`https://api.example.com/users/${userId}`)
      .then((res) => {
        if (!res.ok) throw new Error("Failed to fetch user");
        return res.json();
      })
      .then((data) => {
        if (isActive) setUser(data);
      })
      .catch((err) => {
        if (isActive) setError(err.message);
      })
      .finally(() => {
        if (isActive) setLoading(false);
      });

    return () => {
      isActive = false; // component unmounted or userId changed again
    };
  }, [userId]);

  if (loading) return <p>Loading...</p>;
  if (error) return <p>Error: {error}</p>;

  return (
    <div>
      <h2>{user.name}</h2>
      <p>{user.email}</p>
    </div>
  );
}
How the Flag Works

isActive starts true. If the component unmounts (or userId changes, re-running the effect), the cleanup function sets isActive to false. When the pending fetch eventually resolves, each .then/.catch/.finally checks isActive first and skips the state update if it is no longer true.

Common Mistakes

Avoid These Mistakes
  • Calling fetch() directly in the component body instead of inside useEffect, which triggers a new request on every single render.
  • Forgetting the dependency array on useEffect, causing the fetch to repeat infinitely if it also updates state that the effect depends on.
  • Not checking res.ok, so failed requests (404, 500) are silently treated as successful.
  • Skipping the cleanup flag, which can produce the "Can't perform a React state update on an unmounted component" warning and, in rare cases, stale data overwriting newer state.

Best Practices

  • Always fetch inside useEffect, with an accurate dependency array.
  • Track loading and error state separately from the data itself so the UI can respond to each case.
  • Use a cleanup flag (or an AbortController) to guard against state updates after unmount.
  • Check response.ok and throw manually to route failed HTTP responses into your error handling.
  • For more complex apps, consider a data-fetching library that handles these concerns for you automatically — covered later in this course.

Frequently Asked Questions

Why does my fetch run twice in development?

React's Strict Mode intentionally mounts, unmounts, and remounts components once in development to help surface effect cleanup bugs. It does not happen in production builds.

Should I use async/await instead of .then chains for fetch?

You can, by defining an async function inside the effect and calling it immediately, since the effect callback itself cannot be async. Both styles work; .then chains are shown here for clarity.

Is a cleanup flag the only way to prevent updates after unmount?

No. An AbortController can also cancel the in-flight request itself when the component unmounts, which is a more thorough solution, but the boolean flag is simpler and sufficient for many cases.

What is the difference between fetch and Axios?

fetch is built into the browser and requires manual JSON parsing and error-status checking. Axios is a separate library covered in the next lesson that adds automatic JSON parsing and treats bad HTTP statuses as errors by default.

Can I use fetch to send data, not just retrieve it?

Yes. Passing a second options object with method: "POST" and a body lets fetch send data to a server as well.

Key Takeaways

  • fetch() is the browser's built-in function for making HTTP requests, returning a Promise you resolve with .then(res => res.json()).
  • Call fetch() inside useEffect so data loads once when the component mounts (or when dependencies change).
  • Track loading and error state alongside the fetched data for a complete user experience.
  • A cleanup flag set in useEffect's cleanup function prevents state updates after the component has unmounted.

Summary

The Fetch API gives you a straightforward, dependency-free way to load data into a component, as long as you pair it with useEffect, careful loading/error state, and a cleanup flag to guard against updates after unmount. These patterns form the foundation for every data-fetching approach you will use in React.

Next, you will look at Axios, a popular third-party library that streamlines many of these same steps.

Lesson 50 Completed
  • You understand what the Fetch API is and how it works.
  • You can fetch data inside useEffect on mount.
  • You can track loading and error state alongside fetched data.
  • You know how to avoid state updates after a component unmounts.
Next Lesson →

Axios