React 19 ships use() and <Suspense> but no client-side data cache of its own. Which responsibilities does React deliberately leave to a framework or data library, and how would you decide who owns them in an app you are leading?
answer
- mechanism versus policy
- React defines suspend and retry only
- caching and invalidation are product decisions
- cache() is per-request deduping, not an app cache
- one owner per entity
basics
~20 sReact defines only the render-time protocol: suspend on a pending promise, retry when it settles, surface rejection to an error boundary. Caching, deduping, invalidation, revalidation, retries and server-to-client data transfer are policy, and a framework or library supplies them.
solid answer
~50 sDraw the line at *mechanism versus policy*. React owns the mechanism: a component reads a resource with `use`, suspends while it is pending, retries on settle, and throws rejection to an error boundary. Everything that makes that usable in production is policy — the key schema, deduping in-flight requests, cache lifetime and eviction, staleness and background revalidation, invalidation after a mutation, retry and backoff, and getting server-fetched data to the client so hydration does not refetch. React has no defensible universal default for any of those, so it ships none. As a lead I decide by where the app already gets its structure: if a framework does server-side data loading, let it own the cache and keep client-side reads for genuinely interactive data; if the app is client-rendered and data-heavy, adopt a query library rather than growing your own; only hand-roll for a small, closed set of resources. The failure mode to avoid is two competing caches disagreeing about the same entity.
go deeper
Know that React gives you the loading mechanism only, and that the actual fetching and caching come from a framework or a library rather than from React itself.
Be able to list what is missing from the protocol — keys, deduping, invalidation, revalidation, retries — and explain why suspending on an uncached promise loops without one of them.
Argue the mechanism-versus-policy split and show what you would build versus adopt for a specific app, including the server-to-client handoff so hydration does not refetch everything.
Own the decision and its consequences: one cache owner per entity, an isolation layer so the choice stays reversible, and a single central policy for staleness, invalidation and failure recovery across the product.
## The line React draws It helps to state the split explicitly. **React owns (mechanism):** - reading a resource during render via `use` - suspending on a pending promise and unwinding to the nearest boundary - retrying the render when the promise settles - routing a rejection to the nearest error boundary - scheduling those retries alongside other updates, including transitions **React does not own (policy):** - what a cache key is - deduping concurrent requests for the same key - how long an entry lives and when it is evicted - when data is considered stale and whether to revalidate in the background - what a mutation invalidates - retry counts, backoff, and timeouts - serialising server-fetched data into the client so hydration does not re-request it - pagination, infinite lists, optimistic updates Every item in the second list is a product decision with legitimate opposite answers. A dashboard wants aggressive background revalidation; a document editor wants none. A React that picked one would be wrong for half its users, and — this is the part worth saying out loud in an interview — it would also be a permanent compatibility surface for a library whose whole strategy is to remain a rendering layer. ## Why an integration has to exist at all The protocol is retry-based, and retry re-runs the component. That makes a stable, keyed resource a hard requirement rather than an optimisation: without one you get an unterminating fetch-suspend-retry loop. Something must own that keyed store. React 19's development warning about suspending on an uncached promise is essentially React saying "this responsibility is yours, and here is where it belongs". The one place React does supply help is the server: `cache()` gives Server Components per-request memoization, which is the correct scope for server work. Notice its limits — it is per request, so it is a deduping tool, not an application cache with staleness or invalidation. ## The decision as a lead **1. Where does the data already come from?** If server rendering is the primary path, most reads should be awaited on the server and the results passed down; the interactive remainder is what needs a client cache. Adding a full client query layer on top of a server-loading architecture usually buys duplication rather than capability. **2. How much policy do you actually need?** Count the requirements honestly: deduping and a stable key only? A small in-house cache is defensible and auditable. Do you need staleness windows, refetch on focus, mutation invalidation, pagination, offline behaviour? Those are years of edge cases; adopt something. **3. Who owns an entity's truth?** The expensive failure is two caches holding the same entity and disagreeing — a framework cache and a query library both remembering a user record, invalidated on different triggers. Pick one owner per entity and make the other read through it. **4. What is the exit cost?** A data layer is one of the stickiest dependencies in a frontend, because keys and invalidation calls spread through every feature. Isolate it behind your own hook layer so the components depend on your API, not the vendor's. **5. What is the failure story?** Suspense pairs with error boundaries; a data layer without a retry and recovery policy pushes that burden into every screen. Decide once, centrally, what a failed read does. ## What to avoid saying Two weak answers. One is "React should just include a cache" — it collapses the mechanism/policy distinction the question is testing. The other is "always use a query library" — for an app whose data is loaded on the server and largely static per navigation, that layer is unearned complexity. The strong answer names the responsibilities precisely, then argues for an owner based on the app's rendering architecture and the volume of policy it genuinely needs.
- What does React's cache() give you, and what does it deliberately not give you?In Server Components it memoizes a function's results for the duration of a single server request, so several components can call the same loader with the same arguments and share one call without leaking across users. It is scoped deduping only — no staleness, no revalidation, no invalidation after mutations, and nothing that survives to the client. Application caching is a separate layer.
- How would you keep a data-layer choice from spreading irreversibly through the codebase?Wrap it. Features call your own hooks and loaders, which internally use whichever cache you picked, so keys, invalidation calls and retry policy live in one module. Migration then touches that module plus its tests rather than every screen. It also gives you one place to enforce key conventions and error handling.
- What is the concrete risk of running a framework cache and a query library over the same entities?They invalidate on different triggers, so after a mutation one has fresh data and the other has stale data, and which one a screen sees depends on where it read from. The bugs are intermittent and hard to reproduce. Assign one owner per entity and have the other read through it rather than fetching independently.
saying these in an interview costs you the question
- Says React should simply ship a built-in data cache
- Treats cache() as a general-purpose application cache
- Recommends a query library reflexively for server-loaded apps
- Cannot name any responsibility beyond storing responses
- Ignores server-to-client data transfer after hydration