Capítulo 5 de 24

Chapter 5: Kitchen Sink (Full-Featured Demo)

Core Idea

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.

Setup

  • Routing style: file-based
  • Key dependencies: @tanstack/react-router, @tanstack/router-plugin, @tanstack/react-router-devtools, zod (params/search validation), redaxios, immer
  • Structure: src/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.

Code Example

// 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,
})
  • What it demonstrates: 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.

Key Takeaways

  1. Use 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.
  2. params.parse/params.stringify let a route work with rich types (numbers, dates) internally while the URL stays a plain string.
  3. 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.
  4. A single useRouterState({ select: (s) => s.status === 'pending' }) at the root is enough to drive a global loading indicator for every route transition.

Connects To

  • ch006-kitchen-sink-react-query-file-based: the same dashboard/invoices app, but data fetching and mutations go through TanStack Query (useSuspenseQuery, useMutation) instead of a hand-rolled useMutation hook and raw loader calls.
  • authenticated-routes (not covered in depth here): isolates just the auth-gated routing pattern seen via utils/auth.tsx here (beforeLoad redirecting unauthenticated users) into its own focused example.