Data Fetching Patterns
Date: 2026-08-19
Where you put the fetch decides the shape of the waterfall. Fetching inside components is the natural thing to write and produces requests in series, one component deep at a time; hoisting the decision to the route is the fix, and it’s why loaders exist.
A data fetching pattern is the choice of when a request starts relative to rendering, and who decides to make it.
The three timings
FETCH-ON-RENDER render → discover you need data → fetch → render again
the default when fetching lives in components
FETCH-THEN-RENDER fetch everything → render once
blocks the whole page on the slowest request
RENDER-AS-YOU-FETCH start fetching from the URL, render the shell,
stream each piece in as it lands ← the target
The waterfall, which is the whole problem
Fetching inside components means a request can’t start until its component renders, and its component can’t render until its parent’s request resolves:
fetch-on-render, three levels deep
Page ├──── 180ms ────┤
Product ├──── 210ms ────┤
Reviews ├──── 240ms ────┤
▲
630ms before the page is complete
hoisted to the route — all three known from the URL alone
Page ├──── 180ms ────┤
Product ├──── 210ms ────┤
Reviews ├──── 240ms ────┤
▲
240ms ← the same three requests
Same requests, same server, same data. The only change is who decided to make them — the route rather than the components — and it removes nearly two-thirds of the wait.
The insight that makes it possible: the URL already tells you what the page needs. /products/blue-shoe implies the product, the reviews and the recommendations before a single component has rendered, so nothing has to be discovered by rendering.
Why loaders won
Route-level data loading — a function per route that runs before rendering, on the server, with access to the URL parameters — is the pattern the last generation of frameworks converged on, whatever it’s called. It gets four things at once:
- Parallelism by construction, as above
- Server-side execution, so the request to the database or API doesn’t leave the datacentre
- No loading state in the common case, because data arrives with the HTML
- Automatic revalidation after a mutation, so cache invalidation stops being hand-written — see Reference - React Router for the concrete shape
The cost is that data becomes a property of routes rather than of components, so a component that needs something new can’t just ask for it — the route must be edited too. That’s a real ergonomic loss, and the trade the whole pattern rests on.
Critical versus deferred
Not everything should block the first byte. Split the loader’s work in two: what the page can’t render without, awaited; everything else returned as an unresolved promise and streamed in behind the shell.
awaited product, price, availability blocks HTML — must be right first time
deferred reviews, recommendations, stock streams in — page is 200 without it
at nearby stores
Two rules that come with it: a deferred rejection can’t change the status code — the 200 was sent with the shell, so the failure surfaces as an error boundary mid-page rather than as a 500, which is why deferred work needs its own handling rather than being left to bubble — and anything above the fold must not be deferred, or you’ve moved a Largest Contentful Paint element behind a spinner. See Streaming Responses.
Client-side caching, when you still need it
Data that isn’t route-shaped — an autocomplete, a live basket count, a polled stock level — still needs a client fetching layer, and the reason to use a library rather than an effect is the four things listed in Component State vs Application State: staleness, invalidation, deduplication and races.
The race is the one people underestimate. Type sho, then shoe; the first request resolves second; the results are for the wrong query, and the input shows the right text alongside the wrong list. Only cancellation or a request-identity check fixes it, and neither is something you remember to write by hand every time — Race Conditions.
Preloading is the cheapest win
Start the fetch on link hover or when the link enters the viewport, and the navigation feels instant because the request had a head start of a few hundred milliseconds. Most routing layers do this for you given a hint; it costs a wasted request on links the user never follows, which is nearly always a good trade — Resource Hints.