
一句话:用「
:param」在路径里声明动态段,用useParams()读取;参数变化时组件不会重新挂载(需要在useEffect里依赖参数,或给 element 加key强制重建)。
一、声明与读取
// ① 声明动态段(冒号开头)
<Route path="/users/:id" element={<UserDetail />} />
<Route path="/posts/:year/:month/:slug" element={<Post />} />
// ② 读取参数
import { useParams } from 'react-router-dom';
function UserDetail() {
const { id } = useParams<{ id: string }>(); // ⭐ 泛型给出类型
return <div>用户 ID:{id}</div>;
}
function Post() {
const { year, month, slug } = useParams<{ year: string; month: string; slug: string }>();
return <div>{year} 年 {month} 月:{slug}</div>;
}
⚠️ 参数【永远是字符串】:
/users/123 → params.id === '123'(string,不是 number)
需要数字时要自己转换:const id = Number(params.id)
⚠️ 路径里不允许「斜杠」:
/users/123/456 不会匹配 path="/users/:id"
需要匹配「多段」时用通配符(见下文)
二、多个参数与嵌套参数
// ✅ 多参数
<Route path="/:category/:id" element={<Product />} />;
function Product() {
const { category, id } = useParams();
return <div>{category} / {id}</div>;
}
// ✅ 嵌套路由里的参数是「累积」的(子路由能拿到父路由的参数)
const router = createBrowserRouter([
{
path: 'users/:userId',
element: <UserLayout />,
children: [
{ path: 'orders/:orderId', element: <OrderDetail /> },
],
},
]);
// 访问 /users/1/orders/99
function OrderDetail() {
const { userId, orderId } = useParams(); // ⭐ 两个都能拿到
return <div>用户 {userId} 的订单 {orderId}</div>;
}
// ⚠️ 父布局也能拿到子路由的参数(因为 params 是「整条链」的),
// 但「该不该用」是设计问题:父布局应只关心自己这一层。
三、可选参数
// v6.5+ 支持「可选段」语法(问号后缀)
<Route path="/posts/:lang?/:slug" element={<Post />} />;
// 匹配 /posts/hello 与 /posts/zh/hello
// ⚠️ 注意歧义:/posts/:lang?/:slug 在 /posts/hello 时
// lang 会是 undefined,slug 是 'hello'
// ✅ 更常见的做法:用两个路由(更明确)
<Route path="/posts" element={<PostList />} />
<Route path="/posts/:slug" element={<Post />} />
// ✅ 或用「默认值」处理可选语义
function Post() {
const { lang = 'zh', slug } = useParams(); // 参数缺失时用默认值
return <div>{lang} / {slug}</div>;
}
四、通配符
// ① * 匹配「剩余所有段」(包括斜杠)
<Route path="/files/*" element={<FileBrowser />} />;
function FileBrowser() {
const params = useParams();
console.log(params['*']); // ⭐ 通配符匹配到的部分(如 'a/b/c.txt')
return <div>{params['*']}</div>;
}
// 访问 /files/a/b/c.txt → params['*'] === 'a/b/c.txt'
// ② 路径末尾的 * 常用于「404 兜底」或「嵌套子应用的基路径」
<Route path="/legacy/*" element={<LegacyApp />} />;
// ③ 也可以用在「中间」
<Route path="/docs/*/api" element={<ApiDocs />} />;
// ✅ 用通配符 + 自己解析,做「文件浏览器」或「博客的路由」
function FileBrowser() {
const params = useParams();
const segments = (params['*'] ?? '').split('/').filter(Boolean);
// segments: ['a', 'b', 'c.txt']
return <Breadcrumb segments={segments} />;
}
五、参数变化的处理(重点)
// ⚠️ 从 /users/1 到 /users/2:组件【不会重新挂载】
// (因为 element 类型相同,React 复用了组件实例)
<Route path="/users/:id" element={<UserDetail />} />;
// ❌ 这样写:切换用户时数据不更新
function UserDetailBad() {
const { id } = useParams();
const [user, setUser] = useState(null);
useEffect(() => {
fetchUser(id!).then(setUser);
}, []); // ⚠️ 缺 id 依赖 → 只在挂载时请求一次
return <div>{user?.name}</div>;
}
// ✅ 修复方式一:依赖参数(重新请求)
function UserDetailGood() {
const { id } = useParams();
useEffect(() => {
let cancelled = false;
fetchUser(id!).then((data) => { if (!cancelled) setUser(data); });
return () => { cancelled = true; };
}, [id]); // ⭐ 依赖 id
return <div>{user?.name}</div>;
}
// ✅ 修复方式二:用查询库(自动按 key 重新请求)
function UserDetailBetter() {
const { id } = useParams();
const { data, isLoading } = useQuery({
queryKey: ['user', id], // ⭐ id 变化 → 自动重新请求
queryFn: () => fetchUser(id!),
});
return isLoading ? <Skeleton /> : <div>{data?.name}</div>;
}
// ✅ 修复方式三:需要「重置内部状态」时,用 key 强制重建
function UserDetailRoute() {
const { id } = useParams<{ id: string }>();
return <UserDetailInner key={id} />; // ⭐ id 变化 → 组件重建 → 内部状态全部重置
}
function UserDetailInner() {
const { id } = useParams();
const [tab, setTab] = useState('info'); // 切换用户时会重置为 'info'
// ...
}
| 需求 | 做法 |
|---|---|
| 切换参数时重新请求数据 | useEffect 依赖参数,或用查询库(queryKey 含参数) |
| 切换参数时重置内部状态 | 给 element/组件加 key(强制卸载重建) |
| 既要「保留部分状态」又要「重置部分状态」 | 把「需要重置的状态」下移到「带 key 的子组件」里 |
六、参数校验与 404
// ❌ 常见问题:参数对应的资源不存在时,页面显示空白或崩溃
function UserDetail() {
const { id } = useParams();
const { data } = useQuery({ queryKey: ['user', id], queryFn: () => fetchUser(id!) });
return <div>{data.name}</div>; // ❌ data 为 undefined 时崩溃
}
// ✅ 方式一:组件内处理(声明式)
function UserDetailGood() {
const { id } = useParams();
const { data, isLoading, error } = useQuery({
queryKey: ['user', id],
queryFn: () => fetchUser(id!),
});
if (isLoading) return <Skeleton />;
if (error || !data) return <NotFound />; // ⭐ 单页是 404(保留布局)
return <div>{data.name}</div>;
}
// ✅ 方式二:在 loader 里抛 404(数据路由,推荐)
{
path: 'users/:id',
element: <UserDetail />,
loader: async ({ params }) => {
const user = await fetchUser(params.id!);
if (!user) {
throw new Response('Not Found', { status: 404 }); // ⭐ 交给 errorElement
}
return user;
},
errorElement: <UserError />,
}
function UserError() {
const error = useRouteError();
if (isRouteErrorResponse(error) && error.status === 404) {
return <NotFound />;
}
return <div>出错了</div>;
}
// ✅ 方式三:参数格式校验(如 id 必须是数字)
function UserDetailValidated() {
const { id } = useParams();
const numId = Number(id);
if (!Number.isInteger(numId) || numId <= 0) {
return <NotFound />; // ⭐ 格式非法 → 404(而不是发无效请求)
}
// ...
}
七、匹配规则细节
① 静态段优先于动态段
/users/me 与 /users/:id 同时存在时,/users/me 命中静态段
② 更具体的路径优先
/users/:id/posts 优先于 /users/:id/*
③ 大小写:默认【敏感】(可通过 caseSensitive 配置)
<Route path="/About" caseSensitive /> → /about 不匹配
④ 末尾斜杠:/users 与 /users/ 视为相同
⑤ 通配符 * 贪心匹配「剩余所有段」
八、常见坑
// ① 参数是字符串,直接当数字用
const id = params.id; // ⚠️ '123' 不是 123
if (id > 100) { } // ⚠️ 字符串比较会出错
const numId = Number(params.id); // ✅
// ② 依赖数组漏掉参数(切换时不重新请求)
useEffect(() => { fetchUser(id); }, []); // ❌
// ③ 期望「参数变化时组件重新挂载」
// → 不会,需要 key
// ④ 用「路径里带斜杠的参数」
// /users/a/b 不会匹配 :id(一个段)
// ✅ 用通配符或拆成两段
// ⑤ 参数没有做「URL 编码/解码」
navigate(`/search/${keyword}`); // ⚠️ keyword 含 / 或 ? 会破坏路径
navigate(`/search/${encodeURIComponent(keyword)}`); // ✅
// 读取时也不用手动解码(React Router 已解码)
// ⑥ 多级参数名重复(父子路由用了同名 :id)
// ⑦ 在「父布局」读取子路由的参数(虽然能取到,但耦合了层级)
// ⑧ 一个参数用「中文/特殊字符」但没编码(URL 不合法)
// ⑨ 认为「params 是响应式的」(它随渲染返回新对象,可以正常用)
// ⑩ 在 loader 里没做「参数校验」就请求(无效请求)
// ⑪ 参数变化时「没有取消上一个请求」(竞态,见 7.2)
// ⑫ 用 useParams 在「非路由组件」里(拿不到)
// ⑥ 的说明:多级嵌套时参数名最好不同
{
path: 'users/:userId',
children: [{ path: 'orders/:orderId', element: <Order /> }],
}
// ✅ 用 userId / orderId 而非都用 id(避免覆盖歧义)
// ⑪ 的修复:用 AbortController 取消上一个请求
useEffect(() => {
const controller = new AbortController();
fetchUser(id!, { signal: controller.signal })
.then(setUser)
.catch((e) => { if (e.name !== 'AbortError') setError(e); });
return () => controller.abort(); // ⭐ 参数变化时取消旧请求
}, [id]);
九、面试延伸
- 「动态路由怎么用?」
在路径里用「冒号开头的段」声明参数(path="/users/:id"),在组件里用useParams()读取(返回「参数名 → 值」的对象)。注意三点:① 参数永远是字符串(要数字就自己Number());② 参数不能包含斜杠(一个:id只匹配一个路径段,需要多段用通配符*);③ 嵌套路由里的参数是「累积」的(子路由能拿到父路由的参数)。 -
「参数变化时组件会重新挂载吗?」
不会。从/users/1到/users/2,React Router 渲染的是同一个路由元素(element={<UserDetail />}),React 看到「元素类型相同」就复用组件实例——所以内部state、滚动位置、ref都不会重置。这带来两种处理方式:① 数据层面——useEffect依赖参数(或用查询库把参数放进queryKey);② 状态层面——如果希望「换一个参数 = 换一个实例」,给 element 加key(key={id})强制重建。 -
「
useParams返回的对象是响应式的吗?」
是。每次路由参数变化导致组件重新渲染时,useParams()都会返回新的对象(包含最新参数)——所以「在渲染中使用params.id」是安全且响应式的。唯一要注意的是「params对象本身每次都是新的引用」,所以不要在「依赖数组」里直接放params(会永远认为变了),而是依赖具体字段(params.id)。 -
「如何匹配多段路径?」
用通配符*:path="/files/*"会匹配/files/a/b/c,匹配到的剩余部分可以从useParams()['*']中读取(值为'a/b/c')。常见用途:① 404 兜底(path="*");② 嵌套子应用/旧系统迁移(把某个前缀下的所有路径交给一个组件处理);③ 文件浏览器(自己按/拆分params['*'])。注意通配符是贪心的(匹配尽可能多)。 -
「怎么处理『参数对应的资源不存在』?」
三种方式:① 组件内处理——请求回来后判断!data就渲染<NotFound />(保留布局);② loader 里抛 404(数据路由,推荐)——if (!user) throw new Response('Not Found', { status: 404 }),由最近的errorElement处理(用useRouteError+isRouteErrorResponse读状态);③ 参数格式校验——如果id必须是数字,就在请求前校验(Number.isInteger),不合法直接返回 404(避免发无效请求)。 -
「
/users/me和/users/:id同时存在时会匹配哪个?」
匹配静态段(/users/me)——React Router 的匹配优先级是「更具体/更静态的路径优先」:静态段 > 动态段 > 通配符。因此/users/me会命中静态路由而不是把me当作id。这也是「个人中心」这类页面的常见写法(避免id='me'这种奇怪的参数)。 -
「动态参数需要 URL 编码吗?」
需要——构造 URL 时要编码(encodeURIComponent(keyword)),否则参数里的/、?、#、空格等字符会破坏路径结构(如/search/a/b会被解析成两段)。读取时不用手动解码(React Router 会解码)。此外「路径段」与「查询参数」的编码规则略有差异:路径段用encodeURIComponent,查询参数可以用URLSearchParams(它会自动编码)。 -
「参数变化时怎么避免请求竞态?」
用AbortController:在useEffect里创建 controller、把它传给fetch({ signal }),并在清理函数里controller.abort()。这样「参数变化导致 effect 重新执行」时,上一个请求会被取消,从而避免「旧请求后返回、覆盖新数据」的竞态问题。如果没有取消能力(如第三方 SDK),可以用「忽略标记」(let cancelled = false; ... return () => { cancelled = true; })来忽略过期响应。详见 7.2。
一句话速记
动态段用
:param,读取用useParams()(参数永远是字符串、不能含斜杠,多段用*通配并从params['*']读取);嵌套路由的参数是累积的;参数变化不会重新挂载组件(数据用useEffect依赖参数或查询库queryKey;需要重置状态就加key);静态段优先于动态段;资源不存在时「组件内渲染 404」或「loader 里throw new Response(..., {status:404})交给errorElement」,并在请求前做参数格式校验;构造 URL 时要encodeURIComponent(读取时自动解码);参数变化时用AbortController避免竞态。



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