skip to content

A React codebase fetches everything with `useEffect` plus `fetch`. How do you decide whether to move that onto a caching data layer or onto server-side fetching, and when is fetching in an effect still the right answer?

level: principalimportance: should knowfreq 38%

answer

  1. symptoms first, dependency second
  2. cache and server fetching solve different halves
  3. two sources of truth is the worst state
  4. migrate by resource, not by file
  5. small app, one reader, effect is fine

basics

~20 s

Decide from symptoms, not fashion: duplicate requests, refetch-after-mutation rules, SSR or SEO needs, and per-component loading boilerplate justify a change. A one-screen app with unshared, one-shot data is fine as it is; the two alternatives solve different problems.

solid answer

~50 s

I would drive it from the failure list rather than from a preference. Count the symptoms actually present: the same resource fetched by several components, staleness rules being reinvented per screen, waterfalls on the critical path, content that must be in the server HTML for SEO or previews, and copies of the same entity drifting apart after a write. Then pick the alternative that matches — a caching layer if the problem is duplication, freshness and shared server state; server-side fetching or a route loader if the problem is the round trip, the waterfall, or empty initial HTML. They are not interchangeable, and doing both is common. Fetching in an effect stays correct for one-shot, component-local data with no sharing and no indexing requirement. The real risk is a half-finished migration: two sources of truth for the same entity is worse than either end state, so I would sequence it by resource and set a rule that new code uses the target model.

go deeper

for a junior

You are not expected to make this call, but know that effect fetching is a legitimate starting point and that alternatives exist because of duplication, waterfalls and missing server HTML — not because effects are forbidden.

for a middle

Be able to name which specific problem each alternative solves and to argue that a small app with unshared data needs neither. Avoid answering with a library name before naming a symptom.

for a senior

Show you would gather evidence — duplicate requests, post-mutation staleness, LCP on a throttled connection — before proposing a change, and that you can pick the alternative matching the evidence rather than adopting both by default.

for a principal

Own the transition and the cost model: sequence by resource so no entity has two sources of truth, set a rule for new code and a deletion date for the old path, and commit to metrics that decide whether the migration was worth it.

## Decide from symptoms, not from fashion "Fetching in effects is outdated" is not an argument, and an interviewer will push on it. The defensible version enumerates the concrete costs and checks which ones this codebase is actually paying. The symptom list is the leaf's own failure list, turned into evidence you can look for: - **Duplicate requests.** Several components or screens read the same resource, and the network panel shows it fetched repeatedly, including on every back-navigation. - **Freshness rules reinvented per screen.** Someone has already written "refetch on focus" or "poll every 30 seconds" by hand, in more than one place, slightly differently. - **Post-mutation staleness.** After a save, some other part of the UI keeps showing the old value until a remount. - **Waterfalls on the critical path.** Nested effect fetches serialise round trips before the first meaningful paint. - **Empty server HTML.** The content must be indexable, must generate a link preview, or is the LCP element, and it is not in the markup. - **Loading and error boilerplate.** The same five branches copy-pasted across dozens of components, inconsistently. If you find one or two of these on a small app, fix them in place. If you find most of them, you are already maintaining a bad data layer — the choice is whether to keep writing it yourself. ## The two alternatives solve different problems This is the distinction most candidates blur. **A caching data layer** keeps fetching on the client and adds identity, dedupe, reuse across mounts, invalidation, retry and a status machine. It attacks duplication, staleness and boilerplate. It does *not* remove the round trip, and it does not put anything in the server HTML. **Server-side fetching** — a server component that awaits during render, or a route loader that resolves before render — attacks latency, waterfalls and empty initial HTML by moving the request next to the data. It does not, on its own, give you client-side revalidation or optimistic updates for data that keeps changing while the user sits on the page. So an app whose problem is "three widgets fetch the same profile and drift apart" needs the cache. An app whose problem is "the article body is not in the HTML and the page takes three round trips to become useful" needs server-side fetching. Many real apps need both, with server-rendered initial data and a client cache for the parts that mutate. ## When effects are still right Be explicit that the answer is not "never", or the position sounds dogmatic: - Data used by exactly one component, fetched once, never shared. - Content behind an interaction that is not indexable and not on the critical path. - Data that depends on browser-only state and cannot exist on the server. - An admin tool, an internal dashboard, or a prototype where a dependency and its concepts cost more than the boilerplate saves. A small app with three screens does not need a caching layer, and adding one there is a real cost: another mental model for every contributor, another upgrade path, another set of subtle behaviours to learn. ## Sequencing the migration The most senior part of the answer is usually the transition, not the destination. - **Migrate by resource, not by file.** Move every reader of one entity at once, so that entity never has two sources of truth. Half-migrated entities produce exactly the inconsistency you set out to remove. - **Start where the pain is measurable** — the screen with the duplicate requests, or the route whose LCP is failing — so the first increment justifies the rest. - **Stop the bleeding first.** Agree that new code uses the target model even before old code moves; otherwise the migration never converges. - **Set a deletion deadline for the old path.** Keeping both indefinitely is the failure mode: every contributor then has to learn two data models and decide between them. - **Watch the bundle and the boundary.** A client cache is client bytes; server-side fetching moves work into infrastructure you now have to operate and observe. Neither is free. ## What to measure afterwards Commit to evidence: requests per page load, time to first meaningful content on a throttled connection, the count of hand-written loading branches removed, and post-mutation staleness bugs reported. If none of those move, the migration was taste, not engineering — and being willing to say so is what distinguishes a judgment answer from a preference.

  • Someone argues that server components make client-side caching libraries unnecessary. How do you respond?
    They overlap less than it sounds. Server-side fetching removes the client round trip and fills the initial HTML, which is most of the value for read-mostly pages. But data that changes while the user watches it — lists edited in place, optimistic updates, revalidation on focus or reconnect — still needs client-side cache semantics. The usual production shape is server-rendered initial data plus a client cache for the interactive parts, not one replacing the other.
  • You have limited time and only one screen is painful. Do you migrate the whole app?
    No. Migrate that screen's resources end to end, keep every reader of those entities on the new path, and leave the rest alone. A vertical slice proves the approach, delivers a measurable win, and avoids the worst state — an entity read through two mechanisms at once. Pair it with a rule that new code uses the target model, so the boundary shrinks over time instead of drifting.
  • What would convince you to leave a codebase on useEffect fetching entirely?
    A small surface with no sharing: a handful of screens, each resource read by one component, no indexing or link-preview requirement, no polling or refetch rules, and no complaints about stale data after writes. In that shape the boilerplate is a few dozen lines and a data layer adds a dependency, a mental model and a bundle cost for problems the app does not have.
  • How do you keep a migration like this from stalling half-finished?
    Sequence by resource so each step is complete for one entity, agree that new code only uses the target model, and set an explicit date to delete the old path rather than leaving it as a fallback. Track a number that should move — requests per load, or time to meaningful content — so progress is visible. An indefinite two-model codebase is the outcome that costs more than either destination.

saying these in an interview costs you the question

  • Says useEffect fetching is deprecated in React 19
  • Treats a query library and server components as interchangeable
  • Adopts a data layer without naming a symptom it fixes
  • Plans a file-by-file migration that splits one entity across two paths
  • Ignores bundle size and team learning cost in the decision

context