tanstack router

Type-safe, file-based routing for React SPAs with route-level data loading and TanStack Query integration

Safety Notice

This listing is imported from skills.sh public index metadata. Review upstream SKILL.md and repository scripts before running.

Copy this and send it to your AI assistant to learn

Install skill "tanstack router" with this command: npx skills add jezweb/claude-skills/jezweb-claude-skills-tanstack-router

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

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()):

// 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 <Outlet /> ├── index.tsx → createFileRoute('/') └── posts.$postId.tsx → createFileRoute('/posts/$postId')

App Setup:

import { createRouter, RouterProvider } from '@tanstack/react-router' import { routeTree } from './routeTree.gen' // Auto-generated by plugin

const router = createRouter({ routeTree }) <RouterProvider router={router} />

Core Patterns

Type-Safe Navigation (routes auto-complete, params typed):

<Link to="/posts/$postId" params={{ postId: '123' }} /> <Link to="/invalid" /> // ❌ TypeScript error

Route Loaders (data fetching before render):

export const Route = createFileRoute('/posts/$postId')({ loader: async ({ params }) => ({ post: await fetchPost(params.postId) }), component: ({ useLoaderData }) => { const { post } = useLoaderData() // Fully typed! return <h1>{post.title}</h1> }, })

TanStack Query Integration (prefetch + cache):

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 <h1>{data.title}</h1> }, })

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:

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):

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):

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):

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:

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 }) => ( <div> <p>Error: {error.message}</p> <button onClick={reset}>Retry</button> </div> ), })

Default Error Component (global fallback):

const router = createRouter({ routeTree, defaultErrorComponent: ({ error }) => ( <div className="error-page"> <h1>Something went wrong</h1> <p>{error.message}</p> </div> ), })

Not Found Handling:

export const Route = createFileRoute('/posts/$postId')({ notFoundComponent: () => <div>Post not found</div>, })

Authentication with beforeLoad

Protect routes before they load (no flash of protected content):

Single Route Protection:

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):

// 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):

// main.tsx - pass auth state to router function App() { const auth = useAuth() // Your auth hook

return ( <RouterProvider router={router} context={{ auth }} // Available in beforeLoad /> ) }

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: <Link to="..."> 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 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:

// 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 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:

// 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, GitHub Issue #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:

// ✗ Doesn't work: notFoundComponent on pathless layout export const Route = createFileRoute('/(authenticated)')({ beforeLoad: ({ context }) => { if (!context.auth) throw redirect({ to: '/login' }) }, notFoundComponent: () => <div>Protected 404</div>, // Not rendered! })

// ✓ Works: Define on child routes instead export const Route = createFileRoute('/(authenticated)/dashboard')({ notFoundComponent: () => <div>Protected 404</div>, })

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 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:

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 <div>Error: {error.message}</div> }, })

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, PR #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:

// 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 Why It Happens: SSR streaming mode can't handle unawaited promise rejections. The error escapes the loader context and crashes the worker process.

Prevention:

// ✗ 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 Why It Happens: TanStack Start prerendering doesn't handle empty route sets gracefully - it waits indefinitely for routes that never come.

Prevention:

// ✗ 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, PR #6305 Why It Happens: Vite preview server used for prerendering is not accessible in Docker environment.

Prevention:

// 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 Why It Happens: The head() function can execute before the route loader() finishes, causing meta tags to use placeholder or undefined data.

Prevention:

// ✗ 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 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:

// 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 Why It Happens: Type definitions differ between runtime hook and type helper. NavigateOptions is less strict.

Prevention:

// ✗ 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 Why It Happens: Very common beginner mistake - using 'about' instead of '/about' causes route matching failures.

Prevention:

// ✗ 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):

import { cloudflare } from '@cloudflare/vite-plugin'

export default defineConfig({ plugins: [TanStackRouterVite(), react(), cloudflare()], })

API Routes Pattern (fetch from Workers backend):

// 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.

Source Transparency

This detail page is rendered from real SKILL.md content. Trust labels are metadata-based hints, not a safety guarantee.

Related Skills

Related by shared tags or category signals.

General

tailwind-v4-shadcn

No summary provided by upstream source.

Repository SourceNeeds Review
2.7K-jezweb
General

tanstack-query

No summary provided by upstream source.

Repository SourceNeeds Review
2.5K-jezweb
General

fastapi

No summary provided by upstream source.

Repository SourceNeeds Review
General

zustand-state-management

No summary provided by upstream source.

Repository SourceNeeds Review
1.2K-jezweb