Kodokon kodokon.com

Calling an API: fetch, loading, and errors

Load remote data with fetch inside an effect, cleanly modeling the loading and error states.

10 min · 3 questions

Open this lesson in Kodokon

A network request takes time and can fail: your interface must reflect those realities. The professional convention is to track three distinct pieces of state: the data received, a loading boolean, and a possible error. The request goes inside a useEffect, because contacting a server is a side effect.

JSX
const [users, setUsers] = useState([]);
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);

useEffect(() => {
  fetch("https://api.example.com/users")
    .then((res) => {
      if (!res.ok) throw new Error("HTTP " + res.status);
      return res.json();
    })
    .then((data) => setUsers(data))
    .catch((err) => setError(err.message))
    .finally(() => setLoading(false));
}, []);
Three states for one request: data, loading, error.

In the render, handle the cases in order: loading first, then the error, and finally the data. These early returns keep the code readable and guarantee that the user always sees something coherent: a waiting indicator, a failure message, or the expected list.

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

return (
  <ul>
    {users.map((user) => (
      <li key={user.id}>{user.name}</li>
    ))}
  </ul>
);
Early returns: only one case displayed at a time.

One last trap: if a dependency changes (a userId, for example), a new request goes out while the old one may still be in flight. If the old one answers last, it would overwrite the fresh data. The classic countermeasure: an ignore flag set to true by the cleanup function, to throw away stale responses.

JSX
useEffect(() => {
  let ignore = false;
  setLoading(true);

  fetch("/api/users/" + userId)
    .then((res) => res.json())
    .then((data) => {
      if (!ignore) setUser(data);
    })
    .finally(() => {
      if (!ignore) setLoading(false);
    });

  return () => {
    ignore = true;
  };
}, [userId]);
Every stale response is discarded thanks to the flag.

Knowledge check

Make sure you remember the key points of this lesson.

  1. Why track three states (data, loading, error) for a request?
    • Because fetch requires these three variables to work
    • So the interface reflects each phase: waiting, failure, or success
    • To prevent React from re-firing the request on every render
  2. Your API responds with a 500 status, but the catch never fires. Why?
    • fetch only rejects the promise on a network failure, not for an HTTP error status
    • catch does not work inside a useEffect
    • The server must send a special header to trigger the catch
  3. What is the ignore flag, set to true in the effect's cleanup, used for?
    • To cancel the in-flight HTTP request on the server side
    • To prevent React from re-running the effect
    • To discard the response of a request that has become stale