Lesson 4 of 9 · 22m
Data Fetching Without the Flicker
Loading, error, and empty states are part of the feature. Fetch patterns that handle races, stale data, and cancellation properly.
Data fetching in a bare useEffect is where production React goes to die: no caching, races when inputs change fast, no dedup when two components want the same data. You can do it correctly by hand, or reach for a server-state library — either way, you need to know what correct looks like.
The race nobody sees until production
useEffect(() => {
fetch(`/api/users/${id}`)
.then(r => r.json())
.then(setUser); // race: if id changes twice fast, the *slower* request wins
}, [id]);Fix it with a stale guard or an AbortController:
useEffect(() => {
const ctrl = new AbortController();
fetch(`/api/users/${id}`, { signal: ctrl.signal })
.then(r => r.json())
.then(setUser)
.catch(e => { if (e.name !== "AbortError") setError(e); });
return () => ctrl.abort();
}, [id]);Server state isn't UI state
"Is the user list loaded" and "is the dropdown open" are different kinds of state. Server data has staleness, caching, deduplication, refetching — that's why react-query/SWR exist. In one line: const { data, isPending, error } = useQuery({ queryKey: ["users", filter], queryFn }). Loading states, retries, cache invalidation, and the race above are all handled.
Suspense is for "not ready yet" UI
React 18's <Suspense> lets components declare they're waiting instead of checking flags:
<Suspense fallback={<FeedSkeleton />}>
<Feed /> {/* reads data during render via a Suspense-aware source */}
</Suspense>Libraries (TanStack Query, frameworks' loaders) do the Suspense integration; your components just read the data. The win is composability — three sections can suspend independently, or one boundary can cover a whole panel.
::exercise(fetch-flicker)
Field notes
- Every fetch needs four states designed: loading, error, empty, success. "Empty" is the one people forget — a search with no results isn't an error.
- Optimistic updates (apply locally, rollback on failure) make interfaces feel instant; rollback state needs to be captured before the mutation.
- Polling beats websockets for most things until it doesn't — ask how stale the data can be before you build a realtime pipeline.