TanStack Router
| Field | Value |
|---|---|
| Type | Skill Resource |
| Source | ~/.copilot/skills/frontend/references/tanstack-router.md |
| Description | Not 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
useStatefilter/tab/sort/page state with typed URL state. - Wiring loaders into TanStack Query so loaders don’t double-fetch.
- Auth gating via
_authenticatedlayout routes andbeforeLoad.
How it works
- Pick file-based for any app over ~10 routes; code-based only for tiny apps.
- Install —
pnpm add @tanstack/react-routerand the Vite plugin (@tanstack/router-plugin/vite). - Vite config — register
tanstackRouter({ target: 'react', autoCodeSplitting: true }). The plugin generatesrouteTree.gen.ts. - Root route —
__root.tsxdefines layout, devtools, error boundary, andRouterContext(queryClient, auth). - Route per file —
src/routes/posts.$id.tsxbecomes/posts/$id. Underscore prefix for layout-only routes (_authenticated.tsx,_dashboard.tsx). - Zod-validate every search param.
validateSearch: z.object({ page: z.coerce.number().int().positive().catch(1), ... }). Catch invalid → defaults; never throw on URL noise. - Loaders share TanStack Query.
loader: ({ context: { queryClient }, params }) => queryClient.ensureQueryData(postQuery(params.id))— one cache, no double-fetch. - Auth via
beforeLoad. Wrap protected routes in_authenticated.tsx; redirect with the current location as a search param so login can bounce back. - Preload on intent.
defaultPreload: 'intent',defaultPreloadStaleTime: 0. Hover fires the loader. - 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.tsxwith 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 avalidateSearchschema, refactor the component to read fromuseSearch, and update navigation to typed<Link>withsearch={...}. - “Add auth gating to the dashboard routes” → wrap them in
_authenticated.tsxwith abeforeLoadthat redirects to/login?redirect=<current>so login can bounce back.
Self-rubric
- No
anyin 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
fetchin 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.tsis checked in initially so reviewers can see the generated shape. - Validated:
scripts/check_routes.sh <routes-dir>exits 0 — every route usescreateFileRoute.
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
useStatefor 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
- TanStack Router — canonical.
- File-based routing guide.
- Search params guide.
- Authenticated routes recipe.
- Zod — runtime validation. </content>