--- name: TanStack Router description: | Build type-safe, file-based React routing with TanStack Router. Supports client-side navigation, route loaders, and TanStack Query integration. Prevents 20 documented errors including validation structure loss, param parsing bugs, and SSR streaming crashes. Use when implementing file-based routing patterns, building SPAs with TypeScript routing, or troubleshooting devtools dependency errors, type safety issues, Vite bundling problems, or Docker deployment issues. user-invocable: true --- # TanStack Router Type-safe, file-based routing for React SPAs with route-level data loading and TanStack Query integration --- ## Quick Start **Last Updated**: 2026-01-09 **Version**: @tanstack/react-router@1.146.2 ```bash npm install @tanstack/react-router @tanstack/router-devtools npm install -D @tanstack/router-plugin # Optional: Zod validation adapter npm install @tanstack/zod-adapter zod ``` **Vite Config** (TanStackRouterVite MUST come before react()): ```typescript // vite.config.ts import { TanStackRouterVite } from '@tanstack/router-plugin/vite' export default defineConfig({ plugins: [TanStackRouterVite(), react()], // Order matters! }) ``` **File Structure**: ``` src/routes/ ├── __root.tsx → createRootRoute() with ├── index.tsx → createFileRoute('/') └── posts.$postId.tsx → createFileRoute('/posts/$postId') ``` **App Setup**: ```typescript import { createRouter, RouterProvider } from '@tanstack/react-router' import { routeTree } from './routeTree.gen' // Auto-generated by plugin const router = createRouter({ routeTree }) ``` --- ## Core Patterns **Type-Safe Navigation** (routes auto-complete, params typed): ```typescript // ❌ TypeScript error ``` **Route Loaders** (data fetching before render): ```typescript export const Route = createFileRoute('/posts/$postId')({ loader: async ({ params }) => ({ post: await fetchPost(params.postId) }), component: ({ useLoaderData }) => { const { post } = useLoaderData() // Fully typed! return

