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 with params < Link to = " /posts/$postId " params = { { postId : '123' } }
Post 123 </ Link
// With search params < Link to = " /posts " search = { { page : 2 , filter : 'react' } }
Page 2 </ Link
// Active link styling < Link to = " /posts " activeProps = { { className : 'active' } } inactiveProps = { { className : 'inactive' } } activeOptions = { { exact : true } }
Posts </ Link
// Preloading < Link to = " /posts " preload = " intent "
Posts </ Link
< Link to = " /dashboard " preload = " viewport "
Dashboard </ Link
// Hash < Link to = " /docs " hash = " api-reference "
API Reference </ Link
Programmatic Navigation import { useNavigate , useRouter } from '@tanstack/react-router' function MyComponent ( ) { const navigate = useNavigate ( ) const router = useRouter ( ) // Navigate to a route navigate ( { to : '/posts' , search : { page : 1 } } ) // Navigate with replace navigate ( { to : '/posts' , replace : true } ) // Relative navigation navigate ( { to : '.' , search : ( prev ) => ( { ... prev , page : 2 } ) } ) // Go back/forward router . history . back ( ) router . history . forward ( ) // Invalidate and reload current route router . invalidate ( ) } Redirects import { redirect } from '@tanstack/react-router' // In beforeLoad or loader throw redirect ( { to : '/login' , search : { redirect : location . href } , // Optional status code statusCode : 301 , // Permanent redirect (SSR) } ) Navigation Blocking import { useBlocker } from '@tanstack/react-router' function FormComponent ( ) { const [ isDirty , setIsDirty ] = useState ( false ) useBlocker ( { shouldBlockFn : ( ) => isDirty , withResolver : true , // Shows confirm dialog } ) // Or with custom UI const { proceed , reset , status } = useBlocker ( { shouldBlockFn : ( ) => isDirty , } ) if ( status === 'blocked' ) { return ( < div
< p
Are you sure you want to leave? </ p
< button onClick = { proceed }
Leave </ button
< button onClick = { reset }
Stay </ button
</ div
) } } Code Splitting Automatic (File-Based Routing) With file-based routing, create a lazy file: routes/ posts.tsx # Critical: loader, beforeLoad, meta posts.lazy.tsx # Lazy: component, pendingComponent, errorComponent // posts.tsx (loaded eagerly) export const Route = createFileRoute ( '/posts' ) ( { loader : ( ) => fetchPosts ( ) , } ) // posts.lazy.tsx (loaded lazily) import { createLazyFileRoute } from '@tanstack/react-router' export const Route = createLazyFileRoute ( '/posts' ) ( { component : PostsComponent , pendingComponent : PostsLoading , errorComponent : PostsError , } ) Manual Code Splitting const postsRoute = createRoute ( { getParentRoute : ( ) => rootRoute , path : '/posts' , loader : ( ) => fetchPosts ( ) , } ) . lazy ( ( ) => import ( './posts.lazy' ) . then ( ( d ) => d . Route ) ) Preloading // Router-level defaults const router = createRouter ( { routeTree , defaultPreload : 'intent' , // 'intent' | 'viewport' | 'render' | false defaultPreloadStaleTime : 30_000 , // 30 seconds } ) // Route-level export const Route = createFileRoute ( '/posts/$postId' ) ( { // Stale time for the loader data staleTime : 5_000 , // How long preloaded data stays fresh preloadStaleTime : 30_000 , } ) // Link-level < Link to = " /posts " preload = " intent " preloadDelay = { 100 }
Posts </ Link
Type Safety Register Router Type // Declare module for type inference declare module '@tanstack/react-router' { interface Register { router : typeof router } } Type-Safe Hooks All hooks are fully typed based on the route tree: // useParams - typed to route's params const { postId } = useParams ( { from : '/posts/$postId' } ) // useSearch - typed to route's search schema const { page } = useSearch ( { from : '/posts' } ) // useLoaderData - typed to loader return const data = useLoaderData ( { from : '/posts/$postId' } ) // useRouteContext - typed to route context const { auth } = useRouteContext ( { from : '/protected' } ) Route Generics import { createFileRoute } from '@tanstack/react-router' export const Route = createFileRoute ( '/posts/$postId' ) ( { // TypeScript infers: // params: { postId: string } // search: validated search schema type // loaderData: return type of loader // context: router context type } ) Authenticated Routes // __root.tsx export const Route = createRootRouteWithContext < { auth : AuthContext }
( ) ( { component : RootComponent , } ) // _authenticated.tsx (pathless layout for auth) export const Route = createFileRoute ( '/_authenticated' ) ( { beforeLoad : ( { context , location } ) => { if ( ! context . auth . isAuthenticated ) { throw redirect ( { to : '/login' , search : { redirect : location . href } , } ) } } , } ) // _authenticated/dashboard.tsx export const Route = createFileRoute ( '/_authenticated/dashboard' ) ( { component : Dashboard , // Only accessible when authenticated } ) Scroll Restoration const router = createRouter ( { routeTree , // Enable scroll restoration defaultScrollRestoration : true , } ) // Or per-route export const Route = createFileRoute ( '/posts' ) ( { // Scroll to top on navigation scrollRestoration : true , } ) // Custom scroll restoration key < ScrollRestoration getKey = { ( location ) => location . pathname } /> Route Masking Display a different URL than the actual route: < Link to = " /photos/$photoId " params = { { photoId : photo . id } } mask = { { to : '/photos' , search : { photoId : photo . id } } }
View Photo </ Link
// Or programmatically navigate ( { to : '/photos/$photoId' , params : { photoId : photo . id } , mask : { to : '/photos' , search : { photoId : photo . id } } , } ) Not Found Handling // Global 404 const router = createRouter ( { routeTree , defaultNotFoundComponent : ( ) => < div
Page not found </ div
, } ) // Route-level 404 export const Route = createFileRoute ( '/posts/$postId' ) ( { loader : async ( { params } ) => { const post = await fetchPost ( params . postId ) if ( ! post ) throw notFound ( ) return post } , notFoundComponent : ( ) => < div
Post not found </ div
, } ) Head Management export const Route = createFileRoute ( '/posts/$postId' ) ( { head : ( { loaderData } ) => ( { meta : [ { title : loaderData . title } , { name : 'description' , content : loaderData . excerpt } , { property : 'og:title' , content : loaderData . title } , ] , links : [ { rel : 'canonical' , href :
https://example.com/posts/ ${ loaderData . id }} , ] , } ) , } ) Integration with TanStack Query import { queryOptions } from '@tanstack/react-query' const postsQueryOptions = queryOptions ( { queryKey : [ 'posts' ] , queryFn : fetchPosts , } ) export const Route = createFileRoute ( '/posts' ) ( { loader : ( { context : { queryClient } } ) => { // Ensure data is in cache, won't refetch if fresh return queryClient . ensureQueryData ( postsQueryOptions ) } , component : PostsComponent , } ) function PostsComponent ( ) { // Use the same query options for reactive updates const { data : posts } = useSuspenseQuery ( postsQueryOptions ) return < PostsList posts = { posts } /> } Router Hooks Reference Hook Purpose useRouter() Access router instance useRouterState() Subscribe to router state useParams() Get route path params useSearch() Get validated search params useLoaderData() Get route loader data useRouteContext() Get route context useNavigate() Get navigate function useLocation() Get current location useMatches() Get all matched routes useMatch() Get specific route match useBlocker() Block navigation useLinkProps() Get link props for custom components useMatchRoute() Check if a route matches Best Practices Use file-based routing for most applications - it's simpler and auto-generates the route tree Validate search params with Zod or custom validators for type safety Use loaderDeps to control when loaders re-execute based on search param changes Leverage context for dependency injection (QueryClient, auth state) Use beforeLoad for authentication guards, not in components Separate critical vs lazy code - keep loaders in the main file, components in .lazy.tsx Use preload="intent" on Links for perceived performance Use staleTime to prevent unnecessary refetches during navigation Register the router type for full TypeScript inference across the app Use notFound() instead of conditional rendering for 404 states Colocate search param logic with routes that own them Use pathless layouts ( _authenticated ) for shared auth/layout logic without URL segments Common Pitfalls Forgetting to register the router type ( declare module ) Not using loaderDeps when loader depends on search params (causes stale data) Putting auth checks in components instead of beforeLoad (flash of protected content) Not handling the loading state with pendingComponent Using useEffect for data fetching instead of route loaders Mutating search params directly instead of using navigate/Link Not wrapping the app with RouterProvider Forgetting getParentRoute in code-based route definitions