Try Live
Add Docs
Rankings
Pricing
Enterprise
Docs
Install
Install
Docs
Pricing
Enterprise
More...
More...
Try Live
Rankings
Add Docs
TanStack Router
https://github.com/tanstack/router
Admin
🤖 Fully typesafe Router for React (and friends) w/ built-in caching, 1st class search-param APIs,
...
Tokens:
585,077
Snippets:
2,433
Trust Score:
8
Update:
3 hours ago
Context
Skills
Chat
Benchmark
94.7
Suggestions
Latest
Show doc for...
Code
Info
Show Results
Context Summary (auto-generated)
Raw
Copy
Link
# TanStack Router & TanStack Start TanStack Router is a fully type-safe, framework-agnostic routing library for React, Solid, and Vue applications. It offers end-to-end TypeScript inference across route paths, path parameters, search parameters, and loader data — all without manual type annotations. It provides built-in SWR caching for route loaders, nested layout support, deferred data loading with streaming, file-based route generation, and schema-driven search param validation via Zod, Valibot, ArkType, or Effect/Schema. The library is available as `@tanstack/react-router`, `@tanstack/solid-router`, and `@tanstack/vue-router`. TanStack Start is the companion full-stack framework built on top of TanStack Router. It adds full-document SSR and streaming, type-safe server functions (RPC), middleware, deployment-ready Vite/Rsbuild builds, and React Server Components support. The packages `@tanstack/react-start`, `@tanstack/solid-start`, and `@tanstack/vue-start` extend their respective router packages with server capabilities. Together, Router and Start form a cohesive solution covering everything from client-side routing through to production-ready server rendering. --- ## `createRouter` — Create a router instance Creates and configures the central router instance that manages the entire route tree. The `routeTree` is the only required option. After creating the router, register its type globally via TypeScript declaration merging to enable full project-wide type inference. ```tsx // src/router.tsx import { createRouter } from '@tanstack/react-router' import { routeTree } from './routeTree.gen' // auto-generated by file-based routing export const router = createRouter({ routeTree, defaultPreload: 'intent', // preload routes on link hover defaultStaleTime: 10_000, // cache loader data for 10 s by default defaultPreloadStaleTime: 0, // always re-run loaders when used with external cache context: { queryClient, // pass any context down to all routes }, }) // Register the router type once — unlocks full TypeScript inference everywhere declare module '@tanstack/react-router' { interface Register { router: typeof router } } ``` --- ## File-Based Route Structure — Filesystem conventions TanStack Router generates a type-safe route tree from files inside `src/routes/`. Special prefixes and suffixes control nesting, layouts, path params, and lazy loading. ``` src/routes/ ├── __root.tsx # Always-rendered root layout ├── index.tsx # Matches "/" ├── about.tsx # Matches "/about" ├── posts.tsx # Layout for "/posts/*" ├── posts/ │ ├── index.tsx # Matches "/posts" (exact) │ ├── $postId.tsx # Matches "/posts/:postId" │ └── $postId.edit.tsx # Matches "/posts/:postId/edit" ├── _auth.tsx # Pathless layout (no URL segment) ├── _auth/ │ └── dashboard.tsx # Matches "/dashboard" (wrapped in _auth) └── files.$.tsx # Wildcard splat: "/files/*" ``` Flat-file alternative using `.` notation: ``` posts.$postId.tsx → /posts/:postId settings.profile.tsx → /settings/profile _pathlessLayout.route-a.tsx → /route-a (inside pathless layout) ``` --- ## `createFileRoute` — Define a file-based route Used inside every file under `src/routes/`. Accepts the route path as a type parameter and returns a route definition object. Supports `loader`, `beforeLoad`, `validateSearch`, `component`, `errorComponent`, `pendingComponent`, and more. ```tsx // src/routes/posts/$postId.tsx import { createFileRoute } from '@tanstack/react-router' import { z } from 'zod' export const Route = createFileRoute('/posts/$postId')({ // Validate search params with Zod — types are inferred automatically validateSearch: z.object({ tab: z.enum(['details', 'comments', 'related']).catch('details'), }), // Declare which search params the loader depends on (for cache keying) loaderDeps: ({ search: { tab } }) => ({ tab }), // Fetch data before rendering; receives params, deps, context, abortController loader: async ({ params: { postId }, deps: { tab }, abortController }) => { const [post, tabData] = await Promise.all([ fetchPost(postId, { signal: abortController.signal }), fetchTabData(postId, tab, { signal: abortController.signal }), ]) return { post, tabData } }, // Show this while loader is pending (after 1 s threshold) pendingComponent: () => <Spinner />, // Render when an error occurs errorComponent: ({ error, reset }) => ( <div> <p>{error.message}</p> <button onClick={reset}>Retry</button> </div> ), component: PostPage, }) function PostPage() { const { post, tabData } = Route.useLoaderData() const { tab } = Route.useSearch() const { postId } = Route.useParams() return <div>{post.title}</div> } ``` --- ## `createRootRoute` / `createRootRouteWithContext` — Define the root route Creates the top-level route that wraps every other route. Use `createRootRouteWithContext<T>()` to inject typed context (e.g., auth state, query client) that is available to all descendant routes. ```tsx // src/routes/__root.tsx import { createRootRouteWithContext, Outlet, Link, } from '@tanstack/react-router' import type { QueryClient } from '@tanstack/react-query' interface RouterContext { queryClient: QueryClient auth: { isAuthenticated: boolean; user: User | null } } export const Route = createRootRouteWithContext<RouterContext>()({ // 404 handler — shown when no route matches notFoundComponent: () => <div>Page not found</div>, component: RootLayout, }) function RootLayout() { return ( <div> <nav> <Link to="/" activeOptions={{ exact: true }}>Home</Link> <Link to="/posts">Posts</Link> <Link to="/dashboard">Dashboard</Link> </nav> <main> <Outlet /> </main> </div> ) } ``` --- ## `<Link>` component — Type-safe navigation links Renders an `<a>` tag with a fully type-safe `href`. Supports path params, search params, hash, active styling, intent-based preloading, and optional parameters. ```tsx import { Link } from '@tanstack/react-router' function Navigation() { return ( <nav> {/* Static link — exact active match for home */} <Link to="/" activeOptions={{ exact: true }} activeProps={{ className: 'font-bold' }}> Home </Link> {/* Dynamic path param */} <Link to="/posts/$postId" params={{ postId: '42' }}> Post 42 </Link> {/* Update a single search param functionally without touching others */} <Link to="." search={(prev) => ({ ...prev, page: (prev.page ?? 1) + 1 })}> Next Page </Link> {/* Preload on hover with 100 ms delay */} <Link to="/dashboard" preload="intent" preloadDelay={100}> Dashboard </Link> {/* Hash link */} <Link to="/posts/$postId" params={{ postId: '42' }} hash="comments"> View comments </Link> {/* Optional path parameter — include or omit */} <Link to="/posts/{-$category}" params={{ category: 'tech' }}>Tech</Link> <Link to="/posts/{-$category}" params={{ category: undefined }}>All</Link> {/* Render prop: access isActive in children */} <Link to="/posts"> {({ isActive }) => ( <span style={{ fontWeight: isActive ? 'bold' : 'normal' }}>Posts</span> )} </Link> </nav> ) } ``` --- ## `useNavigate` — Imperative navigation hook Returns a `navigate` function for programmatic navigation, ideal for side-effects like post-form-submission redirects. Accepts all `NavigateOptions` including `replace`, `resetScroll`, and `viewTransition`. ```tsx import { useNavigate } from '@tanstack/react-router' function CreatePostForm() { const navigate = useNavigate({ from: '/posts' }) const handleSubmit = async (e: React.FormEvent<HTMLFormElement>) => { e.preventDefault() const formData = new FormData(e.currentTarget) const { id } = await createPost({ title: formData.get('title') as string, }) // Navigate to the new post, replacing history so back button goes to /posts navigate({ to: '/posts/$postId', params: { postId: id }, replace: true, }) } return <form onSubmit={handleSubmit}>...</form> } ``` --- ## `<Navigate>` component — Declarative immediate redirect Renders nothing and navigates immediately on mount. Useful for client-side conditional redirects within the component tree. ```tsx import { Navigate } from '@tanstack/react-router' function LegacyRoute() { // Redirect old URLs to new structure on mount return ( <Navigate to="/posts/$postId" params={{ postId: 'welcome' }} replace /> ) } ``` --- ## `useMatchRoute` / `<MatchRoute>` — Conditional rendering based on route match Check whether a route matches (including pending transitions) to power optimistic UI or conditional rendering. ```tsx import { Link, MatchRoute, useMatchRoute } from '@tanstack/react-router' import { useEffect } from 'react' // Component version — show spinner while navigating to /users function NavItem() { return ( <Link to="/users"> Users <MatchRoute to="/users" pending> {(match) => match ? <Spinner /> : null} </MatchRoute> </Link> ) } // Hook version — programmatic check function SideEffect() { const matchRoute = useMatchRoute() useEffect(() => { if (matchRoute({ to: '/users', pending: true })) { console.log('Navigating to /users...') } }) return null } ``` --- ## Route `loader` — Data fetching with built-in SWR cache Loaders run before a route renders and their results are cached with stale-while-revalidate semantics. Control caching via `staleTime`, `gcTime`, `loaderDeps`, and `shouldReload`. ```tsx // src/routes/posts.tsx import { createFileRoute } from '@tanstack/react-router' import { z } from 'zod' export const Route = createFileRoute('/posts')({ validateSearch: z.object({ offset: z.number().int().nonnegative().catch(0), limit: z.number().int().positive().catch(20), }), // Expose only used search params to the cache key loaderDeps: ({ search: { offset, limit } }) => ({ offset, limit }), loader: { handler: async ({ deps: { offset, limit }, abortController }) => { const posts = await fetch(`/api/posts?offset=${offset}&limit=${limit}`, { signal: abortController.signal, }).then((r) => r.json()) return posts }, // Wait for fresh data before rendering instead of showing stale content staleReloadMode: 'blocking', }, // Cache results for 30 s; preloads cached for 60 s staleTime: 30_000, gcTime: 5 * 60 * 1000, component: PostsList, }) function PostsList() { const posts = Route.useLoaderData() return <ul>{posts.map((p) => <li key={p.id}>{p.title}</li>)}</ul> } ``` --- ## `beforeLoad` — Pre-load middleware (auth guards, context injection) Runs serially before the loader and all child `beforeLoad` calls. Throwing a `redirect()` aborts the load chain. Returning an object merges it into the route context available to the loader and child routes. ```tsx // src/routes/_authenticated.tsx import { createFileRoute, redirect, isRedirect } from '@tanstack/react-router' export const Route = createFileRoute('/_authenticated')({ beforeLoad: async ({ context, location }) => { try { const user = await context.auth.verifySession() if (!user) { throw redirect({ to: '/login', search: { redirect: location.href }, }) } // Merge user into context — available to all child loaders return { user } } catch (error) { if (isRedirect(error)) throw error throw redirect({ to: '/login', search: { redirect: location.href } }) } }, }) // src/routes/dashboard.tsx — child route reads user from context export const Route = createFileRoute('/dashboard')({ loader: ({ context: { user } }) => fetchDashboard(user.id), component: Dashboard, }) ``` --- ## Search Params — Validated, typed URL state TanStack Router treats search params as JSON-first structured state. Validate with `validateSearch` using plain functions or schema libraries. Read with `useSearch()`, write via `<Link search>` or `navigate({ search })`. ```tsx // src/routes/shop/products.tsx import { createFileRoute, Link, retainSearchParams, stripSearchParams } from '@tanstack/react-router' import { zodValidator } from '@tanstack/zod-adapter' import { z } from 'zod' const defaults = { page: 1, sort: 'newest' as const } const productSearchSchema = z.object({ page: z.number().default(defaults.page), filter: z.string().default(''), sort: z.enum(['newest', 'oldest', 'price']).default(defaults.sort), }) export const Route = createFileRoute('/shop/products')({ validateSearch: zodValidator(productSearchSchema), search: { // Persist `filter` across every outgoing link on this route tree // Strip `page` and `sort` when they hold their default values middlewares: [ retainSearchParams(['filter']), stripSearchParams(defaults), ], }, component: ProductList, }) function ProductList() { const { page, filter, sort } = Route.useSearch() const navigate = Route.useNavigate() return ( <div> {/* Functional update — increment page without touching other params */} <Link from={Route.fullPath} search={(prev) => ({ ...prev, page: prev.page + 1 })}> Next Page </Link> {/* Imperative search update */} <button onClick={() => navigate({ search: (prev) => ({ ...prev, sort: 'price' }) })}> Sort by price </button> </div> ) } ``` --- ## Search Param Adapters — Zod, Valibot, ArkType, Effect/Schema Plug any Standard Schema-compatible validation library directly into `validateSearch`. ```tsx // Valibot (no adapter needed — implements Standard Schema) import * as v from 'valibot' export const Route = createFileRoute('/shop/products/')({ validateSearch: v.object({ page: v.optional(v.fallback(v.number(), 1), 1), sort: v.optional(v.fallback(v.picklist(['newest', 'oldest', 'price']), 'newest'), 'newest'), }), }) // ArkType (no adapter needed) import { type } from 'arktype' export const Route = createFileRoute('/shop/products/')({ validateSearch: type({ page: 'number = 1', sort: '"newest" | "oldest" | "price" = "newest"' }), }) ``` --- ## Deferred Data Loading — `Await` component and unawaited promises Return an unawaited promise from a loader to render fast data immediately while slow data streams in. Works in both client-side navigation and SSR streaming. ```tsx // src/routes/posts/$postId.tsx import { createFileRoute, Await } from '@tanstack/react-router' export const Route = createFileRoute('/posts/$postId')({ loader: async ({ params: { postId } }) => { // Start slow request but don't await it const reviewsPromise = fetchReviews(postId) // Await only the fast data const post = await fetchPost(postId) return { post, reviewsPromise } }, component: PostPage, }) function PostPage() { const { post, reviewsPromise } = Route.useLoaderData() return ( <article> <h1>{post.title}</h1> <p>{post.body}</p> {/* Renders fallback until reviewsPromise resolves */} <Await promise={reviewsPromise} fallback={<p>Loading reviews…</p>}> {(reviews) => ( <ul> {reviews.map((r) => <li key={r.id}>{r.text}</li>)} </ul> )} </Await> </article> ) } ``` --- ## Code Splitting — Automatic and manual lazy routes Split non-critical route code (component, error boundary, pending state) into separate async chunks. Enable globally with `autoCodeSplitting: true` in the bundler plugin, or manually with `.lazy.tsx` suffix files. ```ts // vite.config.ts — enable automatic code splitting import { tanstackRouter } from '@tanstack/router-plugin/vite' export default { plugins: [ tanstackRouter({ autoCodeSplitting: true }), react(), ], } ``` ```tsx // Manual split: critical config stays in posts.tsx // src/routes/posts.tsx import { createFileRoute } from '@tanstack/react-router' import { fetchPosts } from '../api' export const Route = createFileRoute('/posts')({ loader: fetchPosts, // component is omitted here — loaded lazily from posts.lazy.tsx }) ``` ```tsx // src/routes/posts.lazy.tsx — component chunk loaded on demand import { createLazyFileRoute } from '@tanstack/react-router' export const Route = createLazyFileRoute('/posts')({ component: Posts, pendingComponent: () => <div>Loading…</div>, errorComponent: ({ error }) => <div>Error: {error.message}</div>, }) function Posts() { return <div>Posts list</div> } ``` --- ## `getRouteApi` — Access route APIs from split files Retrieve a route's fully-typed hooks (`useLoaderData`, `useSearch`, `useParams`, etc.) in a file that does not import the route definition directly — avoiding circular dependencies in code-split setups. ```tsx // src/components/PostDetails.tsx import { getRouteApi } from '@tanstack/react-router' // Reference the route by its path string — fully type-safe const routeApi = getRouteApi('/posts/$postId') export function PostDetails() { const { post } = routeApi.useLoaderData() // ^? inferred from the route's loader return type const { postId } = routeApi.useParams() const { tab } = routeApi.useSearch() return ( <div> <h2>{post.title}</h2> <p>Post ID: {postId} | Tab: {tab}</p> </div> ) } ``` --- ## `createServerFn` — Type-safe server functions (TanStack Start) Defines server-only RPC endpoints that can be called from loaders, components, or other server functions. The build process replaces server implementations with lightweight stubs in client bundles. ```tsx // src/utils/posts.functions.ts import { createServerFn } from '@tanstack/react-start' import { z } from 'zod' const PostSchema = z.object({ title: z.string().min(1), body: z.string().min(1), }) // GET — typically used for data fetching export const getPosts = createServerFn({ method: 'GET' }).handler(async () => { // Runs only on the server — safe to use DB, env vars, filesystem return db.query.posts.findMany({ orderBy: desc(posts.createdAt) }) }) // POST — with Zod input validation export const createPost = createServerFn({ method: 'POST' }) .inputValidator(PostSchema) .handler(async ({ data }) => { const post = await db.insert(posts).values(data).returning() return post[0] }) // Use in a route loader export const Route = createFileRoute('/posts')({ loader: () => getPosts(), }) // Use in a component with TanStack Query function NewPostForm() { const queryClient = useQueryClient() const mutation = useMutation({ mutationFn: (values: z.infer<typeof PostSchema>) => createPost({ data: values }), onSuccess: () => queryClient.invalidateQueries({ queryKey: ['posts'] }), }) return ( <form onSubmit={(e) => { e.preventDefault() const fd = new FormData(e.currentTarget) mutation.mutate({ title: fd.get('title') as string, body: fd.get('body') as string }) }}> <input name="title" placeholder="Title" /> <textarea name="body" placeholder="Body" /> <button type="submit" disabled={mutation.isPending}> {mutation.isPending ? 'Creating…' : 'Create Post'} </button> </form> ) } ``` --- ## `createCsrfMiddleware` / `createStart` — Application-level middleware (TanStack Start) Configures global request middleware for the Start server. CSRF protection is installed automatically unless you define `src/start.ts`, in which case you must add it manually. ```tsx // src/start.ts import { createStart, createCsrfMiddleware } from '@tanstack/react-start' const csrfMiddleware = createCsrfMiddleware({ filter: (ctx) => ctx.handlerType === 'serverFn', // Optional: allow a specific public origin // origin: 'https://app.example.com', }) export const startInstance = createStart(() => ({ requestMiddleware: [csrfMiddleware], })) ``` --- ## `redirect` / `notFound` — Throw-based navigation control These utilities are thrown (not returned) inside `loader` and `beforeLoad` to declaratively redirect users or trigger 404 handling. `isRedirect` distinguishes redirects from genuine errors in catch blocks. ```tsx import { createFileRoute, redirect, notFound, isRedirect, ErrorComponent, } from '@tanstack/react-router' export const Route = createFileRoute('/posts/$postId')({ loader: async ({ params: { postId }, context }) => { const post = await fetchPost(postId) // Trigger a 404 if the post doesn't exist if (!post) throw notFound() // Redirect if the post was moved to a new slug if (post.redirectTo) { throw redirect({ to: '/posts/$postId', params: { postId: post.redirectTo } }) } return post }, notFoundComponent: () => <div>This post doesn't exist.</div>, errorComponent: ({ error }) => { if (error instanceof UnauthorizedError) return <div>Access denied.</div> // Fall back to the default error renderer for unexpected errors return <ErrorComponent error={error} /> }, }) ``` --- ## `useParams` / `useRouteContext` / `useRouter` — Route state hooks Access path parameters, route-specific context, and the router instance from any component within the matched route tree. ```tsx import { useParams, useRouteContext, useRouter } from '@tanstack/react-router' function PostActions() { // Path params — typed from the route definition const { postId } = useParams({ from: '/posts/$postId' }) // Route context — typed from createRootRouteWithContext + beforeLoad returns const { user, queryClient } = useRouteContext({ from: '/_authenticated' }) // Full router instance — navigate, invalidate, history, etc. const router = useRouter() const handleDelete = async () => { await deletePost(postId) // Invalidate all active route loaders and mark cache as stale router.invalidate() router.navigate({ to: '/posts' }) } return ( <button onClick={handleDelete} disabled={!user.canDelete}> Delete Post </button> ) } ``` --- ## Summary TanStack Router is used primarily in two patterns: as a standalone client-side router in existing React/Solid/Vue applications, and as the foundation of TanStack Start for full-stack SSR applications. In client-side setups, the library handles all routing concerns — file-based route generation, type-safe navigation, search param management as structured JSON state, built-in SWR loader caching, authentication guards via `beforeLoad`, and automatic or manual code splitting. The router integrates seamlessly with TanStack Query for external caching by simply setting `defaultPreloadStaleTime: 0` so every loader event is forwarded to the query client. In full-stack applications with TanStack Start, the same routing APIs are extended with `createServerFn` for type-safe server RPCs, streaming SSR via deferred loaders, CSRF middleware, and deployment-ready builds targeting Node, Bun, Cloudflare Workers, Netlify, and more. The TypeScript declaration merging pattern (`declare module '@tanstack/react-router' { interface Register { router: typeof router } }`) is the key integration point — it propagates the full route tree type through the entire codebase, enabling autocomplete for paths, parameter shapes, search schemas, and loader data without any manual type plumbing.