{post.title}

}, }) ``` **TanStack Query Integration** (prefetch + cache): ```typescript const postOpts = (id: string) => queryOptions({ queryKey: ['posts', id], queryFn: () => fetchPost(id), }) export const Route = createFileRoute('/posts/$postId')({ loader: ({ context: { queryClient }, params }) => queryClient.ensureQueryData(postOpts(params.postId)), component: () => { const { postId } = Route.useParams() const { data } = useQuery(postOpts(postId)) return

{data.title}

}, }) ``` --- ## Virtual File Routes (v1.140+) Programmatic route configuration when file-based conventions don't fit your needs: **Install**: `npm install @tanstack/virtual-file-routes` **Vite Config**: ```typescript import { tanstackRouter } from '@tanstack/router-plugin/vite' export default defineConfig({ plugins: [ tanstackRouter({ target: 'react', virtualRouteConfig: './routes.ts', // Point to your routes file }), react(), ], }) ``` **routes.ts** (define routes programmatically): ```typescript import { rootRoute, route, index, layout, physical } from '@tanstack/virtual-file-routes' export const routes = rootRoute('root.tsx', [ index('home.tsx'), route('/posts', 'posts/posts.tsx', [ index('posts/posts-home.tsx'), route('$postId', 'posts/posts-detail.tsx'), ]), layout('first', 'layout/first-layout.tsx', [ route('/nested', 'nested.tsx'), ]), physical('/classic', 'file-based-subtree'), // Mix with file-based ]) ``` **Use Cases**: Custom route organization, mixing file-based and code-based, complex nested layouts. --- ## Search Params Validation (Zod Adapter) Type-safe URL search params with runtime validation: **Basic Pattern** (inline validation): ```typescript import { z } from 'zod' export const Route = createFileRoute('/products')({ validateSearch: (search) => z.object({ page: z.number().catch(1), filter: z.string().catch(''), sort: z.enum(['newest', 'oldest', 'price']).catch('newest'), }).parse(search), }) ``` **Recommended Pattern** (Zod adapter with fallbacks): ```typescript import { zodValidator, fallback } from '@tanstack/zod-adapter' import { z } from 'zod' const searchSchema = z.object({ query: z.string().min(1).max(100), page: fallback(z.number().int().positive(), 1), sortBy: z.enum(['name', 'date', 'relevance']).optional(), }) export const Route = createFileRoute('/search')({ validateSearch: zodValidator(searchSchema), // Type-safe: Route.useSearch() returns typed params }) ``` **Why `.catch()` over `.default()`**: Use `.catch()` to silently fix malformed params. Use `.default()` + `errorComponent` to show validation errors. --- ## Error Boundaries Handle errors at route level with typed error components: **Route-Level Error Handling**: ```typescript export const Route = createFileRoute('/posts/$postId')({ loader: async ({ params }) => { const post = await fetchPost(params.postId) if (!post) throw new Error('Post not found') return { post } }, errorComponent: ({ error, reset }) => (

Error: {error.message}

), }) ``` **Default Error Component** (global fallback): ```typescript const router = createRouter({ routeTree, defaultErrorComponent: ({ error }) => (

Something went wrong

{error.message}

), }) ``` **Not Found Handling**: ```typescript export const Route = createFileRoute('/posts/$postId')({ notFoundComponent: () =>
Post not found
, }) ``` --- ## Authentication with beforeLoad Protect routes before they load (no flash of protected content): **Single Route Protection**: ```typescript import { redirect } from '@tanstack/react-router' export const Route = createFileRoute('/dashboard')({ beforeLoad: async ({ context }) => { if (!context.auth.isAuthenticated) { throw redirect({ to: '/login', search: { redirect: location.pathname }, // Save for post-login }) } }, }) ``` **Protect Multiple Routes** (layout route pattern): ```typescript // routes/(authenticated)/route.tsx - protects all children export const Route = createFileRoute('/(authenticated)')({ beforeLoad: async ({ context }) => { if (!context.auth.isAuthenticated) { throw redirect({ to: '/login' }) } }, }) ``` **Passing Auth Context** (from React hooks): ```typescript // main.tsx - pass auth state to router function App() { const auth = useAuth() // Your auth hook return ( ) } ``` --- ## Known Issues Prevention This skill prevents **20** documented issues: ### Issue #1: Devtools Dependency Resolution - **Error**: Build fails with `@tanstack/router-devtools-core` not found - **Fix**: `npm install @tanstack/router-devtools` **Issue #2: Vite Plugin Order** (CRITICAL) - **Error**: Routes not auto-generated, `routeTree.gen.ts` missing - **Fix**: TanStackRouterVite MUST come before react() in plugins array - **Why**: Plugin processes route files before React compilation **Issue #3: Type Registration Missing** - **Error**: `` not typed, no autocomplete - **Fix**: Import `routeTree` from `./routeTree.gen` in main.tsx to register types **Issue #4: Loader Not Running** - **Error**: Loader function not called on navigation - **Fix**: Ensure route exports `Route` constant: `export const Route = createFileRoute('/path')({ loader: ... })` **Issue #5: Memory Leak with TanStack Form** (FIXED) - **Error**: Production crashes when using TanStack Form + Router - **Source**: GitHub Issue #5734 (closed Jan 5, 2026) - **Resolution**: Fixed in latest versions of @tanstack/form and @tanstack/react-start. Update both packages to resolve. **Issue #6: Virtual Routes Index/Layout Conflict** - **Error**: route.tsx and index.tsx conflict when using `physical()` in virtual routing - **Source**: GitHub Issue #5421 - **Fix**: Use pathless route instead: `_layout.tsx` + `_layout.index.tsx` **Issue #7: Search Params Type Inference** - **Error**: Type inference not working with `zodSearchValidator` - **Source**: GitHub Issue #3100 (regression since v1.81.5) - **Fix**: Use `zodValidator` from `@tanstack/zod-adapter` instead **Issue #8: TanStack Start Validators on Reload** - **Error**: `validateSearch` not working on page reload in TanStack Start - **Source**: GitHub Issue #3711 - **Note**: Works on client-side navigation, fails on direct page load ### Issue #9: Server Function Validation Errors Lose Structure **Error**: `inputValidator` Zod errors stringified, losing structure on client **Source**: [GitHub Issue #6428](https://github.com/TanStack/router/issues/6428) **Why It Happens**: TanStack Start server function error serialization converts Zod issues array to JSON string in `error.message`, making it unusable without manual parsing. **Prevention**: ```typescript // Server function with input validation export const myFn = createServerFn({ method: 'POST' }) .inputValidator(z.object({ name: z.string().min(2), age: z.number().min(18), })) .handler(async ({ data }) => data) // Client: Workaround to parse stringified issues try { await mutation.mutate({ data: invalidData }) } catch (error) { if (error.message.startsWith('[')) { const issues = JSON.parse(error.message) // Now can use structured error data issues.forEach(issue => { console.log(issue.path, issue.message) }) } } ``` **Official Status**: Known issue, tracking PR for fix ### Issue #10: useParams({ strict: false }) Returns Unparsed Values **Error**: Params typed as parsed but returned as strings after navigation **Source**: [GitHub Issue #6385](https://github.com/TanStack/router/issues/6385) **Why It Happens**: In v1.147.3+, `match.params` is no longer parsed when using `strict: false`. First render works correctly, but after navigation values are stored as strings instead of parsed types. **Prevention**: ```typescript // Route with param parsing export const Route = createFileRoute('/posts/$postId')({ params: { parse: (params) => ({ postId: z.coerce.number().parse(params.postId), }), }, }) // Component: Use strict mode (default) for parsed params function Component() { const { postId } = useParams() // ✓ Parsed as number // const { postId } = useParams({ strict: false }) // ✗ String! // Or manually parse when using strict: false const params = useParams({ strict: false }) const postId = Number(params.postId) } ``` **Official Status**: Known issue, workaround required ### Issue #11: Pathless Route notFoundComponent Not Rendering **Error**: `notFoundComponent` on pathless layout routes ignored **Source**: [GitHub Issue #6351](https://github.com/TanStack/router/issues/6351), [GitHub Issue #4065](https://github.com/TanStack/router/issues/4065) **Why It Happens**: Pathless routes (e.g., `routes/(authenticated)/route.tsx`) don't render their `notFoundComponent`. Instead, the `defaultNotFoundComponent` from router config is triggered. This has been broken since April 2025. **Prevention**: ```typescript // ✗ Doesn't work: notFoundComponent on pathless layout export const Route = createFileRoute('/(authenticated)')({ beforeLoad: ({ context }) => { if (!context.auth) throw redirect({ to: '/login' }) }, notFoundComponent: () =>
Protected 404
, // Not rendered! }) // ✓ Works: Define on child routes instead export const Route = createFileRoute('/(authenticated)/dashboard')({ notFoundComponent: () =>
Protected 404
, }) ``` **Official Status**: Known issue, workaround required ### Issue #12: Aborted Loader Renders errorComponent with Undefined Error **Error**: Rapid navigation aborts previous loader and renders errorComponent with `undefined` error **Source**: [GitHub Issue #6388](https://github.com/TanStack/router/issues/6388) **Why It Happens**: Side effect introduced after PR #4570. When user rapidly navigates (e.g., clicking through list items), aborted fetch requests trigger errorComponent without passing the abort error. **Prevention**: ```typescript export const Route = createFileRoute('/posts/$postId')({ loader: async ({ params, abortController }) => { await fetch(`/api/posts/${params.postId}`, { signal: abortController.signal, }) }, errorComponent: ({ error, reset }) => { // Check for undefined error (aborted request) if (!error) { return null // Or show loading state } return
Error: {error.message}
}, }) ``` **Official Status**: Known issue, workaround required ### Issue #13: Vitest Cannot Read Properties of Null (useState) **Error**: `Cannot read properties of null (reading 'useState')` when running tests with Vitest **Source**: [GitHub Issue #6262](https://github.com/TanStack/router/issues/6262), [PR #6074](https://github.com/TanStack/router/pull/6074) **Why It Happens**: TanStack Start's `tanstackStart()` plugin conflicts with Vitest's React hooks rendering. This is a known duplicate issue with a PR in progress. **Prevention**: ```typescript // Temporary workaround: Comment out tanstackStart() for tests // vite.config.ts export default defineConfig({ plugins: [ // tanstackStart(), // Disable for tests react(), ], test: { environment: 'jsdom' }, }) ``` **Official Status**: PR #6074 in progress to fix ### Issue #14: Throwing Error in Streaming SSR Loader Crashes Dev Server **Error**: Dev server crashes when route loader throws error without awaiting (using `void` instead of `await`) **Source**: [GitHub Issue #6200](https://github.com/TanStack/router/issues/6200) **Why It Happens**: SSR streaming mode can't handle unawaited promise rejections. The error escapes the loader context and crashes the worker process. **Prevention**: ```typescript // ✗ Wrong: void + throw crashes dev server export const Route = createFileRoute('/posts')({ loader: async () => { void fetch('/api/posts').then(r => { throw new Error('boom') // Crashes! }) }, }) // ✓ Correct: Always await or catch export const Route = createFileRoute('/posts')({ loader: async () => { try { const data = await fetch('/api/posts') return data } catch (error) { throw error // Caught by errorComponent } }, }) ``` **Official Status**: Known issue, workaround required ### Issue #15: Prerender Hangs Indefinitely if Filter Returns Zero Results **Error**: Build step hangs when `prerender.filter` returns zero routes **Source**: [GitHub Issue #6425](https://github.com/TanStack/router/issues/6425) **Why It Happens**: TanStack Start prerendering doesn't handle empty route sets gracefully - it waits indefinitely for routes that never come. **Prevention**: ```typescript // ✗ Wrong: Empty filter causes hang tanstackStart({ prerender: { enabled: true, filter: (route) => false, // No routes → hangs! }, }) // ✓ Correct: Ensure at least one route or disable tanstackStart({ prerender: { enabled: true, filter: (route) => route.path === '/' || route.path.startsWith('/posts'), }, }) // Or temporarily disable tanstackStart({ prerender: { enabled: false }, }) ``` **Official Status**: Known issue, workaround required ### Issue #16: Prerendering Does Not Work in Docker **Error**: Build fails in Docker with "Unable to connect" during prerender step **Source**: [GitHub Issue #6275](https://github.com/TanStack/router/issues/6275), [PR #6305](https://github.com/TanStack/router/pull/6305) **Why It Happens**: Vite preview server used for prerendering is not accessible in Docker environment. **Prevention**: ```typescript // vite.config.ts - Make preview server accessible in Docker export default defineConfig({ preview: { host: true, // Bind to 0.0.0.0 instead of localhost }, plugins: [ devtools(), // nitro({ preset: "bun" }), // Remove temporarily if issues persist tanstackStart(), react(), ], }) ``` **Official Status**: PR #6305 in progress ### Issue #17: Route Head Function Executes Before Loader Finishes **Error**: Meta tags generated with incomplete data when `head()` runs before `loader()` **Source**: [GitHub Issue #6221](https://github.com/TanStack/router/issues/6221) **Why It Happens**: The `head()` function can execute before the route `loader()` finishes, causing meta tags to use placeholder or undefined data. **Prevention**: ```typescript // ✗ Wrong: loaderData may not be available yet export const Route = createFileRoute('/posts/$postId')({ loader: async ({ params }) => { const post = await fetchPost(params.postId) return { post } }, head: ({ loaderData }) => ({ meta: [ { title: loaderData.post.title }, // May be undefined! ], }), }) // ✓ Correct: Explicitly await if needed export const Route = createFileRoute('/posts/$postId')({ loader: async ({ params }) => { const post = await fetchPost(params.postId) return { post } }, head: async ({ loaderData }) => { await loaderData // Ensure loaded return { meta: [{ title: loaderData.post.title }], } }, }) ``` **Official Status**: Known issue, workaround required ### Issue #18: Virtual Routes Don't Support Manual Lazy Loading (Community-sourced) **Error**: `createLazyFileRoute` automatically replaced with `createFileRoute` in virtual routes **Source**: [GitHub Issue #6396](https://github.com/TanStack/router/issues/6396) **Why It Happens**: Virtual file routes are designed for automatic code splitting only. Manual lazy routes are not supported - the plugin silently replaces them. **Prevention**: ```typescript // Virtual routes: Use automatic code splitting // vite.config.ts tanstackRouter({ target: 'react', virtualRouteConfig: './routes.ts', autoCodeSplitting: true, // Use automatic splitting }) // Don't use createLazyFileRoute in virtual routes // It will be replaced with createFileRoute automatically ``` **Official Status**: By design (documented behavior) ### Issue #19: NavigateOptions Type Safety Inconsistency (Community-sourced) **Error**: `NavigateOptions` type doesn't enforce required params like `useNavigate()` does **Source**: [TkDodo's Blog: The Beauty of TanStack Router](https://tkdodo.eu/blog/the-beauty-of-tan-stack-router) **Why It Happens**: Type definitions differ between runtime hook and type helper. `NavigateOptions` is less strict. **Prevention**: ```typescript // ✗ Wrong: NavigateOptions doesn't catch missing params const options: NavigateOptions = { to: '/posts/$postId', // No TS error, but params required! } // ✓ Correct: Use useNavigate() return type const navigate = useNavigate() type NavigateFn = typeof navigate // Now type-safe across all usages ``` **Verified**: Cross-referenced with TanStack Query maintainer analysis ### Issue #20: Missing Leading Slash in Route Paths (Community-sourced) **Error**: Routes fail to match when path defined without leading slash **Source**: [Official Debugging Guide](https://tanstack.com/router/latest/docs/framework/react/how-to/debug-router-issues) **Why It Happens**: Very common beginner mistake - using `'about'` instead of `'/about'` causes route matching failures. **Prevention**: ```typescript // ✗ Wrong: Missing leading slash export const Route = createFileRoute('about')({ /* ... */ }) // ✓ Correct: Always start with / export const Route = createFileRoute('/about')({ /* ... */ }) ``` **Verified**: Official documentation, common debugging issue --- ## Cloudflare Workers Integration **Vite Config** (add @cloudflare/vite-plugin): ```typescript import { cloudflare } from '@cloudflare/vite-plugin' export default defineConfig({ plugins: [TanStackRouterVite(), react(), cloudflare()], }) ``` **API Routes Pattern** (fetch from Workers backend): ```typescript // Worker: functions/api/posts.ts export async function onRequestGet({ env }) { const { results } = await env.DB.prepare('SELECT * FROM posts').all() return Response.json(results) } // Router: src/routes/posts.tsx export const Route = createFileRoute('/posts')({ loader: async () => fetch('/api/posts').then(r => r.json()), }) ``` --- **Related Skills**: tanstack-query (data fetching), react-hook-form-zod (form validation), cloudflare-worker-base (API backend), tailwind-v4-shadcn (UI) **Related Packages**: @tanstack/zod-adapter (search validation), @tanstack/virtual-file-routes (programmatic routes) --- **Last verified**: 2026-01-20 | **Skill version**: 2.0.0 | **Changes**: Added 12 new issues from community research (inputValidator structure loss, useParams parsing bug, pathless notFoundComponent, aborted loader errors, Vitest conflicts, SSR streaming crashes, Docker prerender issues, head/loader timing, virtual routes lazy loading limitation, NavigateOptions type inconsistency, leading slash common mistake). Increased error prevention from 8 to 20 documented issues.