tanstack-router

from jezweb/claude-skills

Skills for Claude Code CLI such as full stack dev Cloudflare, React, Tailwind v4, and AI integrations.

213 stars24 forksUpdated Jan 26, 2026
npx skills add https://github.com/jezweb/claude-skills --skill tanstack-router

SKILL.md

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

...
Read full content

Repository Stats

Stars213
Forks24
LicenseMIT License