From tanstack
Implements type-safe React routing with TanStack Router for file-based routes, navigation, search params validation, data loaders, code splitting, and error handling.
npx claudepluginhub tenequm/claude-pluginsThis skill uses the workspace's default tool permissions.
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.
Guides Next.js Cache Components and Partial Prerendering (PPR) with cacheComponents enabled. Implements 'use cache', cacheLife(), cacheTag(), revalidateTag(), static/dynamic optimization, and cache debugging.
Guides building MCP servers enabling LLMs to interact with external services via tools. Covers best practices, TypeScript/Node (MCP SDK), Python (FastMCP).
Generates original PNG/PDF visual art via design philosophy manifestos for posters, graphics, and static designs on user request.
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.
npm install @tanstack/react-router @tanstack/router-plugin
// 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(),
],
})
// 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 />
</>
),
})
// src/routes/index.tsx
import { createFileRoute } from '@tanstack/react-router'
export const Route = createFileRoute('/')({
component: () => <div>Welcome Home</div>,
})
// 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
}
}
// src/main.tsx
import { RouterProvider } from '@tanstack/react-router'
import { router } from './router'
function App() {
return <RouterProvider router={router} />
}
Files in src/routes/ are automatically converted to route configuration by the Vite plugin or CLI.
| 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.
All navigation APIs share to, from, params, search, and hash options.
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>
For imperative navigation from side effects:
const navigate = useNavigate({ from: '/posts' })
const handleSubmit = async (data: PostInput) => {
const post = await createPost(data)
navigate({ to: '/posts/$postId', params: { postId: post.id } })
}
Reusable type-safe link configuration:
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 are first-class - validated, typed, JSON-serialized, and subscribable with fine-grained selectors.
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 (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 }) })
import { retainSearchParams, stripSearchParams } from '@tanstack/react-router'
export const Route = createFileRoute('/shop/products')({
validateSearch: zodValidator(productSearchSchema),
search: {
middlewares: [
retainSearchParams(['globalFilter']),
stripSearchParams({ sort: 'newest' }),
],
},
})
Route loaders run in parallel before rendering with built-in SWR caching.
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>
},
})
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.
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 staleTimeRuns serially before loaders. Use for auth redirects or injecting route-specific context:
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),
})
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)
})
Hierarchical dependency injection via createRootRouteWithContext. Context merges down the tree and is fully type-safe.
// 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:
function App() {
const auth = useAuth()
return <RouterProvider router={router} context={{ auth }} />
}
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>
)
},
})
Two modes via notFoundMode on the router (default: 'fuzzy'):
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.
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.
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 }).
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.
See reference files for deep dives:
references/search-params.md - Custom serialization, Standard Schema validation, arrays/objects, sharing across routesreferences/data-loading.md - Deferred data loading with Await, external data loading, shouldReload, streaming SSRreferences/routing-patterns.md - Virtual file routes, route masking, navigation blocking, authenticated routes, parallel routesreferences/code-splitting.md - Automatic splitting options, loader splitting, directory encapsulation, code-based splittingreferences/ssr.md - SSR setup, streaming, dehydration/hydration, data serialization, TanStack Start integrationnpm install @tanstack/react-router-devtools
// 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.
validateSearch with Zod (via zodValidator) or any Standard Schema library. Use fallback(...).default(...) to retain types.from on navigation hooks and components - Narrows types, improves TS performance, catches route mismatches at runtime.createRootRouteWithContext instead of importing singletons.