← Default view
Data-flow schematic · React 19 · CSR admin SPA

Router-driven data loading: who owns the cache?

Sheet
DL-03
Rev 2026.07
Backend
REST / OpenAPI
Render
Client only (no SSR)
Depth
survey · 7 min
Refs
12 sources
The query library owns the cache. The router stays thin.

Framework-style loader/clientLoader route data only pays for itself with SSR/streaming — which a pure SPA doesn't have [6]. Pick RTK Query if you want OpenAPI codegen, else TanStack Query for the lighter footprint [9]. Choose the router for its routing — and if it's TanStack Router ⭐ 15k, use loaders only to prime the query cache with the router's own cache turned off [3].

01

Three cache-ownership models

follow the arrows — the double-bordered node owns the cache
A
Router owns the data
React Router ⭐ 56k · framework mode
Route matchbefore render
loader / clientLoaderfetch route data
THE ROUTERis the cache · per-route
useLoaderData → Component
action + <Form>auto-revalidates every loader on the page
Trigger: route match, pre-render [1]. Cache is per-route — no cross-route sharing.
✗ SPA: second cache, no streaming payoff
B
Router primes an external cache
Route matchQueryClient in router context
loader: ensureQueryDatafire-and-forget · don't await
TANSTACK QUERYowns cache · dedup · invalidation
useSuspenseQuery → Componentreads the primed query
Router cache OFFdefaultPreloadStaleTime: 0
One request: dedup latches prime + read onto a single fetch [5]. One player controls caching.
△ works, but loaders are optional in CSR
C
Router does no data
Query lib + thin router (wouter ⭐ 7.9k)
Router: URL → componentno loaders, no data contract
Component mountsrequests what it needs
useQuery / RTK hookon mount
QUERY LIBRARYowns everything data
Simplest wiring: TanStack Query ⭐ 50k or RTK Query, cross-route shared cache, tag/key invalidation.
✓ the CSR-SPA fit
02

Who owns what

comparison band · amber column = the recommended owner
Concern A · Router loaders B · TanStack Router + Query C · Query lib + thin router
Fetch trigger Route match, before render Loader primes, component reads Component (hook) mount
Cache owner The router TanStack Query Query lib
Cross-route sharing per-route only shared query cache shared query cache
Invalidation Auto-revalidate all loaders on mutation invalidateQueries / tags Tags (RTK) / keys (TSQ)
OpenAPI codegen hand-wired community (orval / openapi) RTK Query official
Pays off mainly with SSR / streaming either, best with SSR CSR SPA
Router footprint ⭐ 56k ⭐ 15k ⭐ 7.9k (wouter)
03

The duplication trap

loaders and a query cache both want to be the source of truth
when using external caching libraries like TanStack Query, it's best to just turn this off completely because we'd only want one player to control caching
TkDodo — set defaultPreloadStaleTime: 0 so the router never caches [3]
Treat the loader like a simple fire-and-forget event handler that only primes our cache without actually returning any data
Don't await, don't read useLoaderData — read via useSuspenseQuery so the query is GC/refetch-eligible [3]
No shared caching/deduping between routes. No built-in mutation APIs. Works great for apps that share little data between routes.
TanStack Router's built-in cache — exactly the things a data-heavy admin app needs, so you delegate them to Query [2]
clean integration requires choosing one primary data-fetching strategy rather than running both in parallel
Running framework loaders and Query together is the anti-pattern — CSR: Query alone; SSR: embrace loaders [6]
04

Annotations · race conditions & streaming

two failure classes both models must answer

Race conditions

React Router mirrors the browser — “interrupted navigations … cancel in-flight data requests” and it “commits all fresh revalidation responses and cancels stale requests” [7]. A type-ahead via fetcher.submit never shows stale results.

Caveats: v7 re-runs loaders by default after mutations — add shouldRevalidate to stop unwanted refetches [12]; fetcher revalidation still has documented race edge-cases [8]; backend integrity is out of scope [7].

TanStack Query handles the same class structurally: keys identify data, in-flight requests dedupe, stale responses are ignored, and mutation → invalidation is explicit and cross-route [9].

Suspense & streaming

Loader-based streaming (deferred data + Await) is a server-rendering feature — its value is flushing HTML while data resolves. TanStack's Query integration exists largely to “automate SSR dehydration/hydration and streaming” [4].

In a CSR SPA there is no HTML stream, so this benefit evaporates. You're left with client Suspense boundaries — which useSuspenseQuery gives you without any router loader at all [3].

Waterfalls in model B are avoided by not suspending in the loader and using useDeferredValue to keep old UI while new data loads [5].

05

Build spec · data-heavy CSR admin SPA

one cache, owned by the query layer
1

Query library owns the data

RTK Query if OpenAPI codegen matters (official generator → typed endpoints + tag invalidation) [10], else TanStack Query for the lighter footprint and strongest cache [9]. Both command large 2026 adoption — Query ~12M weekly npm downloads [11].

2

Don't adopt React Router framework loaders for a pure SPA

With no SSR, loader/clientLoader add a second cache and a route-data contract with no streaming payoff — and v7's default revalidation becomes tuning overhead [6][12].

3

Router chosen for routing, not data

TanStack Router ⭐ 15k gives type-safe params/search you feed straight into query keys. If you use its loaders, use them only to ensureQueryData with defaultPreloadStaleTime: 0, and read via useSuspenseQuery — never useLoaderData for the payload [3]. A minimal router (wouter ⭐ 7.9k) is a fine model-C pick if you don't need loader-level prefetch.

06

Parts list · sources

12 references
Ref
Source · claim
Type
1
React Router — Data LoadingLoaders fetch before render; actions auto-revalidate all loaders after a mutation.
official
2
TanStack Router — Data LoadingBuilt-in cache suits low-sharing apps; no cross-route dedup, no mutation APIs.
official
3
TkDodo — Router and QueryDisable router cache; loaders fire-and-forget prime; read via useSuspenseQuery.
vendor-blog
4
TanStack — Query integrationIntegration automates SSR dehydration/hydration and streaming.
official
5
master.dev — Data loading (pt 2)Prefetch + useSuspenseQuery dedupe to one request; useDeferredValue avoids waterfalls.
vendor-blog
6
react-router · discussion #14037CSR: Query alone works; running loaders + Query in parallel is an anti-pattern. ⭐ 56k
forum
7
React Router — Race ConditionsCancels interrupted requests, commits only fresh responses; backend integrity out of scope.
official
8
react-router · issue #10473Fetcher revalidation still has documented race-condition edge cases. ⭐ 56k
forum
9
RTK Query vs TanStack QuerySame caching/dedup problems; RTK Query tags auto-refetch related queries on mutation.
vendor-blog
10
@rtk-query/codegen-openapiOfficial OpenAPI code generator → fully-typed endpoints from a spec.
official
11
byteiota — TanStack 2026By early 2026 TanStack Query reached ~12M weekly npm downloads.
vendor-blog
12
Contributing to React Routerv7 re-runs loaders by default after mutations; use shouldRevalidate to opt out.
vendor-blog
07

Related sheets

part of a 5-angle decision framework

↑ Parent expedition

Which React router to adopt in 2026: a decision framework for a client-rendered admin SPA — across five angles, one recurring fact: keep data-fetching out of the router either way.