Tanstack Router

作者 tanstack-skills6f5521ecbdfb無授權條款35 個星標收錄於 2026年10月8日更新於 2026年10月8日儲存庫8 個月前更新

Type-safe routing for React and Solid applications with first-class search params, data loading, and seamless integration with the React ecosystem.

AI 產生的概覽

在 React 與 Solid 應用程式中使用 TanStack Router 建立型別安全路由的參考指南。

功能
此技能是 TanStack Router 的教學參考,涵蓋路由樹、以檔案為基礎的路由慣例、路由設定、資料載入、搜尋參數驗證、導覽、程式碼分割、預先載入、型別安全、驗證守衛、捲動還原、路由遮蔽、找不到頁面處理、head 管理,以及與 TanStack Query 的整合。內容以程式碼範例與表格呈現,而非可執行的工具。文中也列出最佳實務與常見陷阱。
適用情境
適用於在採用 TanStack Router 的 React 或 Solid 應用程式中實作或檢視路由時。適合處理關於以檔案為基礎的路由、具型別的搜尋參數、loader、重新導向或路由層級程式碼分割的問題。
執行需求
本指南假定已有 JavaScript 或 TypeScript 專案並安裝 @tanstack/react-router 套件;以檔案為基礎的路由可選用 @tanstack/router-plugin 或 @tanstack/router-cli,另可選用 @tanstack/react-router-devtools 以及用於搜尋驗證的 Zod。此技能未附帶指令碼,僅為說明文件。

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

bash
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.

tsx
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:

ts
// vite.config.tsimport { defineConfig } from 'vite'import { TanStackRouterVite } from '@tanstack/router-plugin/vite'
export default defineConfig({  plugins: [    TanStackRouterVite(),    // ... other plugins  ],})
File Naming Conventions
File PatternRoute TypeExample Path
__root.tsxRoot layoutN/A (wraps all)
index.tsxIndex route/
about.tsxStatic route/about
$postId.tsxDynamic param/posts/$postId
posts.tsxLayout route/posts/* (layout)
posts/index.tsxNested index/posts
posts/$postId.tsxNested dynamic/posts/123
posts_.$postId.tsxPathless layout/posts/123 (different layout)
_layout.tsxPathless layoutN/A (groups routes)
_layout/dashboard.tsxGrouped route/dashboard
$.tsxSplat/catch-all/*
posts.$postId.edit.tsxDot 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:

tsx
// routes/posts.$postId.tsximport { 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

tsx
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:

tsx
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:

tsx
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:

tsx
// Create router with contextconst router = createRouter({  routeTree,  context: {    queryClient,    auth: undefined!, // Will be provided by RouterProvider  },})
// In root/app componentfunction App() {  const auth = useAuth()  return <RouterProvider router={router} context={{ auth }} />}
// In routesexport const Route = createFileRoute('/protected')({  beforeLoad: ({ context }) => {    if (!context.auth.user) throw redirect({ to: '/login' })  },  loader: ({ context }) => {    return context.queryClient.ensureQueryData(userQueryOptions())  },})

Search Parameters

Validation

tsx
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

tsx
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

tsx
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

tsx
const router = createRouter({  routeTree,  // Custom serialization  search: {    strict: true, // Reject unknown params  },  // Default search param serializer  stringifySearch: defaultStringifySearch,  parseSearch: defaultParseSearch,})

Navigation

Link Component

tsx
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

tsx
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

tsx
import { redirect } from '@tanstack/react-router'
// In beforeLoad or loaderthrow redirect({  to: '/login',  search: { redirect: location.href },  // Optional status code  statusCode: 301, // Permanent redirect (SSR)})

Navigation Blocking

tsx
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
tsx
// 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

tsx
const postsRoute = createRoute({  getParentRoute: () => rootRoute,  path: '/posts',  loader: () => fetchPosts(),}).lazy(() => import('./posts.lazy').then((d) => d.Route))

Preloading

tsx
// Router-level defaultsconst router = createRouter({  routeTree,  defaultPreload: 'intent', // 'intent' | 'viewport' | 'render' | false  defaultPreloadStaleTime: 30_000, // 30 seconds})
// Route-levelexport 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

tsx
// Declare module for type inferencedeclare module '@tanstack/react-router' {  interface Register {    router: typeof router  }}

Type-Safe Hooks

All hooks are fully typed based on the route tree:

tsx
// useParams - typed to route's paramsconst { postId } = useParams({ from: '/posts/$postId' })
// useSearch - typed to route's search schemaconst { page } = useSearch({ from: '/posts' })
// useLoaderData - typed to loader returnconst data = useLoaderData({ from: '/posts/$postId' })
// useRouteContext - typed to route contextconst { auth } = useRouteContext({ from: '/protected' })

Route Generics

tsx
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

tsx
// __root.tsxexport 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.tsxexport const Route = createFileRoute('/_authenticated/dashboard')({  component: Dashboard, // Only accessible when authenticated})

Scroll Restoration

tsx
const router = createRouter({  routeTree,  // Enable scroll restoration  defaultScrollRestoration: true,})
// Or per-routeexport 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:

tsx
<Link  to="/photos/$photoId"  params={{ photoId: photo.id }}  mask={{ to: '/photos', search: { photoId: photo.id } }}>  View Photo</Link>
// Or programmaticallynavigate({  to: '/photos/$photoId',  params: { photoId: photo.id },  mask: { to: '/photos', search: { photoId: photo.id } },})

Not Found Handling

tsx
// Global 404const router = createRouter({  routeTree,  defaultNotFoundComponent: () => <div>Page not found</div>,})
// Route-level 404export 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

tsx
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

tsx
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

HookPurpose
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

  1. Use file-based routing for most applications - it's simpler and auto-generates the route tree
  2. Validate search params with Zod or custom validators for type safety
  3. Use loaderDeps to control when loaders re-execute based on search param changes
  4. Leverage context for dependency injection (QueryClient, auth state)
  5. Use beforeLoad for authentication guards, not in components
  6. Separate critical vs lazy code - keep loaders in the main file, components in .lazy.tsx
  7. Use preload="intent" on Links for perceived performance
  8. Use staleTime to prevent unnecessary refetches during navigation
  9. Register the router type for full TypeScript inference across the app
  10. Use notFound() instead of conditional rendering for 404 states
  11. Colocate search param logic with routes that own them
  12. 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

來源與署名

來源:tanstack-skills/tanstack-skills位於plugins/tanstack-router/skills/tanstack-router提交6f5521e

授權條款: 無授權條款

內容歸原作者所有。SourceWeft 從公開儲存庫中收錄這些內容。

檢舉或申請下架