react-brain
Browse decisions
React & language foundations 3App architecture 9UI 12Platform & native 9Build, test, observe, secure 6AI in React apps 3stack composerdecision recordscensusstaleness bencharchitecturechangelogmethodologyroadmap

entriesapp-architecture · verified 2026-07-10 · react + react-native

Data fetching & caching

reviewedconfidence: hightrack: heldthis tier holds 75% on the public scorecard (6/11 graded · 0 overturned) →

related decisions: state · p2p

cited by: p2p · networking

re-verified 3× — 2026-07-13 · 2026-07-10 · 2026-07-06 · changelog

recommendation

TanStack Query for REST/server-state caching — the default for client-side data fetching.

  • GraphQL backend → Apollo Client (or Relay for first-party TS + scale)
  • client + server both TypeScript, private API → tRPC (type-safety with no codegen)
  • offline / local-first / collaborative → TanStack DB or Zero
  • P2P / serverless / Holepunch backend → data lives in the Hypercore/Autobase stack queried locally via hrpc; a client cache (TanStack Query) is N/A by design — see RB-E-P2P
  • server-rendered web → fetch in RSC and skip a client cache where you can
  • already on Redux Toolkit → RTK Query

Options & tradeoffs

the field considered — and why each one isn’t the default here

optiontradeoffevidence
TanStack Query (5.x)cache, retries, invalidation; framework-agnostic61.2M/wk · ships in 11/34
SWRlighter, hook-first13.2M/wk · ships in 2/34
RTK Queryif already on Redux Toolkit
Apollo Client (4.x) / urql / Relay (21)GraphQL: Relay v21 adds first-party TS + experimental RSC; Apollo 4.2 type-safe defaults5.5M/wk · ships in 2/34
tRPCend-to-end type-safe RPC (no schema/codegen) when client + server are both TS; not for public/polyglot APIs
TanStack DB / Zero / TinyBase / RSClocal-first sync/stores (TanStack DB, Zero 1.0, TinyBase 9) and server-fetched RSC are emerging data-layer alternatives; TanStack DB 0.6 (2026-03) added SQLite-backed persistence (browser WASM, RN/Expo, Node, Electron) + offline transactions — real offline now, still 0.x62k/wk · ships in 1/34

evidence: npm weekly downloads (signals snapshot) · “ships in n/D” = adoption across the production-app census, honest denominators

