Tags: web-dev concept

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.