Skip to content
路由定义

路由定义

TanStack Router 提供多种路由定义方式,覆盖从简单静态页到复杂动态路由的所有场景。


基础路由类型

根路由(Root Route)

// src/routes/__root.tsx
import { createRootRoute, Outlet } from '@tanstack/react-router'

export const Route = createRootRoute({
  component: () => (
    <div>
      <Header />
      <Outlet />  {/* 子路由在此渲染 */}
      <Footer />
    </div>
  ),
  notFoundComponent: () => <div>404 - 页面未找到</div>,
  errorComponent: ({ error }) => <div>出错了:{error.message}</div>,
  pendingComponent: () => <div>加载中...</div>,
})

根路由特性:

  • 没有路径——它不与任何 URL 匹配
  • 始终渲染——所有子路由都在根路由的 <Outlet /> 中渲染
  • 可以定义全局的 notFoundComponenterrorComponentpendingComponent

普通路由

// src/routes/about.tsx
import { createFileRoute } from '@tanstack/react-router'

export const Route = createFileRoute('/about')({
  component: AboutPage,
})

索引路由(Index Route)

index.tsx 是目录的默认路由,当 URL 精确匹配父路径时渲染:

// src/routes/index.tsx → /
export const Route = createFileRoute('/')({
  component: HomePage,
})

// src/routes/posts/index.tsx → /posts
export const Route = createFileRoute('/posts/')({
  component: PostsList,
})

动态路由参数

使用 $ 前缀定义动态路径段:

// src/routes/posts/$postId.tsx
// 匹配:/posts/123、/posts/hello-world
// 不匹配:/posts、/posts/123/comments

import { createFileRoute } from '@tanstack/react-router'

export const Route = createFileRoute('/posts/$postId')({
  component: PostDetail,
  // 可选:参数解析(默认都是 string)
  params: {
    parse: (params) => ({ postId: Number(params.postId) }),
    stringify: ({ postId }) => ({ postId: String(postId) }),
  },
})

使用参数

function PostDetail() {
  const { postId } = Route.useParams() // 类型自动推断
  return <div>文章 ID: {postId}</div>
}

Catch-All / Splat 路由

// src/routes/$.tsx
// 匹配所有未被其他路由匹配的路径

export const Route = createFileRoute('/$')({
  component: CatchAll,
})

function CatchAll() {
  const { _splat } = Route.useParams() // _splat: string
  return <div>未匹配路径: {_splat}</div>
}

路径无关布局(Pathless Layout)

_ 开头的文件/目录是路径无关的,不会出现在 URL 中:

// src/routes/_authenticated.tsx
import { createFileRoute, Outlet, redirect } from '@tanstack/react-router'

export const Route = createFileRoute('/_authenticated')({
  beforeLoad: ({ context, location }) => {
    if (!context.auth.isAuthenticated) {
      throw redirect({
        to: '/login',
        search: { redirect: location.href },
      })
    }
  },
  component: () => (
    <div>
      <Sidebar />
      <Outlet />
    </div>
  ),
})
文件结构                          URL 结构
routes/
├── _authenticated.tsx            ← 路径无关(守卫 + 布局)
├── _authenticated/
│   ├── dashboard.tsx             → /dashboard
│   └── settings.tsx              → /settings
└── _authenticated/_admin.tsx     ← 双重嵌套路径无关
    └── users.tsx                 → /admin/users ❌
                                  → /users      ✅

🚨 陷阱:路径无关布局可以嵌套,但它们都不会出现在 URL 中。_authenticated/_admin/users.tsx 的 URL 是 /users,不是 /admin/users


路由配置选项完整列表

export const Route = createFileRoute('/example')({
  // === 渲染 ===
  component: ExampleComponent,          // 路由匹配成功时渲染

  // === 数据加载 ===
  loader: async ({ params, context }) => {
    return fetchData(params.id)
  },
  beforeLoad: ({ context, location, search }) => {
    // 在 loader 之前执行,可用于守卫和重定向
  },
  loaderDeps: ({ search }) => ({        // 控制 loader 何时重新执行
    page: search.page,
  }),

  // === 搜索参数 ===
  validateSearch: (search) => {         // 验证并解析搜索参数
    return { page: Number(search.page) || 1 }
  },

  // === 参数解析 ===
  params: {
    parse: (params) => ({ id: Number(params.id) }),
    stringify: ({ id }) => ({ id: String(id) }),
  },

  // === 错误与加载状态 ===
  errorComponent: ErrorDisplay,          // loader 失败时渲染
  pendingComponent: LoadingSpinner,      // loader 执行中渲染
  notFoundComponent: NotFound,           // 资源不存在时渲染

  // === 元数据 ===
  staleTime: 30_000,                    // loader 缓存的过期时间(ms)
  preloadStaleTime: 60_000,             // 预加载缓存的过期时间(ms)

  // === 静态数据 ===
  staticData: {                          // 附加到路由的任意静态数据
    title: '示例页面',
    breadcrumb: '示例',
  },
})

虚拟路由(Virtual Routes)

.lazy.tsx 文件存在且主路由文件只剩下配置时,可以删除主路由文件——Vite 插件会生成虚拟路由:

// src/routes/posts/$postId.lazy.tsx
// 主文件 src/routes/posts/$postId.tsx 已删除
// Vite 插件自动生成包含 loader/params 的基本路由配置

import { createLazyFileRoute } from '@tanstack/react-router'

export const Route = createLazyFileRoute('/posts/$postId')({
  component: PostDetail,
})

💡 最佳实践:对于纯展示页面(不需要 loader、beforeLoad、params 解析),直接用虚拟路由可以减少文件数量。


条件路由 / 动态路由注册

代码路由模式支持运行时动态注册:

// 根据权限动态注册管理路由
const adminRoutes = user.hasPermission('admin')
  ? [
      createRoute({
        getParentRoute: () => authenticatedRoute,
        path: '/admin',
        component: AdminPanel,
      }),
    ]
  : []

const routeTree = rootRoute.addChildren([
  indexRoute,
  ...adminRoutes,
  publicRoute,
])

💡 最佳实践:大多数场景下文件路由已足够。仅当需要运行时条件路由微前端场景时才使用代码路由动态注册。


常见路由模式

场景 文件路径 匹配 URL
首页 routes/index.tsx /
静态页 routes/about.tsx /about
带参数 routes/posts/$postId.tsx /posts/123
可选参数 routes/docs/$section.tsx + 搭配 validateSearch /docs/intro
嵌套资源 routes/posts/$postId/comments/$commentId.tsx /posts/5/comments/42
路径无关布局 routes/_auth.tsx 不改变 URL
Catch-all routes/$.tsx 任意未匹配路径

🚨 陷阱:TanStack Router 不支持可选路径参数(如 /:param?)。需要可选参数时,用搜索参数(search params)代替。