npm weekly downloads (from the corpus's last signals run): @tanstack/react-query 61.2M · swr 13.2M · @apollo/client 5.5M · urql 1.0M · @rocicorp/zero 62k · @tanstack/react-db 403k

Verified notes

Server state ≠ client state (see RB-E-STATE). TanStack Query is the #1 React library in State of React 2025; it's transport- and platform-agnostic, so the data layer is shared code across web + RN. Adversarially stress-tested 2026-06-25 (SURVIVES, high): RSC complements (doesn't displace) the cache; TanStack DB builds ON TanStack Query; Zero reached 1.0 (2026-06) with React Native support.

Canonical reading

Editorial annotations on why each piece matters — the articles themselves are the originals; read them there.

Creating Query AbstractionsDominik Dorfmeister (TkDodo), TanStack Query maintainer

Maintainer-authored architecture guidance: queryOptions objects (not custom hooks) are THE abstraction layer for queries — composable, framework-portable, and reusable across useQuery/prefetch/suspense call sites. The how-to-structure companion to 'Why You Want React Query'.

TanStack Router and QueryDominik Dorfmeister (TkDodo)

The selection question this entry's TanStack rows raise: Router's cache is per-route, Query's is global — when route-loader caching alone suffices, when you want the full query cache, and how to layer them without double-fetching. Maintainer-authored.

Why You Want React QueryDominik Dorfmeister (TkDodo), TanStack Query maintainer

Takes a naive fetch-in-useEffect and exposes the five real bugs it hides (race conditions, missing loading/empty states, stale data+error pairs, StrictMode double-fire), then shows how a server cache fixes them. The durable answer to why-not-just-useEffect.

From latency to instant: modernizing GitHub Issues navigation performanceGitHub Engineering

Engineering case study on local-first caching, stale-while-revalidate, service-worker hard-navigation, and preheating vs naive prefetch, with measured percentile gains. Durable client-side data-caching patterns at scale.

The Best Loading States Are No Loading StatesJenna Smith (jjenzz)

Argues loading UI belongs at the app/route level (transitions + preload-on-hover/intersection) rather than scattered component spinners, using blank regions as a preload diagnostic. Durable, framework-agnostic data-loading-UX thinking.

use(): the hook that breaks the rules (on purpose)Sascha Becker

Deep-dive on the use() hook — unwrapping promises/context at render under Suspense, the promise-identity caching requirement, and why it can legally run conditionally unlike other hooks. The modern data-fetching primitive explained.

Real-time cache invalidation (SSE + tRPC + Redis + BullMQ)Armand Sallé

A full architecture for pushing invalidation EVENTS (not data) over SSE to drive TanStack Query cache updates, with reference-counted subscriptions and horizontal-scaling guidance. The real-time dimension the other DATA readings don't cover.

Sources

The full explanation

The reviewed long-form essay behind this entry — the why, not a how-to. Also on GitHub.

About data fetching & server state in React & React Native

Diataxis: Explanation. This page explains how to think about the data layer and why the default is what it is. The candidate list with one-line tradeoffs lives in the index entry RB-E-DATA; this is its companion. It is not step-by-step how-to.

Server state is the actual problem

A fetch() is easy. What is hard is everything around it: the response can go stale, two components asking for the same thing should dedupe, a failure should retry, a mutation elsewhere should invalidate the cached copy, and the screen should show loading / error / empty states without you hand-wiring a boolean soup. That cluster of problems is server-state management, and it is the reason a dedicated library exists at all. (The companion distinction — server state vs client state — is the spine of RB-E-STATE; read that first if it isn't second nature.)

The recurring anti-pattern is solving server-state problems inside a client store (Zustand/Redux): you end up re-implementing caching, isLoading flags, and refetch effects by hand. The moment you see that code, the state wanted a server-state tool.

The default, and why

TanStack Query for REST / server-state caching. It is the default for client-side data fetching, and a healthy app pairs it with a client-state lib (it does not replace one).

This is the consensus pick, not a hunch: TanStack Query is the single most-used library in the React ecosystem in State of React 2025. It earns that by doing the hard cluster above generically — cache, dedupe, background refetch, retry, invalidation — while staying transport- and framework-agnostic (REST, GraphQL, RPC; web and React Native alike). That last property matters for react-brain's thesis: your data layer is mostly the same code on web and native, so it belongs in a shared package (RB-E-CROSSPLATFORM).

The landscape, and when each one wins

Treat these as reasons to deviate, keyed to what your backend is and how your client talks to it.

SWR — the lighter, hook-first cousin. Same stale-while-revalidate model, smaller surface. A reasonable choice when your needs are modest and you value minimalism over TanStack Query's breadth of features. The two are more alike than different; pick by ergonomic taste, not capability anxiety.

RTK Query — server-state caching that ships inside Redux Toolkit. The deciding factor is sociological, not technical: choose it when you are already on RTK and want one mental model and one devtools surface. Do not adopt Redux in order to use RTK Query — that's the tail wagging the dog; use TanStack Query standalone instead.

GraphQL clients — Apollo Client / urql / Relay — these win only when your backend is actually a GraphQL graph. Apollo is batteries-included with a normalized cache; urql is lighter and more composable; Relay is compiler-driven and the most opinionated — it now ships first-party TypeScript and experimental RSC support, and pays off at scale and with Meta-style data-masking discipline, at the cost of a steeper model. Reaching for a GraphQL client over a REST endpoint is choosing a query language, not just a cache.

tRPC — end-to-end type-safe RPC with no schema and no codegen, when client and server are both TypeScript and the API is private to your app. Its superpower is that your server's types are your client's types — change a procedure, the call site fails to compile. Its boundary is the flip side: it deliberately couples client and server, so it is the wrong choice for a public API or a polyglot/mobile-shared backend consumed by non-TS clients.

Local-first / sync engines — TanStack DB, Zero, TinyBase — a different paradigm: the client holds a queryable local store that syncs with the server, so reads are instant and the app works offline and collaboratively. This is powerful for the right product (Linear-style apps), but it is an architecture commitment, not a drop-in cache — adopt it because offline/realtime is a core requirement, not to avoid a loading spinner.

RSC (web only) — when components render on the server, the server can fetch directly and you may not need a client-side cache for read paths at all. This shifts the question from "which client cache?" to "what runs where?" (see RB-E-META-FRAMEWORKS). The catch lives on the mutation path — Server Functions have a real DoS-CVE history (RB-E-SECURITY) — and RSC is a web concern; it does not apply to React Native.

Tradeoffs and failure modes to name out loud

  • Hand-rolled caching in a client store — the cardinal error (see above). Move it to a server-state tool; this usually deletes code.
  • GraphQL without a graph — adopting Apollo/Relay against plain REST endpoints buys query-language overhead with little of the benefit. Use it when the graph is real.
  • tRPC across a trust/language boundary — great inside one TS codebase; wrong for public or polyglot APIs precisely because of the coupling that makes it nice internally.
  • Request waterfalls — fetching sequentially down the tree (each child awaiting its parent) is a latency tax; prefetch on intent (route loaders, hover) and parallelize.
  • Local-first as a quick win — it is a paradigm, not a patch; the migration cost is real.

How it interacts with the rest of the stack

  • Client state (RB-E-STATE) — the two are complementary halves. Server state → TanStack Query; client/UI state → Zustand/Jotai. Don't merge them.
  • Meta-frameworks (RB-E-META-FRAMEWORKS) — RSC and framework loaders (Next.js, TanStack Start, React Router) are also data-fetching surfaces; on the web your data strategy and your framework choice are entangled.
  • React core (RB-E-REACT-CORE)use() + Suspense let you read async resources at render time, which dovetails with how modern data libraries expose data.
  • Cross-platform (RB-E-CROSSPLATFORM) — because TanStack Query is platform-agnostic, your fetching/caching logic is shared code; put it in the common package, not per-platform.

Migrating (the short version)

The migration that pays off is pulling server state out of a client store into TanStack Query — it typically removes more code than it adds (the hand-rolled cache, loading flags, and refetch effects all disappear). Migrations between server-state libraries (SWR ↔ TanStack Query) rarely justify themselves without a concrete missing feature. Step-by-step mechanics are a how-to concern; the principle is "consolidate server state into one cache, by responsibility."

In one paragraph

The data layer's hard problem is server state — staleness, dedupe, retry, invalidation — so use a tool built for it: default to TanStack Query (the most-used React library), pair it with a client-state lib, and keep server state out of that store. Deviate for a reason: RTK Query if already on Redux, a GraphQL client if you truly have a graph, tRPC for a private all-TypeScript API, local-first engines when offline/realtime is core, and RSC when the server can fetch for you (web only).


See also: RB-E-STATE (client vs server state), RB-E-META-FRAMEWORKS (RSC / loaders), RB-E-REACT-CORE (use() + Suspense), RB-E-CROSSPLATFORM (sharing the data layer).

Related in app-architecture: state · p2p · nav · meta-frameworks · forms · networking · crossplatform · desktop