tanstack-router
TanStack Router patterns for type-safe, file-based routing. Covers installation, route configuration, typed params/search, layouts, and navigation. Use when setting up routes, implementing navigation, or configuring route loaders.
Best use case
tanstack-router is best used when you need a repeatable AI agent workflow instead of a one-off prompt.
TanStack Router patterns for type-safe, file-based routing. Covers installation, route configuration, typed params/search, layouts, and navigation. Use when setting up routes, implementing navigation, or configuring route loaders.
Teams using tanstack-router should expect a more consistent output, faster repeated execution, less prompt rewriting.
When to use this skill
- You want a reusable workflow that can be run more than once with consistent structure.
When not to use this skill
- You only need a quick one-off answer and do not need a reusable workflow.
- You cannot install or maintain the underlying files, dependencies, or repository context.
Installation
Claude Code / Cursor / Codex
Manual Installation
- Download SKILL.md from GitHub
- Place it in
.claude/skills/tanstack-router/SKILL.mdinside your project - Restart your AI agent — it will auto-discover the skill
How tanstack-router Compares
| Feature / Agent | tanstack-router | Standard Approach |
|---|---|---|
| Platform Support | Not specified | Limited / Varies |
| Context Awareness | High | Baseline |
| Installation Complexity | Unknown | N/A |
Frequently Asked Questions
What does this skill do?
TanStack Router patterns for type-safe, file-based routing. Covers installation, route configuration, typed params/search, layouts, and navigation. Use when setting up routes, implementing navigation, or configuring route loaders.
Where can I find the source code?
You can find the source code on GitHub using the link provided at the top of the page.
SKILL.md Source
# TanStack Router Patterns
Type-safe, file-based routing for React applications with TanStack Router.
## Installation
```bash
pnpm add @tanstack/react-router
pnpm add -D @tanstack/router-plugin
```
```typescript
// vite.config.ts
import { TanStackRouterVite } from '@tanstack/router-plugin/vite'
import { defineConfig } from 'vite'
import react from '@vitejs/plugin-react'
export default defineConfig({
plugins: [
react(),
TanStackRouterVite(), // Generates route tree
],
})
```
## Automatic Code Splitting (Recommended)
**TanStack Router v1.x+ (2025)** introduces automatic code splitting that separates critical route configuration from non-critical components.
```typescript
// vite.config.ts
import { TanStackRouterVite } from '@tanstack/router-plugin/vite'
import { defineConfig } from 'vite'
import react from '@vitejs/plugin-react'
export default defineConfig({
plugins: [
react(),
TanStackRouterVite({
autoCodeSplitting: true, // NEW: Enable automatic splitting
}),
],
})
```
**What Gets Split:**
| Critical (Always Loaded) | Non-Critical (Lazy Loaded) |
|--------------------------|---------------------------|
| Route configuration | Component |
| Loaders | Error component |
| Search params validation | Pending component |
| beforeLoad | Not-found component |
**Benefits:**
- Smaller initial bundle (route config without components)
- Automatic optimization (no manual `.lazy.tsx` files needed)
- Better perceived performance (loaders start immediately)
**When to Use:**
- **Recommended for all new projects**
- Existing projects: Enable and test bundle sizes
- Large apps benefit most (many routes)
## Bootstrap
```typescript
// src/main.tsx
import { StrictMode } from 'react'
import ReactDOM from 'react-dom/client'
import { RouterProvider, createRouter } from '@tanstack/react-router'
import { routeTree } from './routeTree.gen'
const router = createRouter({ routeTree })
// Register router for type safety
declare module '@tanstack/react-router' {
interface Register {
router: typeof router
}
}
ReactDOM.createRoot(document.getElementById('root')!).render(
<StrictMode>
<RouterProvider router={router} />
</StrictMode>
)
```
## File-Based Routes
```
src/routes/
├── __root.tsx # Root layout (Outlet, providers)
├── index.tsx # "/" route
├── about.tsx # "/about" route
├── users/
│ ├── index.tsx # "/users" route
│ └── $userId.tsx # "/users/:userId" route (dynamic)
└── posts/
├── $postId/
│ ├── index.tsx # "/posts/:postId" route
│ └── edit.tsx # "/posts/:postId/edit" route
└── index.tsx # "/posts" route
```
**Naming Conventions:**
- `__root.tsx` - Root layout (contains `<Outlet />`)
- `index.tsx` - Index route for that path
- `$param.tsx` - Dynamic parameter (e.g., `$userId` → `:userId`)
- `_layout.tsx` - Layout route (no URL segment)
- `route.lazy.tsx` - Lazy-loaded route
## Virtual File Routes
Virtual file routes allow the router to auto-generate route anchors without physical files:
```
src/routes/
├── __root.tsx # Root layout
├── index.tsx # "/" (physical file)
├── about.lazy.tsx # "/about" (virtual route, lazy only)
└── users/
├── index.tsx # "/users" (physical)
└── $userId.lazy.tsx # "/users/:userId" (virtual, lazy only)
```
**Key Insight:** If you only need a component (no loader, no search validation), you can delete the base route file. The router auto-generates a virtual anchor for `.lazy.tsx` files.
**Example - Minimal About Page:**
```typescript
// src/routes/about.lazy.tsx
// No about.tsx needed! Router creates virtual anchor
import { createLazyFileRoute } from '@tanstack/react-router'
export const Route = createLazyFileRoute('/about')({
component: () => <div>About Us</div>,
})
```
**When Virtual Routes Make Sense:**
- Static pages with no data fetching
- Simple UI components
- When you want maximum code splitting
## Root Layout
```typescript
// src/routes/__root.tsx
import { createRootRoute, Outlet } from '@tanstack/react-router'
import { TanStackRouterDevtools } from '@tanstack/router-devtools'
export const Route = createRootRoute({
component: () => (
<>
<nav>
<Link to="/">Home</Link>
<Link to="/about">About</Link>
<Link to="/users">Users</Link>
</nav>
<main>
<Outlet /> {/* Child routes render here */}
</main>
<TanStackRouterDevtools /> {/* Auto-hides in production */}
</>
),
})
```
## Basic Route
```typescript
// src/routes/about.tsx
import { createFileRoute } from '@tanstack/react-router'
export const Route = createFileRoute('/about')({
component: AboutComponent,
})
function AboutComponent() {
return <div>About Page</div>
}
```
## Dynamic Routes with Params
```typescript
// src/routes/users/$userId.tsx
import { createFileRoute } from '@tanstack/react-router'
export const Route = createFileRoute('/users/$userId')({
component: UserComponent,
})
function UserComponent() {
const { userId } = Route.useParams() // Fully typed!
return <div>User ID: {userId}</div>
}
```
## Typed Search Params
```typescript
// src/routes/users/index.tsx
import { createFileRoute } from '@tanstack/react-router'
import { z } from 'zod'
const userSearchSchema = z.object({
page: z.number().default(1),
filter: z.enum(['active', 'inactive', 'all']).default('all'),
search: z.string().optional(),
})
export const Route = createFileRoute('/users/')({
validateSearch: userSearchSchema,
component: UsersComponent,
})
function UsersComponent() {
const { page, filter, search } = Route.useSearch() // Fully typed!
return (
<div>
<p>Page: {page}</p>
<p>Filter: {filter}</p>
{search && <p>Search: {search}</p>}
</div>
)
}
```
## Navigation with Link
```typescript
import { Link } from '@tanstack/react-router'
// Basic navigation
<Link to="/about">About</Link>
// With params
<Link to="/users/$userId" params={{ userId: '123' }}>
View User
</Link>
// With search params
<Link
to="/users"
search={{ page: 2, filter: 'active' }}
>
Users Page 2
</Link>
// With state
<Link to="/details" state={{ from: 'home' }}>
Details
</Link>
// Active link styling
<Link
to="/about"
activeProps={{ className: 'text-blue-600 font-bold' }}
inactiveProps={{ className: 'text-gray-600' }}
>
About
</Link>
```
## Programmatic Navigation
```typescript
import { useNavigate } from '@tanstack/react-router'
function MyComponent() {
const navigate = useNavigate()
const handleClick = () => {
// Navigate to route
navigate({ to: '/users' })
// With params
navigate({ to: '/users/$userId', params: { userId: '123' } })
// With search
navigate({ to: '/users', search: { page: 2 } })
// Replace history
navigate({ to: '/login', replace: true })
// Go back
navigate({ to: '..' }) // Relative navigation
}
return <button onClick={handleClick}>Navigate</button>
}
```
## Route Loaders (Data Fetching)
**Basic Loader:**
```typescript
// src/routes/users/$userId.tsx
import { createFileRoute } from '@tanstack/react-router'
export const Route = createFileRoute('/users/$userId')({
loader: async ({ params }) => {
const user = await fetchUser(params.userId)
return { user }
},
component: UserComponent,
})
function UserComponent() {
const { user } = Route.useLoaderData() // Fully typed!
return <div>{user.name}</div>
}
```
**With TanStack Query Integration** (see **tanstack-query** skill for details):
```typescript
import { queryClient } from '@/app/queryClient'
import { userQueryOptions } from '@/features/users/queries'
export const Route = createFileRoute('/users/$userId')({
loader: ({ params }) =>
queryClient.ensureQueryData(userQueryOptions(params.userId)),
component: UserComponent,
})
```
## Manual Code Splitting (`.lazy.tsx` Pattern)
For fine-grained control over code splitting, split routes into critical and lazy files:
**Critical Route File (posts.tsx):**
```typescript
// src/routes/posts.tsx - Loaded immediately
import { createFileRoute } from '@tanstack/react-router'
import { postsQueryOptions } from '@/features/posts/queries'
import { queryClient } from '@/app/queryClient'
export const Route = createFileRoute('/posts')({
// Critical: loaders, search validation, beforeLoad
loader: () => queryClient.ensureQueryData(postsQueryOptions()),
validateSearch: (search) => postsSearchSchema.parse(search),
})
```
**Lazy Route File (posts.lazy.tsx):**
```typescript
// src/routes/posts.lazy.tsx - Code-split, loaded on navigation
import { createLazyFileRoute } from '@tanstack/react-router'
import { Posts } from '@/features/posts/components/Posts'
export const Route = createLazyFileRoute('/posts')({
// Non-critical: component, error/pending/notFound
component: Posts,
pendingComponent: () => <PostsSkeleton />,
errorComponent: ({ error }) => <PostsError error={error} />,
})
```
**Lazy-Only Properties:**
- `component` - The main route component
- `errorComponent` - Error boundary UI
- `pendingComponent` - Loading/suspense UI
- `notFoundComponent` - 404 UI for this route
**Critical-Only Properties (NOT in lazy):**
- `loader` / `loaderDeps`
- `beforeLoad`
- `validateSearch`
- `search` (search middleware)
- `context`
**Best Practice:** Use automatic code splitting (`autoCodeSplitting: true`) unless you need specific control over what goes in each file.
## Type-Safe Route Data Access (`getRouteApi`)
When building components outside the route file, use `getRouteApi` for type-safe access:
```typescript
// src/features/posts/components/PostHeader.tsx
import { getRouteApi } from '@tanstack/react-router'
// Get typed route API without importing the route
const routeApi = getRouteApi('/posts/$postId')
export function PostHeader() {
// All these are fully typed!
const { postId } = routeApi.useParams()
const { post } = routeApi.useLoaderData()
const { view } = routeApi.useSearch()
const context = routeApi.useRouteContext()
return (
<header>
<h1>{post.title}</h1>
<span>Viewing: {view}</span>
</header>
)
}
```
**Available Methods:**
- `useParams()` - Route parameters
- `useSearch()` - Search/query params
- `useLoaderData()` - Data from loader
- `useRouteContext()` - Route context
- `useMatch()` - Full route match object
**When to Use:**
- Components in separate files from route definition
- Shared components used across routes
- Avoiding circular imports
- Cleaner separation of concerns
**Important:** The route path string must exactly match the route definition.
## Layouts
**Layout Route** (`_layout.tsx` - no URL segment):
```typescript
// src/routes/_layout.tsx
import { createFileRoute, Outlet } from '@tanstack/react-router'
export const Route = createFileRoute('/_layout')({
component: LayoutComponent,
})
function LayoutComponent() {
return (
<div className="dashboard-layout">
<Sidebar />
<div className="content">
<Outlet /> {/* Child routes */}
</div>
</div>
)
}
// Child routes
// src/routes/_layout/dashboard.tsx → "/dashboard"
// src/routes/_layout/settings.tsx → "/settings"
```
## Route Groups
Route groups organize files without affecting URLs using parentheses:
```
src/routes/
├── (auth)/ # Group (not in URL)
│ ├── login.tsx # "/login"
│ ├── register.tsx # "/register"
│ └── forgot-password.tsx # "/forgot-password"
├── (dashboard)/ # Group (not in URL)
│ ├── _layout.tsx # Shared dashboard layout
│ ├── index.tsx # "/" (or "/dashboard")
│ ├── analytics.tsx # "/analytics"
│ └── settings.tsx # "/settings"
└── __root.tsx
```
**Benefits:**
- Organize related routes without nesting URLs
- Apply shared layouts to grouped routes
- Better file organization in large apps
- No URL pollution
**Example - Auth Group Layout:**
```typescript
// src/routes/(auth)/_layout.tsx
export const Route = createFileRoute('/(auth)/_layout')({
component: () => (
<div className="auth-layout">
<Logo />
<Outlet />
</div>
),
})
```
## Loading States
```typescript
export const Route = createFileRoute('/users')({
loader: async () => {
const users = await fetchUsers()
return { users }
},
pendingComponent: () => <Spinner />,
errorComponent: ({ error }) => <ErrorMessage>{error.message}</ErrorMessage>,
component: UsersComponent,
})
```
## Error Handling
```typescript
import { ErrorComponent } from '@tanstack/react-router'
export const Route = createFileRoute('/users')({
loader: async () => {
const users = await fetchUsers()
if (!users) throw new Error('Failed to load users')
return { users }
},
errorComponent: ({ error, reset }) => (
<div>
<h1>Error loading users</h1>
<p>{error.message}</p>
<button onClick={reset}>Try Again</button>
</div>
),
component: UsersComponent,
})
```
## Route Context
**Providing Context:**
```typescript
// src/routes/__root.tsx
export const Route = createRootRoute({
beforeLoad: () => ({
user: getCurrentUser(),
}),
component: RootComponent,
})
// Access in child routes
export const Route = createFileRoute('/dashboard')({
component: function Dashboard() {
const { user } = Route.useRouteContext()
return <div>Welcome, {user.name}</div>
},
})
```
## Route Guards / Auth
```typescript
// src/routes/_authenticated.tsx
import { createFileRoute, redirect } from '@tanstack/react-router'
export const Route = createFileRoute('/_authenticated')({
beforeLoad: ({ context }) => {
if (!context.user) {
throw redirect({ to: '/login' })
}
},
component: Outlet,
})
// Protected routes
// src/routes/_authenticated/dashboard.tsx
// src/routes/_authenticated/profile.tsx
```
## Preloading
**Hover Preload:**
```typescript
<Link
to="/users/$userId"
params={{ userId: '123' }}
preload="intent" // Preload on hover
>
View User
</Link>
```
**Options:**
- `preload="intent"` - Preload on hover/focus
- `preload="render"` - Preload when link renders
- `preload={false}` - No preload (default)
## DevTools
```typescript
import { TanStackRouterDevtools } from '@tanstack/router-devtools'
// Add to root layout
<TanStackRouterDevtools position="bottom-right" />
```
Auto-hides in production builds.
## Best Practices
1. **Use Type-Safe Navigation** - Let TypeScript catch routing errors at compile time
2. **Validate Search Params** - Use Zod schemas for search params
3. **Prefetch Data in Loaders** - Integrate with TanStack Query for optimal data fetching
4. **Use Layouts for Shared UI** - Avoid duplicating layout code across routes
5. **Lazy Load Routes** - Use `route.lazy.tsx` for code splitting
6. **Leverage Route Context** - Share data down the route tree efficiently
## Common Patterns
**Catch-All (Splat) Routes:**
**v1.x Syntax:**
```typescript
// src/routes/files/$.tsx - Catches all paths under /files/
export const Route = createFileRoute('/files/$')({
component: FileViewer,
})
function FileViewer() {
// Access splat via '_splat' key (v1.x+)
const { _splat } = Route.useParams()
// '/files/docs/readme.md' → _splat = 'docs/readme.md'
return <div>File: {_splat}</div>
}
```
**v2 Migration Note:**
In TanStack Router v2 (upcoming), splat routes use `_splat` key consistently:
- v1: `params['*']` or `params._splat` (both work)
- v2: Only `params._splat` (star deprecated)
**Prepare for v2:**
```typescript
// ✅ Future-proof
const { _splat } = Route.useParams()
// ⚠️ Works in v1, deprecated in v2
const splat = Route.useParams()['*']
```
**404 Not Found Route:**
```typescript
// src/routes/$.tsx
export const Route = createFileRoute('/$')({
component: () => <div>404 Not Found</div>,
})
```
**Optional Params:**
```typescript
// Use search params for optional data
const searchSchema = z.object({
optional: z.string().optional(),
})
```
**Multi-Level Dynamic Routes:**
```
/posts/$postId/comments/$commentId
```
## Production Best Practices (2026)
Insights from large-scale TanStack Router deployments:
### 1. File Structure = URL Structure
Colocate everything a page needs within its route folder:
```
src/routes/users/
├── $userId/
│ ├── index.tsx # Route definition
│ ├── index.lazy.tsx # Lazy component
│ ├── UserProfile.tsx # Page-specific component
│ └── useUserActions.ts # Page-specific hooks
└── index.tsx
```
Components/functions belong at the **nearest shared ancestor** in the hierarchy.
### 2. Let Router Handle Loading States
```typescript
// ✅ Recommended - Router handles loading/error
export const Route = createFileRoute('/users/$userId')({
loader: fetchUser,
pendingComponent: UserSkeleton,
errorComponent: UserError,
component: UserProfile, // Only handles happy path!
})
// ❌ Avoid - Manual loading in component
function UserProfile() {
const { data, isLoading, error } = useUser()
if (isLoading) return <Spinner /> // Router should handle this
if (error) return <Error /> // Router should handle this
return <div>{data.name}</div>
}
```
### 3. Preload Strategy
```typescript
// List views → preload detail on hover
<Link
to="/users/$userId"
params={{ userId }}
preload="intent" // Preload on hover
>
{user.name}
</Link>
// Critical navigation → preload on render
<Link to="/dashboard" preload="render">
Dashboard
</Link>
```
### 4. Search Params for Everything Shareable
If users should be able to share or bookmark a specific view, use search params:
```typescript
const searchSchema = z.object({
tab: z.enum(['overview', 'activity', 'settings']).default('overview'),
page: z.number().default(1),
sort: z.enum(['name', 'date', 'score']).optional(),
})
```
## TanStack Start (Full-Stack Framework)
**TanStack Start** is the full-stack meta-framework built on TanStack Router:
**Stack:**
- TanStack Router (routing)
- Vite (bundler)
- Nitro (server)
- Vinxi (dev server)
**When to Consider Start:**
- New full-stack projects
- Need SSR/SSG out of the box
- Want alternatives to Next.js/Remix
- Prefer TanStack's type-safety approach
**When to Stick with Router + Vite:**
- SPAs without server requirements
- Existing Vite projects
- When you need maximum control
**Resources:**
- [TanStack Start Docs](https://tanstack.com/start)
- [Start vs Router](https://tanstack.com/start/latest/docs/framework/react/comparison)
**Note:** Start is still maturing. For production SPAs in 2026, TanStack Router + Query + Vite remains the recommended stack.
## Related Skills
- **tanstack-query** - Server state management, caching, and route loader integration
- **react-typescript** - React 19 patterns, component composition, and Actions
- **shadcn-ui** - UI components with proper route integration
- **browser-debugging** - DevTools and debugging TanStack Router
- **testing-frontend** - Testing routes and navigationRelated Skills
openrouter-trending-models
Fetch trending programming models from OpenRouter rankings. Use when selecting models for multi-model review, updating model recommendations, or researching current AI coding trends. Provides model IDs, context windows, pricing, and usage statistics from the most recent week.
task-complexity-router
Complexity-based task routing for optimal model selection and cost efficiency. Use when deciding which model tier to use, analyzing task complexity, optimizing API costs, or implementing tiered routing. Trigger keywords - "routing", "complexity", "model selection", "tier", "cost optimization", "haiku", "sonnet", "opus", "task analysis".
router-query-integration
Use when setting up route loaders or optimizing navigation performance. Integrates TanStack Router with TanStack Query for optimal data fetching. Covers route loaders with query prefetching, ensuring instant navigation, and eliminating request waterfalls.
tanstack-query
Comprehensive TanStack Query v5 patterns for async state management. Covers breaking changes, query key factories, data transformation, mutations, optimistic updates, authentication, testing with MSW, and anti-patterns. Use for all server state management, data fetching, and cache invalidation tasks.
test-skill
A test skill for validation testing. Use when testing skill parsing and validation logic.
bad-skill
This skill has invalid YAML in frontmatter
release
Plugin release process for MAG Claude Plugins marketplace. Covers version bumping, marketplace.json updates, git tagging, and common mistakes. Use when releasing new plugin versions or troubleshooting update issues.
Claudish Integration Skill
**Version:** 1.0.0
transcription
Audio/video transcription using OpenAI Whisper. Covers installation, model selection, transcript formats (SRT, VTT, JSON), timing synchronization, and speaker diarization. Use when transcribing media or generating subtitles.
final-cut-pro
Apple Final Cut Pro FCPXML format reference. Covers project structure, timeline creation, clip references, effects, and transitions. Use when generating FCP projects or understanding FCPXML structure.
ffmpeg-core
FFmpeg fundamentals for video/audio manipulation. Covers common operations (trim, concat, convert, extract), codec selection, filter chains, and performance optimization. Use when planning or executing video processing tasks.
statusline-customization
Configuration reference and troubleshooting for the statusline plugin — sections, themes, bar widths, and script architecture