React Router v6 相比 v5 是一次彻底的重写,路由配置从组件嵌套变成了声明式树形结构,嵌套路由靠 <Outlet> 占位,动态参数靠 useParams 读取。很多从 v5 迁移过来的开发者第一反应是不适应:Switch 没了,exact 没了,render 属性也没了。本文结合完整可运行的示例,把 v6 中最常用的嵌套路由与动态路由参数讲透,并梳理几个高频踩坑点。

v6 的路由配置基本写法
v6 用 <Routes> 替代了 v5 的 <Switch>,路由匹配算法换成了新的路径排名机制,最匹配的路径优先渲染,因此不再需要 exact。看一个最小可用的配置:
import { BrowserRouter, Routes, Route } from 'react-router-dom';
import Home from './pages/Home';
import About from './pages/About';
import NotFound from './pages/NotFound';
function App() {
return (
<BrowserRouter>
<Routes>
<Route path="/" element={<Home />} />
<Route path="/about" element={<About />} />
<Route path="*" element={<NotFound />} />
</Routes>
</BrowserRouter>
);
}
export default App;注意两处变化:一是渲染内容从 component/render 换成了 element,传入的是真实的 JSX 元素而非组件类,这样可以更自然地给页面组件传 props;二是 path="*" 作为兜底路由放在最后,匹配所有未命中的路径,相当于 404 页面。
另一个容易忽略的点是路由注册顺序。v5 依赖书写顺序加 exact 控制匹配,v6 则按路径的具体程度排名,/user/score 比 /user/:id 更具体,会优先匹配,所以子路由的书写顺序不再影响结果,这一点对复杂路由表非常友好。
嵌套路由与 Outlet 占位符
嵌套路由是 v6 变化最大的部分。v5 时代需要在子组件里再次写一堆 Route 并配合 match.path 拼路径,v6 则把层级关系直接体现在 Route 的标签嵌套上,父路由组件里放一个 <Outlet> 作为子路由的渲染出口:
import { Routes, Route, Outlet, Link } from 'react-router-dom';
function Layout() {
return (
<div>
<nav>
<Link to="/dashboard">仪表盘</Link> |{' '}
<Link to="/dashboard/settings">设置</Link>
</nav>
<Outlet /> {/* 子路由内容渲染在这里 */}
</div>
);
}
function Dashboard() {
return <h2>仪表盘首页</h2>;
}
function Settings() {
return <h2>设置页面</h2>;
}
function App() {
return (
<Routes>
<Route path="/dashboard" element={<Layout />}>
<Route index element={<Dashboard />} />
<Route path="settings" element={<Settings />} />
</Route>
</Routes>
);
}这里有个关键细节:index 属性表示索引路由。当访问 /dashboard 时父路径命中但没有子路径,索引路由会填充到 Outlet 的位置,相当于子路由的默认页。如果父路由用了嵌套写法却没有提供 index 路由,直接访问 /dashboard 时页面会是空白,这是新手最常见的疑惑之一。
子路由的 path 写相对路径即可,不需要重复写 /dashboard 前缀,v6 会自动拼出完整路径 /dashboard/settings。这种相对路径写法的好处是重构父级路径时只需改一处,子路由不用跟着改。如果确实想让父路由只承担布局职责而不占用 URL 段,可以去掉父级的 path 属性,只保留 element,这就是常见的路径less布局写法。
动态路由参数与 useParams
动态路由用冒号语法定义参数,比如 path="/user/:id",组件内部用 useParams 钩子读取。配合上面讲到的嵌套结构,来一个完整的例子:
import { useParams, useNavigate, Routes, Route } from 'react-router-dom';
import { useEffect, useState } from 'react';
function UserProfile() {
const { id } = useParams();
const navigate = useNavigate();
const [user, setUser] = useState(null);
useEffect(() => {
// id 变化时重新请求,比如 /user/1 切换到 /user/2
fetch(`https://api.ipipp.com/users/${id}`)
.then(res => res.json())
.then(setUser);
}, [id]);
if (!user) return <p>加载中...</p>;
return (
<div>
<h2>{user.name}</h2>
<button onClick={() => navigate('/user/list')}>返回列表</button>
</div>
);
}
function App() {
return (
<Routes>
<Route path="/user/:id" element={<UserProfile />} />
<Route path="/user/list" element={<p>用户列表</p>} />
</Routes>
);
}有两点必须强调。第一,useParams 返回的值永远是字符串,拿到数字 id 做运算前要 Number(id) 转换,否则容易出现字符串拼接的隐蔽 bug。第二,同一个组件在不同参数间切换时(例如从 /user/1 点到 /user/2),组件不会卸载重建,只是参数变了,所以请求逻辑必须放在 useEffect 里并把 id 加进依赖数组,否则页面数据不会更新。
可选参数方面,v6 去掉了 v5 的问号可选语法,如果想让某个参数可有可无,需要注册两条路由指向同一组件,例如同时写 path="/goods" 和 path="/goods/:id"。此外还可以用 useSearchParams 读取查询字符串参数,比如 /goods?sort=price 中通过 searchParams.get('sort') 拿到 price,它返回的对象用法与浏览器原生 URLSearchParams 一致。
除了 useParams,v6 还提供 useLocation 获取当前路径信息、useNavigate 做编程式跳转替代 v5 的 useHistory。useNavigate 返回的函数既接受字符串路径,也接受对象形式,例如 navigate(-1) 等价于后退一步,navigate('/login', { replace: true }) 则不会在历史记录中留下当前页,适合登录后跳转这类场景。
常见报错与排查思路
实际开发中遇到的几个高频问题值得单独说明。一是 useParams 拿到空对象,多半是当前组件并不在带参数的路由层级内,或者路由注册时把组件写成了 element={<UserProfile />} 之外的错误形式;hooks 只能在路由组件内部使用,在普通被引入组件里调用会取不到值。二是 刷新后 404,这通常是服务端没有配置回退,所有路径都需要返回 index.html,开发服务器加 historyApiFallback,Nginx 则用 try_files 指向入口文件。
三是嵌套路由页面空白,检查父路由是否忘了写 <Outlet>,或者父路由标签没有闭合成嵌套形式而是写成了自闭合。四是版本混用导致报错,v5 的 useHistory 在 v6 里已经删除,如果项目里同时存在两个版本依赖,会出现莫名其妙的 undefined,建议用 npm ls react-router-dom 确认依赖树干净统一。
掌握嵌套路由的 Outlet 机制和 useParams 的使用时机,就覆盖了 v6 日常开发八成以上的场景。剩下的动态路由懒加载可以用 React.lazy 配合 element 组合,等用到再做深入也不迟。
React Router v6嵌套路由动态路由参数修改时间:2026-09-16 08:42:57