Capítulo 5 de 24
The flagship "everything at once" example: typed router context (auth), search-param validation with Zod, param parsing/stringifying, custom mutation hooks, breadcrumbs, a global pending spinner, and a dashboard with invoices. It exists as the reference for how the router's advanced features compose together in one real app shape.
@tanstack/react-router, @tanstack/router-plugin, @tanstack/react-router-devtools, zod (params/search validation), redaxios, immersrc/main.tsx creates the router with a typed context: { auth: undefined! } placeholder, then injects the real auth object into RouterProvider at render time. src/routes/__root.tsx uses createRootRouteWithContext<{ auth: Auth }>() to type that context and renders a nav sidebar plus useRouterState for a global loading spinner. src/routes/dashboard.invoices.$invoiceId.tsx shows params + search validation, a loader, and a custom mutation hook together. src/utils/auth.tsx is a plain mutable auth object used for the login-gated demo routes.// src/routes/__root.tsx
export const Route = createRootRouteWithContext<{
auth: Auth
}>()({
component: RootComponent,
})
function RouterSpinner() {
const isLoading = useRouterState({ select: (s) => s.status === 'pending' })
return <Spinner show={isLoading} />
}
// src/routes/dashboard.invoices.$invoiceId.tsx
export const Route = createFileRoute('/dashboard/invoices/$invoiceId')({
params: {
parse: (params) => ({
invoiceId: z.number().int().parse(Number(params.invoiceId)),
}),
stringify: ({ invoiceId }) => ({ invoiceId: `${invoiceId}` }),
},
validateSearch: (search) =>
z.object({
showNotes: z.boolean().optional(),
notes: z.string().optional(),
}).parse(search),
loader: ({ params: { invoiceId } }) => fetchInvoiceById(invoiceId),
component: InvoiceComponent,
})
params.parse/params.stringify coerce a URL string param into a typed value (here, a number) and back; validateSearch gives fully-typed, validated search params (search.showNotes, search.notes) accessible via Route.useSearch(); createRootRouteWithContext<T>() types the context object every route's loader and beforeLoad can read.createRootRouteWithContext<{...}>() plus a context object passed to createRouter (and reinjected into RouterProvider) to thread dependencies like auth or a query client through every route without prop drilling.params.parse/params.stringify let a route work with rich types (numbers, dates) internally while the URL stays a plain string.validateSearch with Zod turns arbitrary query strings into a validated, typed search object; combine with navigate({ search: (old) => ({ ...old, notes }) }) to update just one search key immutably.useRouterState({ select: (s) => s.status === 'pending' }) at the root is enough to drive a global loading indicator for every route transition.useSuspenseQuery, useMutation) instead of a hand-rolled useMutation hook and raw loader calls.utils/auth.tsx here (beforeLoad redirecting unauthenticated users) into its own focused example.