loaders & data loading

[check: …]
📖 Read the full guide — route loaders, loaderDeps, the TanStack Query integration pattern, and the cold-vs-preloaded latency race. This page is the interactive companion.

The one idea

A route loader prefetches the data the route needs BEFORE it renders — and you point it at TanStack Query so the data is cached and the next visit is instant.

The router is the only thing that knows where the user is headed before content renders, so it is the right place to kick off fetches. The loader runs on match (cause enter / preload / stay), returns data read via Route.useLoaderData(), and is gated for re-runs by loaderDeps. The recommended pattern with TanStack Query: the loader merely primes the Query cache (queryClient.ensureQueryData) and the component reads via useSuspenseQuery — so a preloaded navigation lands at ~0ms.

runs before render primes the cache loaderDeps = re-run key preload → ~0ms nav pendingComponent after 1000ms

1 · The load lifecycle — navigate, preload, change the deps

A pure state-machine simulation of the route loading lifecycle. Toggle defaultPreload, then drive the route: navigate runs the loader (cause enter), a preloaded navigate primes the cache first and lands at 0ms, and changing loaderDeps (e.g. the page search param) re-runs the loader regardless of staleTime. The gold-check above runs the identical pure functions over fresh states.

defaultPreload
last navigate latency (user-perceived)
— hit navigate to start the lifecycle —

2 · Cold load vs preloaded — the latency race

Same route, same LATENCY = 280ms loader. On the left the user clicks cold (the loader runs during the navigation — they wait). On the right the route was preloaded (loader already ran in the background on hover) so the click is a cache hit at ~0ms. Hit run race to animate both bars; toggle preload to flip the right lane between cold and preloaded.

right lane

cold navigate user waits

click → loader runs (cause: enter) → render
latency paid during navigation
total:

preloaded navigate cache hit

hover → loader runs in background (cause: preload) →
click → cache hit → render · ~0ms
total:

3 · The recommended pattern — loader primes the Query cache

Per TkDodo (the Query author): treat the loader as an event handler. It fires queryClient.ensureQueryData to start the fetch as early as possible (even before the component bundle arrives), and the component reads via useSuspenseQuery. The loader does not hand data to useLoaderData — Query needs Observers (from the hook) for refetch-on-focus, invalidation and GC. Set defaultPreloadStaleTime: 0 so every preload primes the cache.

// 1. wire the QueryClient into router context (createRootRouteWithContext)
const router = createRouter({
  routeTree,
  context: { queryClient },
  defaultPreloadStaleTime: 0,   // ← preload always primes the Query cache
})

// 2. the route loader PRIMES the cache (fire-and-forget event handler)
export const Route = createFileRoute('/posts/$postId')({
  loader: async ({ context, params }) => {
    await context.queryClient.ensureQueryData(postOptions(params.postId))
  },
  component: Post,
})

// 3. the component READS via useSuspenseQuery (not useLoaderData!)
function Post() {
  const { postId } = Route.useParams()
  const { data } = useSuspenseQuery(postOptions(postId)) // data: Post (never undefined)
  return <article>{data.title}</article>
}

Approaches — when each runs, and where the cache lives

Three primitives, one recommended combo. The router cache is per-route (SWR); the Query cache is global and shared across routes — which is why the combo wins for data needed in more than one place.

Honest scope (Jun 2026): this is a deterministic simulation of the documented TanStack Router v1 loader / loaderDeps / Query-integration API (verified against tanstack.com/router and tkdodo.eu) — not a live router instance. Latencies are simulated constants, not real network. Cross-refs: 🔗 navigation_links (preload triggers this loader) · 🔗 router_type_safety (typed loader return) · 🔗 path_search_params (search feeds loaderDeps).