the data-loading pipeline (runs before render)
TanStack Router runs a typed pipeline before the
route component mounts. beforeLoad is the gatekeeper (auth, redirects,
context); loader fetches the data; the component renders only when
both resolve. No useEffect fetch, no loading flicker, no waterfall —
and parent/child loaders run in parallel.
| step | runs | job | can it abort? |
|---|---|---|---|
beforeLoad(ctx) |
1st — before loader | auth checks, throw redirect(), set up context |
yes — throw redirect / error |
loader(ctx) |
2nd — after beforeLoad | fetch data (fetch API, DB, ensureQueryData) |
yes — throw to bubble to error boundary |
component |
3rd — after loader | useLoaderData() / useRouteContext() |
no — data is already in hand |
1 · the lifecycle you write (edit me)
2 · Babel compiles JSX → element tree
<LoaderDemo/> becomes React.createElement(LoaderDemo).
The beforeLoad promise chains into loader, which chains
into setData + setPhase('done') — the component only flips to "done"
when the loader resolves, mirroring how TanStack Router waits before render.
// (hit "compile & render" to see Babel's output)
3 · live React (the lifecycle, proven)
Click "Run Lifecycle" to fire the simulated pipeline. The timeline tracks beforeLoad (300ms auth check) → loader (500ms fetch) → component. The gold-check does this automatically and asserts the component only shows "John Doe" after both async steps resolve — proving the strict ordering.
phase: — · gold: idle → beforeLoad → loader → done → expect "John Doe"
intent → pattern
| intent | pattern | why |
|---|---|---|
| check auth / guard a route | beforeLoad: async ({'>'}) ={'>'} { throw redirect({ to: '/login' }) } |
throws abort the whole navigation — component never mounts |
| pass data to loader + component | beforeLoad returns {'{ user }'} → read context.user |
context flows down the route tree, typed end-to-end |
| fetch route data | loader: ({'>'}) ={'>'} fetch(url).then(r={'>'}r.json()) |
runs after beforeLoad; result cached per route+params |
| read loaded data in component | Route.useLoaderData() / useRouteContext() |
typed; no useEffect, no loading flicker |
| re-run loader on search-param change | loaderDeps: ({'>'}) ={'>'} ({'{ q: deps.search.q }'}) |
deep-equal gate; only re-fetches when deps actually change |
| fine-grained caching + preloading | ensureQueryData in loader + useSuspenseQuery in component |
router primes the Query cache; component reads from cache |