Overview
TanStack Router is a fully type-safe router for React (and Solid) applications. It provides file-based routing, first-class search parameter management, built-in data loading, code splitting, and deep TypeScript integration. It serves as the routing foundation for TanStack Start (the full-stack framework).
Package: @tanstack/react-router
CLI: @tanstack/router-cli or @tanstack/router-plugin (Vite/Rspack/Webpack)
Devtools: @tanstack/react-router-devtools
Installation
npm install @tanstack/react-router
# For file-based routing with Vite:
npm install -D @tanstack/router-plugin
# Or standalone CLI:
npm install -D @tanstack/router-cli
Core Concepts
Route Trees
Routes are organized in a tree structure. The root route is the top-level layout, and child routes nest underneath.
import { createRootRoute, createRoute, createRouter } from '@tanstack/react-router'
const rootRoute = createRootRoute({
component: RootLayout,
})
const indexRoute = createRoute({
getParentRoute: () => rootRoute,
path: '/',
component: HomePage,
})
const aboutRoute = createRoute({
getParentRoute: () => rootRoute,
path: '/about',
component: AboutPage,
})
const routeTree = rootRoute.addChildren([indexRoute, aboutRoute])
const router = createRouter({ routeTree })
File-Based Routing
File-based routing automatically generates the route tree from your file structure. Configure with Vite plugin:
// vite.config.ts
import { defineConfig } from 'vite'
import { TanStackRouterVite } from '@tanstack/router-plugin/vite'
export default defineConfig({
plugins: [
TanStackRouterVite(),
// ... other plugins
],
})
File Naming Conventions
| File Pattern | Route Type | Example Path |
|---|---|---|
__root.tsx | Root layout | N/A (wraps all) |
index.tsx | Index route | / |
about.tsx | Static route | /about |
$postId.tsx | Dynamic param | /posts/$postId |
posts.tsx | Layout route | /posts/* (layout) |
posts/index.tsx | Nested index | /posts |
posts/$postId.tsx | Nested dynamic | /posts/123 |
posts_.$postId.tsx | Pathless layout | /posts/123 (different layout) |
_layout.tsx | Pathless layout | N/A (groups routes) |
_layout/dashboard.tsx | Grouped route | /dashboard |
$.tsx | Splat/catch-all | /* |
posts.$postId.edit.tsx | Dot notation | /posts/123/edit |
Special Prefixes
_prefix: Pathless routes (layout groups without URL segment)$prefix: Dynamic path parameters(folder)parentheses: Route groups (organizational, no URL impact)
Route Configuration
Each route can define:
// routes/posts.$postId.tsx
import { createFileRoute } from '@tanstack/react-router'
export const Route = createFileRoute('/posts/$postId')({
// Validation for path params
params: {
parse: (params) => ({ postId: Number(params.postId) }),
stringify: (params) => ({ postId: String(params.postId) }),
},
// Search params validation
validateSearch: (search: Record<string, unknown>) => {
return {
page: Number(search.page ?? 1),
filter: (search.filter as string) || '',
}
},
// Data loading
loader: async ({ params, context, abortController }) => {
return fetchPost(params.postId)
},
// Loader dependencies (re-run loader when these change)
loaderDeps: ({ search }) => ({ page: search.page }),
// Stale time for cached loader data
staleTime: 5_000,
// Preloading
preloadStaleTime: 30_000,
// Error component
errorComponent: PostErrorComponent,
// Pending/loading component
pendingComponent: PostLoadingComponent,
// 404 component
notFoundComponent: PostNotFoundComponent,
// Before load hook (authentication, redirects)
beforeLoad: async ({ context, location }) => {
if (!context.auth.isAuthenticated) {
throw redirect({
to: '/login',
search: { redirect: location.href },
})
}
},
// Head/meta management
head: () => ({
meta: [{ title: 'Post Details' }],
}),
// Component
component: PostComponent,
})
function PostComponent() {
const { postId } = Route.useParams()
const post = Route.useLoaderData()
const { page, filter } = Route.useSearch()
return <div>{post.title}</div>
}
Data Loading
Route Loaders
export const Route = createFileRoute('/posts')({
loader: async ({ context }) => {
// Access router context (e.g., queryClient)
const posts = await context.queryClient.ensureQueryData({
queryKey: ['posts'],
queryFn: fetchPosts,
})
return { posts }
},
component: PostsComponent,
})
function PostsComponent() {
const { posts } = Route.useLoaderData()
// ...
}
Loader Dependencies
Control when loaders re-execute:
export const Route = createFileRoute('/posts')({
loaderDeps: ({ search: { page, filter } }) => ({ page, filter }),
loader: async ({ deps: { page, filter } }) => {
return fetchPosts({ page, filter })
},
})
Deferred Data Loading
Stream non-critical data:
import { Await, defer } from '@tanstack/react-router'
export const Route = createFileRoute('/dashboard')({
loader: async () => {
const criticalData = await fetchCriticalData()
const deferredData = defer(fetchSlowData())
return { criticalData, deferredData }
},
component: DashboardComponent,
})
function DashboardComponent() {
const { criticalData, deferredData } = Route.useLoaderData()
return (
<div>
<CriticalSection data={criticalData} />
<Suspense fallback={<Loading />}>
<Await promise={deferredData}>
{(data) => <SlowSection data={data} />}
</Await>
</Suspense>
</div>
)
}
Context-Based Data Loading
Provide shared dependencies via router context:
// Create router with context
const router = createRouter({
routeTree,
context: {
queryClient,
auth: undefined!, // Will be provided by RouterProvider
},
})
// In root/app component
function App() {
const auth = useAuth()
return <RouterProvider router={router} context={{ auth }} />
}
// In routes
export const Route = createFileRoute('/protected')({
beforeLoad: ({ context }) => {
if (!context.auth.user) throw redirect({ to: '/login' })
},
loader: ({ context }) => {
return context.queryClient.ensureQueryData(userQueryOptions())
},
})
Search Parameters
Validation
import { z } from 'zod'
const postSearchSchema = z.object({
page: z.number().default(1),
filter: z.string().default(''),
sort: z.enum(['date', 'title']).default('date'),
})
export const Route = createFileRoute('/posts')({
validateSearch: postSearchSchema,
// Or manual validation:
// validateSearch: (search) => postSearchSchema.parse(search),
})
Reading Search Params
function PostsComponent() {
// From route
const { page, filter, sort } = Route.useSearch()
// Or from any component with useSearch hook
const search = useSearch({ from: '/posts' })
}
Updating Search Params
import { useNavigate } from '@tanstack/react-router'
function Pagination() {
const navigate = useNavigate()
const { page } = Route.useSearch()
return (
<button
onClick={() =>
navigate({
search: (prev) => ({ ...prev, page: prev.page + 1 }),
})
}
>
Next Page
</button>
)
}
// Or via Link component
<Link
to="/posts"
search={(prev) => ({ ...prev, page: 2 })}
>
Page 2
</Link>
Search Param Options
const router = createRouter({
routeTree,
// Custom serialization
search: {
strict: true, // Reject unknown params
},
// Default search param serializer
stringifySearch: defaultStringifySearch,
parseSearch: defaultParseSearch,
})
Navigation
Link Component
import { Link } from '@tanstack/react-router'
// Static route
<Link to="/about">About</Link>
// Dynamic route wi