home / skills / tenequm / claude-plugins / tanstack-router
This skill helps you implement type-safe React routing with TanStack Router, enabling file-based routing, loaders, and safe navigation.
npx playbooks add skill tenequm/claude-plugins --skill tanstack-routerReview the files below or copy the command above to add this skill to your agents.
---
name: tanstack-router
description: Build type-safe React applications with TanStack Router. Use when implementing file-based or code-based routing, type-safe navigation, search params validation, data loading, code splitting, or route-level error handling. Triggers on "tanstack router", "file-based routing", "type-safe routing", "search params", "route loader", or file patterns like routes/*.tsx, __root.tsx, routeTree.gen.ts.
---
# TanStack Router v1
A fully type-safe router for React with first-class search param APIs, built-in data loading with SWR caching, file-based route generation, and 100% inferred TypeScript support.
## When to Use This Skill
- Setting up file-based or code-based routing in a React application
- Building type-safe navigation with Link, useNavigate, or router.navigate
- Validating and managing URL search params as typed state
- Loading data in route loaders with SWR caching
- Code splitting routes for optimal bundle size
- Handling not-found errors and error boundaries per route
- Implementing route context for dependency injection
- Configuring preloading strategies (intent, viewport, render)
- Integrating TanStack Query with route loaders
- Adding navigation blocking for unsaved changes
- Building SSR applications (for full SSR, see the tanstack-start skill)
## Quick Start Workflow
### 1. Install and Configure (Vite)
```bash
npm install @tanstack/react-router @tanstack/router-plugin
```
```ts
// vite.config.ts
import { defineConfig } from 'vite'
import react from '@vitejs/plugin-react'
import { tanstackRouter } from '@tanstack/router-plugin/vite'
export default defineConfig({
plugins: [
tanstackRouter({ autoCodeSplitting: true }),
react(),
],
})
```
### 2. Create Routes
```tsx
// src/routes/__root.tsx
import { createRootRoute, Link, Outlet } from '@tanstack/react-router'
export const Route = createRootRoute({
component: () => (
<>
<nav>
<Link to="/">Home</Link>
<Link to="/about">About</Link>
</nav>
<Outlet />
</>
),
})
```
```tsx
// src/routes/index.tsx
import { createFileRoute } from '@tanstack/react-router'
export const Route = createFileRoute('/')({
component: () => <div>Welcome Home</div>,
})
```
### 3. Create and Register the Router
```tsx
// src/router.ts
import { createRouter } from '@tanstack/react-router'
import { routeTree } from './routeTree.gen'
export const router = createRouter({ routeTree })
declare module '@tanstack/react-router' {
interface Register {
router: typeof router
}
}
```
```tsx
// src/main.tsx
import { RouterProvider } from '@tanstack/react-router'
import { router } from './router'
function App() {
return <RouterProvider router={router} />
}
```
## File-Based Routing
Files in `src/routes/` are automatically converted to route configuration by the Vite plugin or CLI.
### Naming Conventions
| Convention | Purpose | Example |
|---|---|---|
| `__root.tsx` | Root route (always rendered) | `src/routes/__root.tsx` |
| `index.tsx` | Index route for parent path | `src/routes/index.tsx` matches `/` |
| `.` separator | Nested route (flat files) | `posts.tsx` = `/posts` |
| `$param` | Dynamic path parameter | `posts.$postId.tsx` = `/posts/:postId` |
| `_` prefix | Pathless layout route | `_layout.tsx` wraps children, no URL segment |
| `_` suffix | Non-nested route | `posts_.edit.tsx` breaks out of `posts` nesting |
| `-` prefix | Excluded from routing | `-components/Button.tsx` for colocated files |
| `(folder)` | Route group (no URL segment) | `(auth)/login.tsx` = `/login` |
| `.lazy.tsx` | Lazy-loaded component | `posts.lazy.tsx` for code-split components |
| `.route.tsx` | Directory route file | `posts/route.tsx` instead of `posts.tsx` |
Flat (`posts.$postId.tsx`) and directory (`posts/$postId.tsx`) structures can be mixed freely.
## Type-Safe Navigation
All navigation APIs share `to`, `from`, `params`, `search`, and `hash` options.
### Link Component
```tsx
import { Link } from '@tanstack/react-router'
<Link to="/posts/$postId" params={{ postId: '123' }}>View Post</Link>
// Relative navigation
<Link from="/posts/$postId" to="..">Back to Posts</Link>
// Active styling
<Link to="/posts" activeProps={{ className: 'font-bold' }} activeOptions={{ exact: true }}>
Posts
</Link>
```
### useNavigate Hook
For imperative navigation from side effects:
```tsx
const navigate = useNavigate({ from: '/posts' })
const handleSubmit = async (data: PostInput) => {
const post = await createPost(data)
navigate({ to: '/posts/$postId', params: { postId: post.id } })
}
```
### linkOptions Helper
Reusable type-safe link configuration:
```tsx
import { linkOptions } from '@tanstack/react-router'
const postLink = linkOptions({ to: '/posts/$postId', params: { postId: '123' } })
<Link {...postLink}>View Post</Link>
```
Always provide `from` on Link and hooks to narrow types and improve TS performance. Without `from`, TypeScript must check against all routes.
## Search Params
Search params are first-class - validated, typed, JSON-serialized, and subscribable with fine-grained selectors.
### Validation with Zod
```tsx
import { zodValidator, fallback } from '@tanstack/zod-adapter'
import { z } from 'zod'
const productSearchSchema = z.object({
page: fallback(z.number(), 1).default(1),
filter: fallback(z.string(), '').default(''),
sort: fallback(z.enum(['newest', 'oldest', 'price']), 'newest').default('newest'),
})
export const Route = createFileRoute('/shop/products')({
validateSearch: zodValidator(productSearchSchema),
})
```
Use `fallback(...).default(...)` from the Zod adapter to retain types. Plain `.catch()` causes type loss. Valibot and ArkType work without adapters via Standard Schema support.
### Reading and Writing
```tsx
// Reading (type-safe)
const { page, sort } = Route.useSearch()
// From code-split component (avoids circular imports)
const search = getRouteApi('/shop/products').useSearch()
// Loose typing for shared components
const search = useSearch({ strict: false })
// Writing via Link
<Link from={Route.fullPath} search={(prev) => ({ ...prev, page: prev.page + 1 })}>Next</Link>
// Writing via useNavigate
const navigate = useNavigate({ from: Route.fullPath })
navigate({ search: (prev) => ({ ...prev, page: 2 }) })
```
### Search Middlewares
```tsx
import { retainSearchParams, stripSearchParams } from '@tanstack/react-router'
export const Route = createFileRoute('/shop/products')({
validateSearch: zodValidator(productSearchSchema),
search: {
middlewares: [
retainSearchParams(['globalFilter']),
stripSearchParams({ sort: 'newest' }),
],
},
})
```
## Data Loading
Route loaders run in parallel before rendering with built-in SWR caching.
### Basic Loader
```tsx
export const Route = createFileRoute('/posts')({
loader: () => fetchPosts(),
component: () => {
const posts = Route.useLoaderData()
return <ul>{posts.map(p => <li key={p.id}>{p.title}</li>)}</ul>
},
})
```
### loaderDeps - Search Params as Cache Keys
```tsx
export const Route = createFileRoute('/posts')({
validateSearch: z.object({ page: z.number().catch(1), limit: z.number().catch(10) }),
loaderDeps: ({ search: { page, limit } }) => ({ page, limit }),
loader: ({ deps: { page, limit } }) => fetchPosts({ page, limit }),
})
```
Only include deps you actually use - returning the entire `search` object causes unnecessary cache invalidation.
### Caching and Staleness
- `staleTime` - How long data is fresh (default: 0 for navigation, 30s for preload)
- `gcTime` - How long unused data stays in cache (default: 30 minutes)
- `shouldReload` - Custom reload logic beyond staleTime
### beforeLoad - Guards and Context
Runs serially before loaders. Use for auth redirects or injecting route-specific context:
```tsx
export const Route = createFileRoute('/dashboard')({
beforeLoad: ({ context }) => {
if (!context.auth.isAuthenticated) {
throw redirect({ to: '/login', search: { redirect: '/dashboard' } })
}
return { user: context.auth.user }
},
loader: ({ context: { user } }) => fetchDashboard(user.id),
})
```
### Pending Components
```tsx
export const Route = createFileRoute('/posts')({
loader: () => fetchPosts(),
pendingComponent: () => <Spinner />,
pendingMs: 1000, // Wait before showing (default: 1000)
pendingMinMs: 500, // Minimum display time to avoid flash (default: 500)
})
```
## Route Context
Hierarchical dependency injection via `createRootRouteWithContext`. Context merges down the tree and is fully type-safe.
```tsx
// src/routes/__root.tsx
import { createRootRouteWithContext } from '@tanstack/react-router'
interface RouterContext { queryClient: QueryClient }
export const Route = createRootRouteWithContext<RouterContext>()({
component: RootComponent,
})
// src/router.ts - context is required by the type
const router = createRouter({ routeTree, context: { queryClient } })
// Child routes access context in loaders
export const Route = createFileRoute('/posts')({
loader: ({ context: { queryClient } }) => queryClient.ensureQueryData(postsQueryOptions()),
})
```
Pass React hooks/state at runtime via `RouterProvider`:
```tsx
function App() {
const auth = useAuth()
return <RouterProvider router={router} context={{ auth }} />
}
```
## Error Handling
### errorComponent
```tsx
export const Route = createFileRoute('/posts/$postId')({
loader: ({ params }) => fetchPost(params.postId),
errorComponent: ({ error }) => {
const router = useRouter()
return (
<div>
<p>{error.message}</p>
<button onClick={() => router.invalidate()}>Retry</button>
</div>
)
},
})
```
### notFoundComponent
Two modes via `notFoundMode` on the router (default: `'fuzzy'`):
- **fuzzy** - nearest parent route with children and a notFoundComponent handles it
- **root** - root route always handles it
```tsx
import { notFound } from '@tanstack/react-router'
export const Route = createFileRoute('/posts/$postId')({
loader: async ({ params }) => {
const post = await getPost(params.postId)
if (!post) throw notFound()
return { post }
},
notFoundComponent: () => <p>Post not found</p>,
})
```
Set `defaultNotFoundComponent` on the router for app-wide fallback.
## Code Splitting
**Automatic (recommended):** Enable `autoCodeSplitting: true` in the Vite plugin. Non-critical config (component, errorComponent, pendingComponent, notFoundComponent) is split into separate chunks automatically.
**Manual with .lazy.tsx:** Split into two files - critical config in `posts.tsx` (loader, validateSearch), non-critical in `posts.lazy.tsx` (component via `createLazyFileRoute`).
The root route (`__root.tsx`) does not support code splitting since it always renders.
## Preloading
```tsx
const router = createRouter({
routeTree,
defaultPreload: 'intent', // Preload on hover/touch
defaultPreloadDelay: 50, // ms delay (default: 50)
})
```
Strategies: `'intent'` (hover/touch), `'viewport'` (Intersection Observer), `'render'` (on mount). Override per-link with `preload` prop. Manual: `router.preloadRoute({ to, params })`.
## TanStack Query Integration
```tsx
const postQueryOptions = (postId: string) =>
queryOptions({ queryKey: ['post', postId], queryFn: () => fetchPost(postId) })
export const Route = createFileRoute('/posts/$postId')({
loader: async ({ context: { queryClient }, params: { postId } }) => {
await queryClient.ensureQueryData(postQueryOptions(postId))
},
component: () => {
const { postId } = Route.useParams()
const { data } = useSuspenseQuery(postQueryOptions(postId))
return <div>{data.title}</div>
},
})
```
Set `defaultPreloadStaleTime: 0` on the router when using external caching so loaders always fire.
## Advanced Topics
See reference files for deep dives:
- `references/search-params.md` - Custom serialization, Standard Schema validation, arrays/objects, sharing across routes
- `references/data-loading.md` - Deferred data loading with Await, external data loading, shouldReload, streaming SSR
- `references/routing-patterns.md` - Virtual file routes, route masking, navigation blocking, authenticated routes, parallel routes
- `references/code-splitting.md` - Automatic splitting options, loader splitting, directory encapsulation, code-based splitting
- `references/ssr.md` - SSR setup, streaming, dehydration/hydration, data serialization, TanStack Start integration
## DevTools
```bash
npm install @tanstack/react-router-devtools
```
```tsx
// src/routes/__root.tsx
import { TanStackRouterDevtools } from '@tanstack/react-router-devtools'
export const Route = createRootRoute({
component: () => (
<>
<Outlet />
<TanStackRouterDevtools />
</>
),
})
```
Automatically excluded from production. Use `TanStackRouterDevtoolsInProd` if needed in prod.
## Best Practices
1. **Use file-based routing with autoCodeSplitting** - Generates the route tree and optimizes bundles. Fall back to code-based only when you need programmatic control.
2. **Always validate search params** - Use `validateSearch` with Zod (via `zodValidator`) or any Standard Schema library. Use `fallback(...).default(...)` to retain types.
3. **Provide `from` on navigation hooks and components** - Narrows types, improves TS performance, catches route mismatches at runtime.
4. **Extract only needed deps in loaderDeps** - Return only params your loader uses, not the full search object.
5. **Use route context for dependency injection** - Pass QueryClient, auth, or services via `createRootRouteWithContext` instead of importing singletons.
6. **Set preload to 'intent' globally** - Dramatically improves perceived performance with minimal effort.
7. **Use router.invalidate() in error components** - Reloads data and resets the error boundary together.
## Resources
- **Official Docs**: https://tanstack.com/router/latest/docs/framework/react/overview
- **GitHub**: https://github.com/TanStack/router
- **Examples**: https://tanstack.com/router/latest/docs/framework/react/examples
- **Query Integration**: https://tanstack.com/router/latest/docs/router/integrations/query
- **Discord**: https://discord.gg/tanstack
- **Migrate from React Router**: https://tanstack.com/router/latest/docs/framework/react/installation/migrate-from-react-router
This skill helps you build type-safe React applications using TanStack Router. It streamlines file-based or code-based routing, typed navigation, validated search params, route loaders with caching, and route-level error handling. Use it to add code splitting, preloading strategies, and hierarchical route context to React apps.
The skill inspects route files (e.g., src/routes/*.tsx, __root.tsx, routeTree.gen.ts) and provides guidance for creating a route tree, registering a router, and wiring RouterProvider. It explains file-based conventions, typed Link/useNavigate APIs, search param validation (Zod/Standard Schema), SWR-backed loaders, and code-splitting/preload strategies. It also covers route context, beforeLoad guards, pending/error/not-found components and integration with TanStack Query.
How do search param validators keep TypeScript types?
Use the zodAdapter with fallback(...).default(...) so defaults are preserved in the inferred types; standard schema support also works for Valibot or ArkType.
When should I split route code automatically vs manually?
Enable autoCodeSplitting in the Vite plugin for most cases. Use manual .lazy.tsx files only when you need finer control over which parts are loaded together.