Loader Lifecycle — beforeLoad → loader → component

[check: …]
📖 guide (.md) ← react deep dive
📖 Pair this live React playground with the companion guide (.md) — this page is the rendered ground truth. ↗ Builds on frontend/tanstack-start: loaders & data — the basics of loaders; this bundle goes to the FULL lifecycle: beforeLoad → loader → component, with redirects, context, parallel loaders, and TanStack Query integration.

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.

steprunsjobcan 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

intentpatternwhy
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