skip to content

In TanStack Router, how do beforeLoad and router context let an _authenticated layout route gate its children using auth state that lives in a React hook?

level: middleimportance: should knowfreq 40%

answer

  1. hooks cannot run in beforeLoad
  2. createRootRouteWithContext declares the shape
  3. RouterProvider context passes the value
  4. serial, parent first, then invalidate()

basics

~20 s

Declare the context type with createRootRouteWithContext, pass the hook's value through <RouterProvider context={{ auth }}>, and read context.auth in the layout's beforeLoad, throwing redirect() when signed out. beforeLoad runs parent-first before loaders; call router.invalidate() when auth changes.

solid answer

~40 s

Hooks cannot be called inside `beforeLoad` or `loader`, because they are plain functions, not components. So the auth value is **injected**: the root route is created with `createRootRouteWithContext<{ auth: AuthState }>()`, which makes `context` a required, typed option of `createRouter`; a component inside your auth provider calls `useAuth()` and renders `<RouterProvider router={router} context={{ auth }} />`. The `_authenticated` pathless layout then checks `context.auth` in `beforeLoad` and throws `redirect({ to: '/login', search: { redirect: location.href } })`. `beforeLoad` functions run **serially, parent before child**, before any loader, and if one throws, none of its children attempt to load. Whatever `beforeLoad` returns is merged into context for descendants, so it can hand `user` down typed. After login or logout, call `router.invalidate()` so the router recomputes context and re-runs the checks.

code

tsx · 7 lines
tsx
// src/routes/__root.tsx
import { createRootRouteWithContext, Outlet } from "@tanstack/react-router";
import type { AuthState } from "../auth";

export const Route = createRootRouteWithContext<{ auth: AuthState }>()({
  component: () => <Outlet />,
});

go deeper

for a junior

Know that a TanStack Router route can check auth in beforeLoad and throw redirect() to send a signed-out user to /login.

for a middle

Explain the wiring: createRootRouteWithContext types the context, RouterProvider's context prop supplies the hook value, and beforeLoad runs serially before loaders.

for a senior

Build a context chain that hands a typed user to descendants, call router.invalidate() on auth changes, handle failing auth checks with isRedirect(), and keep API authorization mandatory.

for a principal

Decide what belongs in router context across a large app — auth, clients, feature flags — and how its invalidation is triggered without coupling every feature to the router.

## The problem Auth state usually lives in React: an `AuthProvider` with a `useAuth()` hook. TanStack Router's route functions — **`beforeLoad`** and **`loader`** — are plain async functions the router calls outside rendering. React's rules of hooks forbid calling `useAuth()` there. The router's answer is **router context**: typed, hierarchical dependency injection from your app into every route function. ## Declaring and supplying context 1. **Declare the shape** on the root route with `createRootRouteWithContext<{ auth: AuthState }>()({ component: Root })`. Note the double call: the first fixes the type, the second takes the route options. 2. **Satisfy it** when creating the router: `createRouter({ routeTree, context: { auth: undefined! } })`. TypeScript now *requires* the context. 3. **Supply the live value** from React: a component inside `AuthProvider` calls `const auth = useAuth()` and renders `<RouterProvider router={router} context={{ auth }} />`. Every route function now receives `context.auth`, typed. ## Gating a subtree with `beforeLoad` In file-based routing, a file named `_authenticated.tsx` is a **pathless layout route**: it wraps its children without adding a URL segment. Its `beforeLoad` becomes the gate: - if `context.auth.isAuthenticated` is false, `throw redirect({ to: "/login", search: { redirect: location.href } })`, - otherwise return nothing, or return `{ user }` to add it to context. `redirect()` here takes the same options as `navigate`, so `to`, `search` and `replace` are all type-checked. ## Why `beforeLoad` is the right hook The docs give the route loading order: 1. **Matching**, top-down: param parsing and `validateSearch`. 2. **Pre-loading, serial**: each route's `beforeLoad`, parent before child. 3. **Loading, parallel**: component preloads and `loader` functions. Two properties make it a real gate: - a parent's `beforeLoad` runs **before any of its children's** `beforeLoad` functions, - **if `beforeLoad` throws, none of its children attempt to load** — their loaders do not run. So a signed-out visit to `/billing` — a child of the pathless `_authenticated` layout, which adds no URL segment — never starts the billing loader. ## Context accumulates down the tree Whatever a `beforeLoad` returns is **merged into the context** seen by that route's descendants, and the type is inferred. A pattern that follows: | Route | `beforeLoad` returns | Descendants see | |---|---|---| | root | nothing (context from `RouterProvider`) | `{ auth }` | | `_authenticated` | `{ user: auth.user! }` | `{ auth, user }` | | `_authenticated/admin` | `{ permissions }` after a role check | `{ auth, user, permissions }` | Loaders further down can use `context.user` without re-checking, and TypeScript knows it is defined. ## Keeping `/login` outside the gate The login route must not sit under `_authenticated`, or a signed-out user would be redirected from the login page to itself. The mirror check is useful too: `/login`'s own `beforeLoad` can redirect an *already* signed-in user onward to the validated `redirect` search param or a default such as `/dashboard`, so bookmarking the login page does not strand a signed-in user on a form they no longer need. ## Keeping the router in sync with auth changes Context is read when routes load. When the user logs in or out, React state changes, but routes already matched do not re-evaluate by themselves. The docs' pattern is to call **`router.invalidate()`** when auth changes — after `login()` on the login page, after `logout()` in the layout, or from an auth-state listener. Invalidation makes the router recompute context and reload the active matches, so the gate re-runs with the new state. ## Limits worth stating - A route guard is **not a data authorization boundary**; the docs say any endpoint returning private data must authorize the request itself. - If the auth check itself can fail (network error), catch it and rethrow intentional redirects using `isRedirect()`, so a transient error does not surface as a crash. A strong answer names all four moving parts: typed context from `createRootRouteWithContext`, the live value through `RouterProvider`'s `context` prop, a throwing `beforeLoad` that blocks children, and `router.invalidate()` to re-run it.

  • Why is beforeLoad a better place for this check than the layout route's loader?
    `beforeLoad` functions run serially, parent first, and a throw stops the children from loading at all. Loaders run in parallel, so a check in the layout's loader would not stop the child loaders from starting. `beforeLoad` can also return values that are merged into descendants' context.
  • What happens if you forget router.invalidate() after logging out?
    The routes already matched keep their previous context and results, so the protected layout can stay on screen until the next navigation re-runs `beforeLoad`. Invalidating makes the router recompute context and reload the active matches, so the gate sees the logged-out state and redirects.

saying these in an interview costs you the question

  • You can call useAuth() directly inside beforeLoad
  • beforeLoad functions for parent and child run in parallel
  • A beforeLoad redirect still lets child loaders run first
  • Changing the context prop re-runs every beforeLoad automatically without invalidate()
  • A beforeLoad guard is enough to protect the data behind the API