Skip to content
嵌套路由与布局

嵌套路由与布局

嵌套路由是 TanStack Router 最强大的特性之一。通过 <Outlet> 和路径无关布局,你可以构建复杂的 UI 层级结构,同时保持 URL 清晰。


Outlet — 子路由渲染出口

<Outlet> 是子路由的渲染位置。如果没有 <Outlet>,子路由组件不会被渲染:

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

export const Route = createRootRoute({
  component: () => (
    <div className="app-layout">
      <Header />
      <Sidebar />
      <main>
        <Outlet />  {/* 所有子路由都在这里渲染 */}
      </main>
      <Footer />
    </div>
  ),
})

路由树与 <Outlet> 的对应关系:

__root__ (Header + Sidebar + <Outlet> + Footer)
├── / (渲染在 root 的 <Outlet> 中)
├── /about (渲染在 root 的 <Outlet> 中)
├── /posts (有布局 + 自己的 <Outlet>)
│   ├── / (渲染在 posts 的 <Outlet> 中)
│   └── /$postId (渲染在 posts 的 <Outlet> 中)
└── /settings (渲染在 root 的 <Outlet> 中)

布局路由(Layout Route)

在任意目录下创建同名文件即为该目录的布局路由:

// src/routes/posts.tsx
// 这是 /posts/* 所有子路由的布局

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

export const Route = createFileRoute('/posts')({
  component: () => (
    <div className="posts-layout">
      <aside className="posts-sidebar">
        <RecentPosts />
      </aside>
      <section className="posts-content">
        <Outlet />  {/* /posts/ 和 /posts/$postId 渲染在此 */}
      </section>
    </div>
  ),
})
文件结构:
routes/
├── posts.tsx              ← 布局路由(带 Outlet)
├── posts/
│   ├── index.tsx          ← /posts
│   └── $postId.tsx        ← /posts/123

渲染结果:
/posts → posts.tsx → index.tsx 在 Outlet 中
/posts/123 → posts.tsx → $postId.tsx 在 Outlet 中

💡 最佳实践:布局路由非常适合共享 UI 结构(侧边栏、面包屑、Tab 导航)。只把会随子路由变化的 UI 放在子路由组件中,静态部分放在布局中。


路径无关布局(Pathless Layout)

_ 开头的路由是路径无关的——它们提供布局和逻辑分组,但不改变 URL

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

export const Route = createFileRoute('/_authenticated')({
  component: () => (
    <div className="authenticated-layout">
      <UserSidebar />
      <main>
        <Outlet />
      </main>
    </div>
  ),
})
文件结构                          URL 路径
routes/
├── __root.tsx
├── _authenticated.tsx            ← 仅在路由树中存在,不影响 URL
├── _authenticated/
│   ├── dashboard.tsx             → /dashboard(不是 /_authenticated/dashboard)
│   └── profile.tsx               → /profile
├── _public.tsx                   ← 另一个路径无关布局
├── _public/
│   ├── index.tsx                 → /
│   └── login.tsx                 → /login

多层嵌套路径无关布局

// routes/_authenticated.tsx        — 第一层:认证守卫
// routes/_authenticated/_admin.tsx — 第二层:管理员守卫
// routes/_authenticated/_admin/users.tsx → URL: /users

export const Route = createFileRoute('/_authenticated/_admin')({
  beforeLoad: ({ context }) => {
    if (!context.auth.hasRole('admin')) {
      throw redirect({ to: '/unauthorized' })
    }
  },
  component: () => (
    <div className="admin-layout">
      <AdminSidebar />
      <Outlet />
    </div>
  ),
})

🚨 陷阱:多层的路径无关布局在 URL 中都是不可见的。_authenticated/_admin/users.tsx 的 URL 是 /users。如果两个不同的路径无关布局下都有 users.tsx,会导致路由冲突。


布局中的 Loader 和 beforeLoad

布局路由可以有独立的 loader 和 beforeLoad:

// src/routes/_authenticated.tsx
export const Route = createFileRoute('/_authenticated')({
  // beforeLoad 先于所有子路由执行
  beforeLoad: ({ context }) => {
    if (!context.auth.isAuthenticated) {
      throw redirect({ to: '/login' })
    }
  },

  // loader 与子路由的 loader 并行执行
  loader: async ({ context }) => {
    const user = await fetchUser(context.auth.userId)
    return { user }
  },

  component: () => <Outlet />,
})

执行顺序:

导航到 /dashboard
  1. _authenticated.beforeLoad()     ← 串行先执行
  2. dashboard.beforeLoad()           ← 父完成后子执行
  3. _authenticated.loader()          ← 并行执行  }
  4. dashboard.loader()               ← 并行执行  } 同时发起
  5. 组件渲染(所有 loader 完成后)

🔬 深入原理beforeLoad串行的(父 → 子),因为子路由可能需要父路由的 context。而 loader并行的,因为它们之间没有依赖关系。这个设计确保了守卫的安全性,同时最大化了数据加载的性能。


访问父路由的数据

静态数据

// 父路由定义 staticData
export const Route = createFileRoute('/posts')({
  staticData: { layout: 'wide' },
})

// 子路由通过 useRouteContext 或直接从路由树获取

context 传递

// 根路由定义 context 类型
import { createRootRouteWithContext } from '@tanstack/react-router'

interface MyRouterContext {
  auth: AuthState
}

export const Route = createRootRouteWithContext<MyRouterContext>()({
  component: () => <Outlet />,
})

// App.tsx 中传递 context
<RouterProvider router={router} context={{ auth }} />

嵌套 Outlet 的完整示例

// __root.tsx
export const Route = createRootRoute({
  component: () => (
    <div>
      <GlobalHeader />
      <div className="flex">
        <Outlet /> {/* 第一层子路由 */}
      </div>
    </div>
  ),
})

// _authenticated.tsx
export const Route = createFileRoute('/_authenticated')({
  component: () => (
    <div className="flex">
      <AppSidebar />
      <div className="flex-1">
        <Breadcrumb />
        <Outlet /> {/* 第二层子路由 */}
      </div>
    </div>
  ),
})

// posts.tsx
export const Route = createFileRoute('/posts')({
  component: () => (
    <div>
      <PostsTabs />
      <Outlet /> {/* 第三层子路由 */}
    </div>
  ),
})

// 最终渲染 /posts/123:
// GlobalHeader
// ├── AppSidebar | Breadcrumb
// │   └── PostsTabs
// │       └── PostDetail (postId=123)

布局路由 vs 路径无关布局 vs 组件组合

方案 何时使用 特点
布局路由 (posts.tsx) 为特定路径段提供共享 UI 反映在 URL 中,带自己的 loader
路径无关布局 (_auth.tsx) 逻辑分组(认证/角色/主题) 不改变 URL,适合守卫和 context
普通组件组合 可复用的 UI 片段 不使用路由层级,纯组件嵌套

💡 最佳实践:如果一段 UI 总是和特定 URL 路径绑定——用布局路由。如果一段 UI 只是逻辑上的分组(如"所有需要登录的页面")——用路径无关布局。如果一段 UI 与路由无关——用普通组件。