# 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: () => , // Render when an error occurs errorComponent: ({ error, reset }) => (

{error.message}

), component: PostPage, }) function PostPage() { const { post, tabData } = Route.useLoaderData() const { tab } = Route.useSearch() const { postId } = Route.useParams() return
{post.title}
} ``` --- ## `createRootRoute` / `createRootRouteWithContext` — Define the root route Creates the top-level route that wraps every other route. Use `createRootRouteWithContext()` 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()({ // 404 handler — shown when no route matches notFoundComponent: () =>
Page not found
, component: RootLayout, }) function RootLayout() { return (
) } ``` --- ## `` component — Type-safe navigation links Renders an `` 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 ( ) } ``` --- ## `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) => { 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
...
} ``` --- ## `` 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 ( ) } ``` --- ## `useMatchRoute` / `` — 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 ( Users {(match) => match ? : null} ) } // 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
    {posts.map((p) =>
  • {p.title}
  • )}
} ``` --- ## `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 `` 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 (
{/* Functional update — increment page without touching other params */} ({ ...prev, page: prev.page + 1 })}> Next Page {/* Imperative search update */}
) } ``` --- ## 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 (

{post.title}

{post.body}

{/* Renders fallback until reviewsPromise resolves */} Loading reviews…

}> {(reviews) => (
    {reviews.map((r) =>
  • {r.text}
  • )}
)}
) } ``` --- ## 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: () =>
Loading…
, errorComponent: ({ error }) =>
Error: {error.message}
, }) function Posts() { return
Posts list
} ``` --- ## `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 (

{post.title}

Post ID: {postId} | Tab: {tab}

) } ``` --- ## `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) => createPost({ data: values }), onSuccess: () => queryClient.invalidateQueries({ queryKey: ['posts'] }), }) return (
{ e.preventDefault() const fd = new FormData(e.currentTarget) mutation.mutate({ title: fd.get('title') as string, body: fd.get('body') as string }) }}>