Fetching Data — And Why It Is Harder Than It Looks
IntermediateThe three-line version in an effect is a demo. The production version handles loading, errors, races, cancellation, refetching and caching — which is why a library usually wins.
Overview
Fetching in an effect looks trivial, and every tutorial shows it. What the tutorial omits is everything that makes it correct: what the user sees while it is in flight, what happens on failure, what happens when the parameter changes mid-request, whether the component is still mounted, and whether the same data is refetched by three components on the same screen. Writing this by hand once is worth doing, because it explains exactly which problems a data library is solving and why "I will just use useEffect" stops scaling around the third screen.
The Complete Manual Version
Everything the three-line version leaves out.
function useProblems(topic) {
const [state, setState] = useState({ status: 'loading' })
useEffect(() => {
const controller = new AbortController()
setState({ status: 'loading' })
fetch(`/api/problems?topic=${encodeURIComponent(topic)}`, {
signal: controller.signal,
})
.then(res => {
if (!res.ok) throw new ApiError(res.status) // fetch does NOT reject on 4xx
return res.json()
})
.then(data => setState({ status: 'success', data }))
.catch(err => {
if (err.name === 'AbortError') return // superseded, not a failure
setState({ status: 'error', error: err })
})
return () => controller.abort()
}, [topic])
return state
}
// That is the minimum for ONE endpoint. Now add: retry, caching
// across components, refetch on window focus, invalidation after a
// mutation, and pagination.Rendering Every State
Four states, all of which the user will see. Skipping one is the most common review comment on junior code.
const { status, data, error } = useProblems(topic)
if (status === 'loading') return <ProblemsSkeleton />
if (status === 'error') return <ErrorState error={error} onRetry={refetch} />
if (data.length === 0) return <EmptyState topic={topic} />
return <ProblemList problems={data} />
// The four, always:
// loading — a skeleton matching the final layout beats a spinner,
// because it does not shift the page when data arrives
// error — say what failed and offer a way to retry
// empty — "no problems yet" is a different message from an error
// success — the actual UI
// A common miss: data && data.map(...) with no empty branch, so an
// empty result renders a blank screen that looks broken.Where the Request Belongs
Keep fetch calls out of components, and keep the shape honest.
// features/problems/api.js — one place per resource
export async function listProblems({ topic, page = 1, signal }) {
const url = new URL('/api/problems', API_BASE)
url.searchParams.set('topic', topic)
url.searchParams.set('page', String(page))
const res = await fetch(url, { signal, credentials: 'include' })
if (!res.ok) throw new ApiError(res.status, await res.text())
return res.json()
}
// The component never builds a URL or reads a status code.
// Benefits: one place to add auth headers, one place to change the
// base URL, and the function is testable on its own.
// Waterfalls — the performance bug of nested fetching:
// <Page> fetches user, then <Profile> fetches settings using user.id,
// then <Avatar> fetches the image. Three sequential round trips.
// Fetch in parallel where the requests are independent:
const [user, problems] = await Promise.all([getUser(), listProblems()])Key Points to Remember
- 1fetch does not reject on 4xx or 5xx — check res.ok explicitly or errors pass silently
- 2A correct manual fetch needs abort, race protection, and a status for each outcome
- 3Render all four states: loading, error, empty and success — a missing empty state looks like a bug
- 4A skeleton that matches the final layout avoids the layout shift a spinner causes
- 5Keep fetch calls in a per-resource api module, and parallelise independent requests to avoid waterfalls
Interview Questions
Sign in to ask AriaWhat does a production-quality fetch inside useEffect need that a tutorial version omits?
Why does fetch not throw on a 404 response?
What is a request waterfall and how do you avoid one?
Ask Aria about Fetching Data — And Why It Is Harder Than It Looks
Your personal AI tutor — ask anything about this concept
Revision Status
Personal Notes
Sign in to save personal notes for this topic.
Discussion
Sign in to join the discussion.