
一句话:嵌套路由让你把「布局」抽取到父路由组件里(用
<Outlet />标记子路由的渲染位置),从而实现「导航栏不动、只切换内容区」;index路由表示「父路径本身的默认页面」。
一、为什么需要嵌套路由
// ❌ 每个页面都重复写一遍布局 → 大量重复代码
function Home() {
return (
<div>
<TopNav />
<Sidebar />
<main>首页内容</main>
</div>
);
}
function Settings() {
return (
<div>
<TopNav /> {/* 重复 */}
<Sidebar /> {/* 重复 */}
<main>设置内容</main>
</div>
);
}
// ✅ 用嵌套路由:布局只写一次
const router = [
{
path: '/',
element: <AppLayout />, {/* ⭐ 布局组件 */}
children: [
{ index: true, element: <Home /> }, // 渲染在 <Outlet /> 处
{ path: 'settings', element: <Settings /> },
{ path: 'users/:id', element: <UserDetail /> },
],
},
];
function AppLayout() {
return (
<div>
<TopNav />
<Sidebar />
<main>
<Outlet /> {/* ⭐ 子路由渲染在这里 */}
</main>
</div>
);
}
关键收益:
① 布局复用(导航栏、侧栏、页脚只写一次)
② 导航栏「不重新挂载」——切换页面时它保持稳定(不闪、不重置状态)
③ 可以「逐层叠加」布局(App 布局 → 模块布局 → 页面内容)
④ 路由级错误边界、懒加载可以「分层配置」
二、Outlet 的渲染机制
// React Router 的渲染链
<Route path="/" element={<AppLayout />}>
<Route path="dashboard" element={<DashboardLayout />}>
<Route path="settings" element={<Settings />} />
</Route>
</Route>
访问 /dashboard/settings 时的渲染结构:
<AppLayout> ← 第一层
<TopNav />
<Outlet> ← 这里是 DashboardLayout
<DashboardLayout> ← 第二层
<Sidebar />
<Outlet> ← 这里是 Settings
<Settings /> ← 第三层(叶子)
</Outlet>
</DashboardLayout>
</Outlet>
</AppLayout>
⚠️ 每一层的「布局组件」都会渲染,同时在它的 <Outlet /> 处渲染下一层。
因此「上层组件不会因路由切换而重新挂载」。
// 对比:不用嵌套路由时(所有页面平铺)
<Routes>
<Route path="/" element={<Home />} />
<Route path="/settings" element={<Settings />} />
</Routes>
// → 从 / 切到 /settings 时,Home 卸载、Settings 挂载,
// 如果两边都渲染 <TopNav />,导航栏会被「卸载重建」(状态丢失、可能有闪烁)
// ✅ 用嵌套路由后,从 / 切到 /settings 时,AppLayout 保持挂载
// → 只有 <Outlet /> 里的内容被替换 → 导航栏状态保留
三、index 路由
{
path: '/dashboard',
element: <DashboardLayout />,
children: [
{ index: true, element: <Overview /> }, // ⭐ 匹配 /dashboard
{ path: 'settings', element: <Settings /> }, // 匹配 /dashboard/settings
],
}
index 路由的含义:
「父路径本身的默认子路由」(相当于 v5 的 `<Route exact path="/dashboard">`)
⚠️ 三个要点:
① index 路由【不能有 children】(它就是叶子)
② index 路由【不能有 path】(它的路径就是父路径)
③ 一个父路由只能有【一个】index 子路由
// ✅ 常见的「重定向到默认子页」写法
{
path: '/dashboard',
element: <DashboardLayout />,
children: [
{ index: true, element: <Navigate to="overview" replace /> }, // ⭐ 重定向
{ path: 'overview', element: <Overview /> },
{ path: 'settings', element: <Settings /> },
],
}
// 访问 /dashboard → 自动跳到 /dashboard/overview
四、多级嵌套
const router = createBrowserRouter([
{
path: '/',
element: <RootLayout />, // 全局:顶部导航 + 页脚
errorElement: <RootError />,
children: [
{ index: true, element: <Home /> },
{
path: 'admin', // 一级:管理模块
element: <AdminLayout />, // 管理侧栏
errorElement: <AdminError />,
children: [
{ index: true, element: <Navigate to="users" replace /> },
{ path: 'users', element: <UserManage /> },
{
path: 'users/:id', // 二级
element: <UserDetailLayout />, // 详情内的子标签页
children: [
{ index: true, element: <UserInfo /> },
{ path: 'orders', element: <UserOrders /> },
{ path: 'logs', element: <UserLogs /> },
],
},
],
},
],
},
]);
访问 /admin/users/123/orders 的渲染链:
<RootLayout>
<AdminLayout>
<UserDetailLayout>
<UserOrders /> ← 叶子
多级嵌套的建议:
① 一般 2~3 层足够,太深会让「数据获取 + 错误处理」变复杂
② 每层的「布局职责」要清晰(全局 / 模块 / 页面内)
③ 错误边界要「分层」——模块级的错误不应该让整个应用崩掉
④ 路径层级与「UI 层级」保持一致(便于理解)
五、404 兜底
// ✅ 方式一:放在「最外层」的 children 里(推荐)
{
path: '/',
element: <RootLayout />,
children: [
{ index: true, element: <Home /> },
{ path: 'about', element: <About /> },
{ path: '*', element: <NotFound /> }, // ⭐ 放在最后
],
}
// 访问不存在的路径时:RootLayout 依然渲染(导航栏还在),只有内容区显示 404
// ⚠️ 方式二:放在最外层(不推荐)
{
path: '*',
element: <NotFound />, // ⚠️ 整页 404(没有导航栏)
}
// 好处:绝对兜底(包括「根路径拼错」的情况)
// 坏处:用户失去导航能力(连「返回首页」都要自己加)→ 体验差
// ✅ 最佳实践:两者都配
const router = createBrowserRouter([
{
path: '/',
element: <RootLayout />,
errorElement: <RootError />,
children: [
{ index: true, element: <Home /> },
{ path: 'about', element: <About /> },
{ path: '*', element: <NotFound /> }, // ⭐ 应用内 404(保留布局)
],
},
{ path: '*', element: <GlobalNotFound /> }, // ⭐ 绝对兜底(如 html 里的路径拼错)
]);
// 404 页面建议包含:
function NotFound() {
return (
<div className="not-found">
<h1>404</h1>
页面不存在或已被移除
<Link to="/">返回首页</Link> {/* ⭐ 给出路 */}
<button onClick={() => navigate(-1)}>返回上一页</button>
</div>
);
}
六、布局路由(无 path 的 Route)
// ✅ 「布局路由」——只提供布局与上下文,不占用路径段
{
path: '/',
element: <AppProviders>, // ⭐ 只包 Provider,不加路径段
children: [
{ index: true, element: <Home /> },
{ path: 'about', element: <About /> },
],
}
// 访问 / 与 /about 都会经过 AppProviders
// ⚠️ 前提:AppProviders 内部必须渲染 children 或 <Outlet />
function AppProviders() {
return (
<QueryClientProvider client={queryClient}>
<AuthProvider>
<Outlet /> {/* ⭐ 必须有 */}
</AuthProvider>
</QueryClientProvider>
);
}
布局路由的常见用途:
① 注入全局 Provider(QueryClient、主题、用户)
② 统一的「页面骨架」(顶部导航 + 页脚)
③ 统一的「权限校验」(<RequireAuth><Outlet /></RequireAuth>)
④ 统一的「滚动位置重置」(切页面回到顶部)
// ③ 权限校验的布局路由写法
function RequireAuth() {
const { user } = useAuth();
const location = useLocation();
if (!user) {
return <Navigate to="/login" state={{ from: location }} replace />;
}
return <Outlet />; {/* ⭐ 通过校验后渲染子路由 */}
}
const router = createBrowserRouter([
{ path: '/login', element: <Login /> },
{
path: '/',
element: <RequireAuth />, {/* ⭐ 所有子路由都需要登录 */}
children: [
{ index: true, element: <Dashboard /> },
{ path: 'settings', element: <Settings /> },
],
},
]);
七、Outlet 传上下文
// 父布局通过 Outlet 给子路由传值(避免重复请求)
function UserDetailLayout() {
const { id } = useParams<{ id: string }>();
const { data: user, isLoading } = useQuery({
queryKey: ['user', id],
queryFn: () => fetchUser(id!),
});
if (isLoading) return <Skeleton />;
return (
<div>
<UserHeader user={user} />
<TabNav />
<Outlet context={{ user }} /> {/* ⭐ 传上下文 */}
</div>
);
}
// 子路由读取
function UserOrders() {
const { user } = useOutletContext<{ user: User }>(); // ⭐ 直接用,不用再请求
return <div>{user.name} 的订单</div>;
}
八、常见坑
// ① 父布局组件忘记写 <Outlet />
function AppLayout() {
return <div><TopNav /></div>; // ❌ 子路由无处渲染(页面空白)
}
// ② index 路由写了 path
{ path: '/dashboard', children: [{ index: true, path: 'x', element: <X /> }] } // ❌
// ③ 一个父路由配了多个 index 子路由
children: [
{ index: true, element: <A /> },
{ index: true, element: <B /> }, // ❌ 只有第一个生效
]
// ④ 子路由的 path 写成了「绝对路径」
{ path: 'admin', children: [{ path: '/admin/users', element: <U /> }] } // ⚠️ 会拼错
// ✅ { path: 'users', element: <U /> }
// ⑤ 404 写成了「整页 404」(用户失去导航)
// ⑥ 嵌套过深(4 层以上)导致「数据获取与错误处理」复杂
// ⑦ 每层都请求同一份数据(应该用 Outlet context 或 Query 缓存)
// ⑧ 布局路由忘了渲染 Outlet
// ⑨ 认为「嵌套路由会自动传递数据」(需要 Outlet context 或 Query 缓存)
// ⑩ 在父布局里用 useParams 但该参数属于「子路由」(取不到)
// ⑪ 切换子路由时「滚动位置不重置」(应该在布局里处理)
// ⑫ 选项卡状态放在父路由(切换子路由时保留,可能不符合预期)
// ⑪ 的修复:在布局里监听 location 变化并重置滚动
function AppLayout() {
const { pathname } = useLocation();
useEffect(() => {
window.scrollTo(0, 0); // ⭐ 路由变化回到顶部
}, [pathname]);
return (
<div>
<TopNav />
<main><Outlet /></main>
</div>
);
}
// ⑩ 的说明:useParams 只返回「当前路由链」上的参数,所以父布局能拿到子路由的参数
// 其实是可以取到的(params 是「累积」的)——但「该不该取」是设计问题:
// ✅ 父布局只关心「自己这一层」的参数
// ✅ 子路由的参数由子路由自己读取
九、面试延伸
- 「嵌套路由是怎么工作的?」
用「布局组件 +<Outlet />」:父路由的element是布局组件(导航栏、侧栏等),其中用<Outlet />标记「子路由的渲染位置」。子路由的path是相对的(相对于父路由)。匹配到深层路径时,React Router 会逐层渲染:外层布局 → 中层布局 → … → 叶子组件,每层在自己的Outlet处渲染下一层。关键收益是「上层布局不会因路由切换而重新挂载」(导航栏状态保留、不闪烁)。 -
「
index路由是什么?」
index路由表示「父路径本身的默认子路由」(相当于 v5 的<Route exact path="/parent">)。三个要点:① 不能有path(路径就是父路径);② 不能有children(它是叶子);③ 一个父路由只能有一个index子路由。常见用法是「index路由 +<Navigate>」做「访问/dashboard自动跳到/dashboard/overview」的重定向。 -
「为什么嵌套路由能让导航栏『不重新挂载』?」
因为嵌套路由把「导航栏」放进了父路由的布局组件里,而父布局在子路由切换时不会被卸载——React Router 只是替换<Outlet />处渲染的内容。对比「不用嵌套路由」的写法(每个页面各自渲染一遍<TopNav />):从/切到/settings时,两个页面的组件树完全不同 → React 卸载一个、挂载另一个 → 导航栏被「卸载重建」(滚动位置、动画、输入状态全部重置,还可能有视觉闪烁)。 -
「404 应该配在哪里?」
最佳实践是配两处:① 应用内 404(放在「根布局的 children 末尾」,path="*")——这样 404 页面保留导航栏,用户还能用导航;② 绝对兜底(放在最外层路由数组的末尾)——处理「连根布局都匹配不到」的极端情况。只配「整页 404」的问题是用户失去导航能力(看到一个孤零零的 404 页面),体验较差。此外 404 页面应该给出「返回首页」「返回上一页」等出路。 -
「什么是布局路由(无 path 的 Route)?」
指「只有element、没有path的路由节点」——它不占用路径段,只用来「包一层」:渲染 Provider(QueryClient、主题、用户)、统一骨架、统一权限校验。关键前提是「它的element里必须渲染children或<Outlet />」,否则子路由无处渲染(页面空白)。它的典型用途是把「登录校验」做成一个布局路由:<RequireAuth><Outlet /></RequireAuth>,让所有子路由自动受保护。 -
「嵌套路由里的数据应该放在哪一层获取?」
原则是「放在真正需要它的那一层」——但要注意「父子都需要同一份数据」时的处理:① 用Outlet context——父路由请求一次,通过<Outlet context={{ data }} />传给子路由(子路由用useOutletContext读取);② 用查询库的缓存——父子都用同一个queryKey(TanStack Query 会自动去重、共享缓存,不会发两次请求)。避免的做法是「父组件用useEffect请求、子组件也请求一次」(重复请求 + 状态不一致)。 -
「嵌套路由有什么缺点?」
三点:① 过度嵌套会复杂化——4 层以上的嵌套会让「数据获取、错误处理、状态归属」都变难;② 生命周期不好控制——因为父布局不重新挂载,「切页面重置状态」需要显式处理(如滚动位置、Tab 选中态);③ 路径与 UI 强耦合——如果 UI 层级变化(如把侧栏从「布局」改成「可折叠的抽屉」),路由结构可能也要跟着调整。所以建议「2~3 层为宜、每层职责清晰(全局/模块/页面内)」。 -
「切换子路由时怎么重置滚动位置?」
在「布局组件」里监听pathname变化并重置:useEffect(() => { window.scrollTo(0, 0); }, [pathname])(用useLocation().pathname)。如果要「返回时恢复滚动位置」(如从详情页返回列表页),需要额外记录「每个路径的滚动位置」并在返回时恢复。也可以用ScrollRestoration组件(数据路由提供,会自动处理「前进/后退时恢复位置、普通导航时回到顶部」)。
一句话速记
嵌套路由 = 「父路由放布局 +
<Outlet />标记子路由渲染位置」,收益是「导航栏不重新挂载」;index路由表示「父路径本身的默认子页」(不能有path、不能有children、每层只能一个),常配<Navigate>做默认重定向;布局路由(无path)用于「统一包 Provider / 权限校验」,前提是内部必须渲染<Outlet />;404 配两处(应用内保留布局 + 最外层绝对兜底),并给出「返回首页/上一页」;子路由path用相对路径;数据放真正需要的那一层(父子共享用Outlet context或同一个queryKey);切换路由要显式重置滚动位置;嵌套 2~3 层为宜。



最新评论
读过书不知道欧·亨利的人少。教科书上选文有
这小生活不错呀
不错,必须顶一下!
看着你还在坚持,很好
看来忙了也没时间更新博客了
NIce。学习了。。。。
网站不错!!!!
简洁实用,好文章!