动态路由是现代 Web 应用最常见的需求之一:同一个路由模板匹配多个类似的 URL,同时提取出变化部分用于数据获取或逻辑处理。React Router v6 提供了简洁且类型安全的 API 来处理动态路由、路由参数和查询参数。
动态路由与路由参数
定义动态路由
在路由路径中使用 :参数名 声明一个动态段,冒号后面的标识符即为参数的名称。例如,一个用户详情页可能对应 /users/123、/users/456 等不同 URL。
// router/index.jsx
import { createBrowserRouter } from 'react-router-dom';
import UserDetail from './UserDetail';
const router = createBrowserRouter([
{
path: '/users/:userId',
element: <UserDetail />,
},
]);
上面的 :userId 会匹配 /users/ 之后的任意单一路径段,并将该段的值作为参数 userId 注入到组件中。
在组件中获取路由参数:useParams
React Router v6 使用 useParams Hook 读取当前路由的动态参数,返回一个键值对对象,键名为声明的参数名。
// UserDetail.jsx
import { useParams } from 'react-router-dom';
function UserDetail() {
const { userId } = useParams(); // userId 为 string 类型
console.log(userId); // 如果 URL 是 /users/42,这里输出 "42"
return <h1>用户详情:{userId}</h1>;
}
useParams 返回的都是字符串类型,因为 URL 本身就是文本。如果你需要将参数作为数字处理,记得进行类型转换。
可选参数与多段参数
React Router v6 原生不支持可选参数(如 /users/:id?),但可以通过定义多个路由路径或使用 * 通配符来变通实现。如果需要更复杂的模式匹配,可以考虑升级到未来版本或使用自定义匹配逻辑。
对于需要捕获多段路径的场景,可以使用通配符 ,它会匹配任意数量的路径段,并将这些段存储在 对应的键中。
{
path: '/docs/*',
element: <DocPage />,
}
function DocPage() {
const { '*': splat } = useParams(); // 如果 URL 是 /docs/a/b,则 splat = "a/b"
}
查询参数(Query Parameters)
查询参数是 URL 问号(?)后面的键值对部分,例如 /search?keyword=react&page=2。React Router v6 提供了 useSearchParams Hook 来读写查询参数,其用法类似于 useState,但数据会同步到浏览器地址栏的查询字符串中。
读取查询参数
import { useSearchParams } from 'react-router-dom';
function SearchPage() {
const [searchParams] = useSearchParams();
const keyword = searchParams.get('keyword') || '';
const page = Number(searchParams.get('page')) || 1;
return (
<div>
<p>搜索关键词:{keyword}</p>
<p>当前页码:{page}</p>
</div>
);
}
searchParams 是一个 URLSearchParams 的实例,除了 get,还可以使用 getAll(获取数组参数)、has、forEach 等方法。
修改查询参数
useSearchParams 返回的第二个元素是更新函数,调用它会重新设置查询字符串并触发导航。你可以传入一个新的 URLSearchParams 对象、一个一般的对象(React Router 会自动序列化),或者一个回调函数来基于当前参数进行修改。
function SearchPage() {
const [searchParams, setSearchParams] = useSearchParams();
const updateKeyword = (newKeyword) => {
setSearchParams({ keyword: newKeyword, page: String(1) }); // 重置页码
};
const goToPage = (pageNum) => {
const params = new URLSearchParams(searchParams);
params.set('page', String(pageNum));
setSearchParams(params);
};
}
需要注意,setSearchParams 的默认行为会触发路由导航(可以理解为一个 useNavigate 的便捷封装),因此它会更新浏览器历史记录栈,并且组件会重新渲染。如果你不希望增加历史记录条目,可以传入 { replace: true } 选项。
路由参数 vs 查询参数
两者都用于在 URL 中传递数据,但用途不同,选择时可以参考以下原则:
| 类型 | 用途 | 示例 |
|------|------|------|
| 路由参数 | 指定资源标识,通常必填,是资源路径的一部分 | /users/zhangsan |
| 查询参数 | 提供非必需的过滤、排序、分页等辅助信息 | /users?role=admin&page=2 |
一个具体的业务场景:用户列表页需要分页和筛选,那么可以设计成:
路径:/users
查询参数:?page=1&keyword=react&status=active
但如果是一个编辑页,则需要定位到具体的实体,此时路由参数更合适:
路径:/users/:id/edit
动态路由与加载器结合的实际模式
在 React Router v6.4+ 的数据路由中,我们经常需要在加载数据时读取路由参数或查询参数。loader 函数可以接收一个包含 params 和 request 的对象,让你能够在渲染组件之前就拿到这些值。
{
path: '/users/:userId',
loader: async ({ params }) => {
const user = await fetchUser(params.userId);
return { user };
},
element: <UserDetail />,
}
function UserDetail() {
const { user } = useLoaderData();
// 无需手动 useParams,数据已在加载器中根据参数获取
}
类似地,在 loader 里也可以利用 request.url 解析查询参数:
{
path: '/users',
loader: async ({ request }) => {
const url = new URL(request.url);
const page = url.searchParams.get('page') || 1;
const data = await fetchUsers(page);
return data;
},
}
这种方式让数据获取与组件渲染解耦,并且数据在加载阶段就已经准备就绪,避免了先渲染再请求的“闪烁”问题。
常见陷阱与注意事项
- 参数类型:
useParams返回的值都是字符串,请务必在使用时转换成需要的类型(如Number)。 - 查询参数的竞态:如果连续快速调用
setSearchParams,React Router 会对它们进行合并,但这可能导致组件不必要的多次渲染。使用函数式更新或批量修改可以减少次数。 - 参数变化时的副作用:当路由参数改变时(如从
/users/1跳转到/users/2),组件默认会被卸载并重新挂载(如果使用了相同的组件但 key 不同),或者复用。如果你想在参数变化时重新获取数据,可以用useEffect监听参数变化,但更推荐使用loader机制,它会自动处理参数变化后的数据重新请求。 - URL 安全与编码:查询参数中的特殊字符需要编码,
URLSearchParams和setSearchParams会自动处理,但你直接拼接字符串时需留意。
掌握动态路由和查询参数的用法,你就能构建出符合 RESTful 风格的、深度可链接的 React 应用,让用户可以通过 URL 直接访问特定的界面状态。