# TanStack Router

> Build type-safe, file-based React routing with TanStack Router. Supports client-side navigation, route loaders, and TanStack Query integration. Use when implementing file-based routing patterns, building SPAs with TypeScript routing, or troubleshooting devtools dependency errors, type safety issues, or Vite bundling problems.

- Skill: `majiayu000/tanstack-router-2` (Agent Skill, multi-file: 2 files)
- Install (CLI): `npx skillmds@latest add majiayu000/tanstack-router-2`
- Raw SKILL.md: https://api.skillmd.com/api/skills/majiayu000/tanstack-router-2/raw
- Safety review: pending (external: skill-scanner PASS, skillspector PASS)
- Works with: Claude Code, Claude.ai, OpenAI Codex
- Category: Web & Frontend
- Author: majiayu000 (https://skillmd.com/u/majiayu000)
- Updated: 2026-09-09
- Page: https://skillmd.com/skills/majiayu000/tanstack-router-2

---


# 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-03
**Version**: @tanstack/react-router@1.144.0

```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 <Outlet />
├── 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 })
<RouterProvider router={router} />
```

---

## Core Patterns

**Type-Safe Navigation** (routes auto-complete, params typed):
```typescript
<Link to="/posts/$postId" params={{ postId: '123' }} />
<Link to="/invalid" /> // ❌ 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 <h1>{post.title}</h1>
  },
})
```

**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 <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**:
```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 }) => (
    <div>
      <p>Error: {error.message}</p>
      <button onClick={reset}>Retry</button>
    </div>
  ),
})
```

**Default Error Component** (global fallback):
```typescript
const router = createRouter({
  routeTree,
  defaultErrorComponent: ({ error }) => (
    <div className="error-page">
      <h1>Something went wrong</h1>
      <p>{error.message}</p>
    </div>
  ),
})
```

**Not Found Handling**:
```typescript
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**:
```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 (
    <RouterProvider
      router={router}
      context={{ auth }} // Available in beforeLoad
    />
  )
}
```

---

## Known Issues & Solutions

**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**
- **Error**: Production crashes when using TanStack Form + Router
- **Source**: GitHub Issue #5734 (known issue, still open as of v1.144)
- **Workaround**: Use React Hook Form or Formik instead

**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

---

## 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)

