Data Fetching

Fetch in an Effect, Store the Result in State

Data Fetching

The basic shape: an effect kicks off a fetch, a setter stores the result, the render shows loading / data / error.

5 min read Level 2/5 #react#fetch#useEffect
What you'll learn
  • Fetch data inside an effect
  • Handle loading and error states
  • Re-fetch when inputs change

For most apps, the typical data-loading pattern is an effect that starts a fetch and stores the result in state. Render shows one of three states: loading, error, or data.

The Shape

function UserCard({ id }) {
  const [user, setUser]       = useState(null);
  const [loading, setLoading] = useState(true);
  const [error, setError]     = useState(null);

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

    fetch(`/api/users/${id}`)
      .then(res => {
        if (!res.ok) throw new Error(`HTTP ${res.status}`);
        return res.json();
      })
      .then(data => setUser(data))
      .catch(err => setError(err))
      .finally(() => setLoading(false));
  }, [id]);

  if (loading) return <p>Loading…</p>;
  if (error)   return <p>Oops: {error.message}</p>;
  return <p>{user.name}</p>;
}

That’s it for the basic shape. The next lessons add two refinements: avoiding stale results and aborting in-flight requests.

Three States Pattern

StateShow
LoadingA spinner, skeleton, or “Loading…”
ErrorThe error message, maybe a retry button
DataThe thing
(Empty)“No results” if you’re showing a list

A common refactor is to compress these into a single state object:

const [state, setState] = useState({ status: "loading" });
// {status: "loading"} | {status: "error", err} | {status: "data", data}

A useReducer works well for this (chapter 5).

With async/await

The .then chain can be flattened with async/await inside the effect — but you can’t make the effect callback itself async (React requires it returns a cleanup or undefined). Define an inner async function:

useEffect(() => {
  async function load() {
    try {
      setLoading(true);
      const res = await fetch(`/api/users/${id}`);
      if (!res.ok) throw new Error(`HTTP ${res.status}`);
      setUser(await res.json());
    } catch (err) {
      setError(err);
    } finally {
      setLoading(false);
    }
  }
  load();
}, [id]);

Re-Fetching On Change

Putting id in deps makes the effect re-run whenever id changes. That’s exactly what you want for “show the user whose id is in the URL”.

Real Apps Use A Library

For most real apps, raw fetch in useEffect is replaced by React Query (TanStack Query) or SWR. They give you caching, deduplication, retries, and revalidation — for free.

Why we still teach the raw version:

  • It’s the mental model — libraries are productivity over the same primitives
  • You’ll see it in older code and small projects
  • Sometimes you really do need control over the request

Up Next

If the user changes id while a fetch is in flight, you can end up showing the wrong user. That’s a race condition.

Race Conditions →