Skip to content
Intermediate14 min study

Client-side data fetching patterns

Design when requests start, how they cache, dedupe, and revalidate — separate from which API or protocol is used.

Question progress0 / 10 completed
Start the lesson
Client-side data fetching patterns visual explanation

WHAT YOU WILL BE ABLE TO DO

Learning outcomes

  • Explain client-side data fetching patterns in plain language.
  • Connect the behavior to the underlying browser or framework model.
  • Implement the core pattern and reason through edge cases.
  • Answer common follow-ups without relying on memorized phrases.

01Explain it simply

How a client fetches data — when it starts, whether it caches, how it dedupes, how it revalidates, how it handles loading and errors — is a design problem separate from which protocol or API shape it uses. Getting it wrong produces waterfalls, flicker, and stale or duplicated data.

Think of it like this: it's the difference between a well-run kitchen and a chaotic one using the same ingredients: does an order start cooking as soon as it's in, or wait for the previous one to plate? If two tables order the same dish within a minute, does the kitchen make it twice? Is a finished dish kept warm for the next identical order?

One-line definition: Design when requests start, how they cache, dedupe, and revalidate — separate from which API or protocol is used.

02Mental model

A mature client data layer, hand-rolled or a library like TanStack Query or SWR, gives each request a cache key, deduplicates in-flight requests for the same key, serves a cached value immediately while revalidating in the background, and centralizes loading and error state. It avoids waterfalls by starting independent requests in parallel and hoisting data needs so a route can fetch before components render. Mutations update the cache optimistically and roll back on failure.

03Step by step

  • Give every request a stable cache key derived from its parameters.
  • Deduplicate concurrent requests for the same key so ten components mounting don't fire ten calls.
  • Serve stale cached data instantly, then revalidate in the background and update in place.
  • Start independent requests in parallel; never let one component's fetch block another's unrelated fetch.
  • Colocate or hoist data needs so a route can begin fetching before its component tree renders.
  • For mutations, update the cache optimistically and provide a rollback path on error.

04Working example

JavaScript
const { data, isLoading } = useQuery({  queryKey: ["topic", slug],  queryFn: () => fetchTopic(slug),  staleTime: 60_000,});// Two components using the same queryKey share one request and one cache entry.

The queryKey identifies this request; any other component using the same key gets the same in-flight promise and cached result. staleTime lets the client serve the cached value without refetching for a minute, then revalidate in the background on next access rather than blocking on a fresh request.

05Where it is used

  • Eliminating duplicate requests when many components need the same data
  • Making navigation feel instant by showing cached data while revalidating
  • Centralizing retry, loading, and error handling instead of repeating it per component
  • Keeping related views in sync after a mutation via cache updates

06Common mistakes

  • Fetching in a child component that only renders after its parent's fetch resolves, creating a serial waterfall
  • No request deduplication, so a list and a header both requesting the current user fire two calls
  • Caching with no revalidation, showing data that's silently hours stale
  • Optimistic updates with no rollback, leaving the UI showing a change the server rejected

07Interview answer

How to say it out loud: "Data fetching on the client is its own design problem regardless of whether the API is REST or GraphQL. The core patterns: give each request a cache key, dedupe in-flight requests for the same key so a dozen components don't each fire the call, serve cached data immediately and revalidate in the background, and start independent requests in parallel instead of letting one component's fetch gate another's. The classic bug is a waterfall — a child fetches only after the parent's fetch resolves and it renders — and the fix is hoisting the data requirements up so the route fetches everything at once. Mutations update the cache optimistically with a rollback if the server rejects."

Separate which API from how the client fetches. Name the concrete patterns — cache key, dedup, stale-while-revalidate, parallel vs waterfall, optimistic update with rollback — and identify a waterfall as an architecture problem, not a slow endpoint.

A page shows a spinner, then renders a header, then shows another spinner while a list loads. Both requests are independent. What's the problem and the fix?

It's a request waterfall — the list request doesn't start until the header has fetched and rendered, so two independent requests run serially; the fix is initiating both in parallel at the route level before the components render, so total wait is the slower request, not the sum.

DDConcept deep dives

Deep dive 1

How the client fetches is a separate design axis from which API

REST, GraphQL, and RPC settle request and response shape; they say nothing about when a request starts, whether it's cached, whether concurrent identical requests are deduplicated, how staleness is handled, or where loading and error state lives. A GraphQL app with a naive fetch layer still suffers waterfalls and duplicate requests, because those are client-architecture problems.

  • Evaluate a data layer on cache keys, dedup, revalidation, and request timing — not on the protocol.
  • A purpose-built data library provides the server-cache lifecycle a hand-rolled store usually gets wrong.
  • Keep genuine UI state separate from cached server data.

Deep dive 2

Waterfalls are a structural problem, not a slow endpoint

A request waterfall happens when requests that could run in parallel instead run in series because each fires from a component that only mounts after the previous request resolves and renders. Total latency becomes the sum, not the max. The fix is structural: hoist a route's data requirements so all independent requests start together, and keep only genuine data dependencies sequential.

  • Fetch at the route or loader level, before the component tree renders, to parallelize.
  • A spinner, then content, then another spinner for independent data is the visible symptom.
  • Only a real dependency (need ids before fetching per-id detail) justifies a sequential request.

