Data Fetching with useEffect

Data fetching means loading information from a server (an API) and showing it in your UI. Because a network request is a side effect , it belongs in useEffect . The reliable recipe: track loading , error , and data , run an async loader inside the effect, and clean up so a late response can't update an unmounted component.

Learn Data Fetching with useEffect in our free React course — a beginner-friendly interactive lesson with runnable examples, a practice exercise and a quick…

Part of the free React course at LearnCodingFast — hands-on lessons with examples you run in your browser, plus practice exercises and a quick quiz.

1️⃣ Three States: Loading, Error, Data

Every fetch goes through three situations. Give each its own state so your render always has exactly one thing to show:

Your turn — write the same three-way decision yourself:

2️⃣ An Async Loader Inside the Effect

You can't make the effect callback itself async (it would return a promise where React expects a cleanup). Instead, define an async function inside the effect and call it. Put the id in the deps so it re-fetches when the id changes.

3️⃣ Cleanup — Ignore Stale Responses

If the component unmounts (or the id changes) before the request resolves, calling setState on the old result is a bug. Guard with an ignore flag flipped by the cleanup function:

These lines make a correct async loader. Put them in the right order:

📋 Quick Reference

1. Why can't the useEffect callback be async ?

An async function returns a promise, but the effect must return nothing or a cleanup function. Define an inner async function and call it.

2. You fetch user id . What goes in the dependency array so it re-fetches per user?

[id] — the effect re-runs whenever the id changes.

A stale response from a previous render setting state after the component unmounted or the id changed (a race condition).

Write the async loader from the outline, await the fake fetch, and log the result. Run it and check your output.

Practice quiz

Why does a network request belong inside useEffect?

  • Because fetch only works inside effects
  • Because useEffect makes the request faster
  • Because a request is a side effect and shouldn't run during render
  • Because render functions can be async

Answer: Because a request is a side effect and shouldn't run during render. Rendering must be pure. A network request is a side effect, so it belongs in useEffect rather than directly in the render body.

Which three pieces of state model a fetch well?

  • loading, error, data
  • start, middle, end
  • request, response, retry
  • open, closed, pending

Answer: loading, error, data. Tracking loading, error, and data lets you render exactly one of a spinner, an error message, or the result — never a half-rendered screen.

Why can't you make the useEffect callback itself async?

  • Async functions can't call fetch
  • React forbids await anywhere in components
  • It would run the effect twice
  • An async function returns a promise, but the effect must return nothing or a cleanup function

Answer: An async function returns a promise, but the effect must return nothing or a cleanup function. An async callback returns a promise, but useEffect expects undefined or a cleanup function. Define an async function inside the effect and call it.

What is the correct pattern for awaiting inside an effect?

  • Make the effect callback async
  • Define an inner async function (e.g. load()) and call it
  • Use await directly at the top of the effect
  • Return a promise from the effect

Answer: Define an inner async function (e.g. load()) and call it. Define an async function such as load() inside the effect, then invoke it: async function load() { ... } load();.

What does an 'ignore' flag (or AbortController) in cleanup prevent?

  • A stale response setting state after unmount or after the input changed
  • A second render of the component
  • The fetch from ever starting
  • Errors from being thrown

Answer: A stale response setting state after unmount or after the input changed. If the component unmounts or the id changes before the request resolves, the guard stops the stale response from calling setState — avoiding a race condition.

To re-fetch whenever a user id changes, what goes in the dependency array?

Putting [id] in the deps makes the effect re-run whenever id changes, re-fetching the right user.

Where is the cleanup flag flipped to true?

  • Inside the async load function
  • In the function returned by the effect (the cleanup function)
  • In the render body
  • In a separate useState call

Answer: In the function returned by the effect (the cleanup function). The effect returns () => { ignore = true; }. React runs that cleanup before re-running the effect or on unmount, marking the previous request stale.

What commonly causes an infinite re-fetch loop?

  • Using an AbortController
  • Awaiting the response
  • Checking res.ok before parsing
  • Putting an object/array recreated each render in the deps, or omitting deps

Answer: Putting an object/array recreated each render in the deps, or omitting deps. A fresh object/array in the deps changes identity every render, re-running the effect endlessly. Depend on a stable primitive like id.

After await fetch(url), why check if (!res.ok) before using the body?

  • res.ok is required to call res.json()
  • fetch only rejects on HTTP 500 errors, not 404, so you must check the status yourself
  • It speeds up parsing
  • It is purely stylistic and optional

Answer: fetch only rejects on HTTP 500 errors, not 404, so you must check the status yourself. fetch resolves even for 404/500 responses; it only rejects on network failure. Check res.ok (or res.status) and throw to handle HTTP errors.

What do production apps typically use instead of hand-rolled useEffect fetching?

  • Nothing — useEffect is always best
  • Class component lifecycle methods only
  • A data library like React Query or SWR for caching, retries, and dedup
  • useLayoutEffect for fetching

Answer: A data library like React Query or SWR for caching, retries, and dedup. The useEffect pattern is a great foundation, but real apps usually reach for React Query or SWR, which add caching, retries, and request deduplication.