嵌套路由与布局
嵌套路由是 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 与路由无关——用普通组件。