Deep dive 3

Cache, dedupe, and revalidate turn the network into a detail

A mature data layer keys each request, returns the same in-flight promise to every caller of that key, serves the cached value immediately while revalidating in the background, and updates every subscriber in place. Mutations apply optimistically to the cache and roll back on failure. The result is that navigation feels instant and the network becomes something the user rarely waits on directly.

  • Deduplication means ten components asking for the current user produce one request.
  • Stale-while-revalidate trades one stale render for consistently instant ones.
  • Optimistic updates without a rollback path leave the UI lying about server state.

QAInterview questions and model answers

Attempt each answer aloud before opening it. The model answer shows the depth and precision expected in an interview; it is not a script to memorize.

Intermediate · Conceptual · 1 min · Question 1Why is client data fetching a design problem separate from the API choice?Open model answer

Model answer

REST versus GraphQL versus RPC decides the request and response shape, but caching, deduplication, revalidation, request timing, and loading and error handling are client concerns that apply regardless. A GraphQL app with a naive fetch layer still has waterfalls and duplicate requests.

Open question page →
Intermediate · Conceptual · 1 min · Question 2What is a request waterfall and why does it hurt?Open model answer

Model answer

It's when requests that could run in parallel instead run in sequence because each depends on the previous one completing and rendering. Total latency becomes the sum of the requests rather than the slowest one, so a page with three 200ms requests takes 600ms instead of 200ms.

Open question page →
Intermediate · Conceptual · 1 min · Question 3How does request deduplication work and why does it matter?Open model answer

Model answer

The data layer keys requests by their parameters and returns the same in-flight promise to every caller of the same key, so ten components mounting and each asking for the current user produce one network request. Without it, unrelated components independently refetch the same data.

Open question page →
Intermediate · Conceptual · 1 min · Question 4What does stale-while-revalidate mean at the application data layer?Open model answer

Model answer

On access, the client immediately returns the cached value if present, then fetches fresh data in the background and updates the cache and UI when it arrives. The user sees something instantly and it becomes correct shortly after, instead of waiting on every navigation.

Open question page →
Intermediate · Conceptual · 1 min · Question 5How should optimistic updates handle failure?Open model answer

Model answer

Apply the change to the local cache immediately, keep the previous value, send the request, and if it fails, roll the cache back to the previous value and surface an error. Skipping the rollback path leaves the UI showing a change the server rejected.

Open question page →
Advanced · Conceptual · 1 min · Question 6Where should data requirements live to avoid waterfalls in a routed app?Open model answer

Model answer

Hoisted to the route or a loader that runs before the component tree renders, so all of a route's data requests can start together. Fetching inside deeply nested components that only mount after their parents resolve is the structural cause of waterfalls.

Open question page →
Intermediate · Conceptual · 1 min · Question 7When is it appropriate to not cache a request?Open model answer

Model answer

For data that must be exactly current at read time and has consequences if stale — a checkout's final price, an account balance, a permissions check — or for one-off actions. Even then, showing a cached value while revalidating can be acceptable if the authoritative check happens server-side at the decisive moment.

Open question page →
Advanced · Conceptual · 1 min · Question 8How do pagination and infinite scroll interact with a normalized cache?Open model answer

Model answer

Pages are appended to a cached list keyed by the query plus cursor, and individual items are often also stored by id so an update in one place reflects everywhere. Getting invalidation right — what to refetch after a create or delete — is the harder part.

Open question page →
Intermediate · Conceptual · 1 min · Question 9What's the risk of putting server data into a global client store like Redux by hand?Open model answer

Model answer

You reimplement caching, deduplication, revalidation, and garbage collection yourself, usually incompletely, and mix server cache concerns with genuine client state. Purpose-built data libraries handle the server-cache lifecycle, leaving the store for real UI state.

Open question page →
Intermediate · Conceptual · 1 min · Question 10How should a data layer handle retries?Open model answer

Model answer

Retry transient failures (network errors, 5xx) with backoff and a cap, don't retry client errors (4xx) that will fail again, and make retries cancelable so navigating away stops them. Centralizing this beats each call site inventing its own retry logic.

Open question page →

SCScenario questions

Scenario 1

A dashboard renders a user header, then a projects list, then per-project stats. Each waits for the previous to render before fetching. Initial load takes 1.5 seconds for what should be a few hundred milliseconds of data.

  1. Map which requests actually depend on each other's data.
  2. Move independent requests to start in parallel at the route level.
  3. Add a cache with keys so repeat views and shared data don't refetch.
  4. Use stale-while-revalidate so subsequent visits render instantly.
  5. Only keep a genuine dependency (project ids before per-project stats) sequential.
Reveal worked answer

This is a waterfall: the header, projects, and stats requests are largely independent but run in series because each fires from a component that only mounts after the previous one renders. I'd hoist the data requirements to the route and kick off the header and projects requests together; per-project stats genuinely need the project ids, so those start as soon as the projects response lands, ideally batched. Adding a keyed cache with stale-while-revalidate then makes repeat visits feel instant. Total load drops to roughly the slowest single request plus the one real dependency.

Verify and go deeper