Skip to content

TanStack Router

FieldValue
TypeSkill Resource
Source~/.copilot/skills/frontend/references/tanstack-router.md
DescriptionNot specified

Source Content

TanStack Router

Set up TanStack Router so the type system, the URL, and the data cache all agree. Absorbed from the former tanstack-router-architect skill. File-based routes, Zod-validated search params, loaders that share TanStack Query, and beforeLoad auth gates — no any-typed escape hatches.

Use for

  • Starting a new React + TypeScript app with TanStack Router.
  • Migrating from React Router (especially v6 → TanStack).
  • Replacing useState filter/tab/sort/page state with typed URL state.
  • Wiring loaders into TanStack Query so loaders don’t double-fetch.
  • Auth gating via _authenticated layout routes and beforeLoad.

How it works

  1. Pick file-based for any app over ~10 routes; code-based only for tiny apps.
  2. Installpnpm add @tanstack/react-router and the Vite plugin (@tanstack/router-plugin/vite).
  3. Vite config — register tanstackRouter({ target: 'react', autoCodeSplitting: true }). The plugin generates routeTree.gen.ts.
  4. Root route__root.tsx defines layout, devtools, error boundary, and RouterContext (queryClient, auth).
  5. Route per filesrc/routes/posts.$id.tsx becomes /posts/$id. Underscore prefix for layout-only routes (_authenticated.tsx, _dashboard.tsx).
  6. Zod-validate every search param. validateSearch: z.object({ page: z.coerce.number().int().positive().catch(1), ... }). Catch invalid → defaults; never throw on URL noise.
  7. Loaders share TanStack Query. loader: ({ context: { queryClient }, params }) => queryClient.ensureQueryData(postQuery(params.id)) — one cache, no double-fetch.
  8. Auth via beforeLoad. Wrap protected routes in _authenticated.tsx; redirect with the current location as a search param so login can bounce back.
  9. Preload on intent. defaultPreload: 'intent', defaultPreloadStaleTime: 0. Hover fires the loader.
  10. Typed Links everywhere. <Link to="/posts/$id" params={{ id }} search={{ tab }} /> — compiler enforces shape. No string concat.

Examples

  • “Start a new React + TS app with TanStack Router” → install, wire tanstackRouter({ target: 'react', autoCodeSplitting: true }) in Vite, set up __root.tsx with RouterContext (queryClient + auth), and scaffold a first route.
  • “Migrate this React Router v6 app to TanStack” → map the route tree to file-based, port loaders to ensureQueryData, replace string URLs with typed <Link>, and validate every search param with Zod + .catch().
  • “The filter/sort state is in useState — move it to the URL” → add a validateSearch schema, refactor the component to read from useSearch, and update navigation to typed <Link> with search={...}.
  • “Add auth gating to the dashboard routes” → wrap them in _authenticated.tsx with a beforeLoad that redirects to /login?redirect=<current> so login can bounce back.

Self-rubric

  • No any in route definitions. Search, params, loader data are all inferred.
  • Every search param has a Zod schema with .catch(). URL noise can’t crash a route.
  • Loaders go through TanStack Query. No raw fetch in a loader.
  • Auth checked in beforeLoad, not in a component effect. No flash of protected content.
  • Typed <Link> for every navigation. No string URLs.
  • routeTree.gen.ts is checked in initially so reviewers can see the generated shape.
  • Validated: scripts/check_routes.sh <routes-dir> exits 0 — every route uses createFileRoute.

scripts/check_routes.sh <routes-dir> confirms every route file uses createFileRoute and warns when a dynamic-segment route has no loader:.

Anti-patterns refused

  • useState for filter/tab/sort/page → URL state.
  • String concat for URLs → typed <Link>.
  • fetch() inside a loader → bypasses cache, double-fetches on mount.
  • Auth check inside a component → use beforeLoad.

References