Fetching data
Load data with loading and error states, avoid race conditions with AbortController, and know when to reach for a library.
Nearly every app loads data from a server: courses, user profiles, search results. In this lesson you'll fetch data in a React component the right way — with loading and error states, cancellation to avoid race conditions, and a reusable pattern — and learn when to hand the job to a library instead.
We'll use the free practice API JSONPlaceholder (https://jsonplaceholder.typicode.com).
Fetching on mount#
Key points:
- The effect callback isn't
asyncitself; it definesloadand calls it. (An async callback would return a promise, but React expects a cleanup function or nothing.) - Check
res.ok—fetchdoesn't reject on 404/500. - Handle three states: loading, error and success. A single
statusvalue can't be "loading" and "error" at once. - The cleanup aborts the request. In development Strict Mode, the first request is aborted immediately and a second one runs — that's expected.
Fetching when a prop changes#
When data depends on a prop or state, add it to the dependencies:
Why the cleanup matters: race conditions
Imagine the user switches from user 1 to user 2 quickly, and user 1's response is slower. Without cleanup:
Response 1 would overwrite user 2's posts, showing the wrong data. Aborting the previous request (or setting an ignore flag in cleanup) guarantees only the latest request can update state:
Extracting a useFetch hook#
The pattern repeats, so extract it into a custom hook (you'll learn these properly soon):
Now any component can fetch with one line.
Sending data (mutations)#
Creating, updating or deleting data happens in response to user actions, so it belongs in event handlers, not effects:
Organising API code#
Keep fetch details out of components with a small API module:
Components then call getUsers(controller.signal) — and when the backend changes, you edit one file.
Avoiding waterfalls#
If a parent fetches, renders a child, and the child then fetches, the requests run one after another — a waterfall. When requests are independent, start them together:
Why most apps use a data library#
Hand-written effects work, but production apps also need:
- Caching — don't refetch the same data every time a component mounts.
- De-duplication — ten components asking for the same user should make one request.
- Background refetching — refresh stale data when the window regains focus.
- Retries, pagination, optimistic updates and invalidation after mutations.
That's what TanStack Query does (it has its own lesson in the Advanced module):
Frameworks offer alternatives too: React Router loaders fetch before a page renders, and Next.js Server Components fetch on the server. React 19's use() API can also read a promise during render together with Suspense.
Common mistakes#
- Making the effect callback
async. - Not checking
res.ok. - No loading or error state — users see a blank screen or a crash.
- Forgetting cleanup, causing race conditions and "state update on unmounted component" style bugs.
- Fetching in response to clicks via an effect instead of directly in the handler.
- Putting an object or array literal in the dependency array, causing refetch loops.
What's next#
Next: useRef — reaching into the DOM to focus and measure elements, and storing values that shouldn't trigger re-renders.
Check your understanding
Quick quiz
1.Why might a slow response for an old
userIdoverwrite the data for the current one?2.Why can't you write
useEffect(async () => { ... }, [])?3.What problems does a data library like TanStack Query solve that a hand-written
useEffect+fetchdoesn't?
Finished reading?
Mark this lesson complete to track your progress.