React Router 路由实战总结:跳转方式与参数接收
React Router 是 React 官方生态中最主流的路由库(当前主流版本为 v6/v7,本文基于 v6+ 的 Hooks API)。核心理念:路由即组件,路径变化驱动 UI 变化。本文总结路由的几种跳转方式,以及
useParams/useLocation/useSearchParams三种接收参数的方法。
目录
- 安装与基础配置
- 路由跳转的几种方式
- useNavigate 编程式跳转详解
- useParams 接收路径参数
- useSearchParams 接收查询参数
- useLocation 接收 state 参数
- 三种传参方式对比与选型
- 注意事项与常见坑
1. 安装与基础配置
npm install react-router-dom
# 或
pnpm add react-router-dom注意:v6 中 Web 端用的是
react-router-dom包(它内部依赖react-router),所有 Hooks 都从react-router-dom导入。
1.1 声明路由表
// main.tsx
import { createBrowserRouter, RouterProvider } from "react-router-dom";
import Home from "./pages/Home";
import UserDetail from "./pages/UserDetail";
import Search from "./pages/Search";
import NotFound from "./pages/NotFound";
const router = createBrowserRouter([
{ path: "/", element: <Home /> },
// :id 是动态路径参数
{ path: "/user/:id", element: <UserDetail /> },
{ path: "/search", element: <Search /> },
// 兜底 404
{ path: "*", element: <NotFound /> },
]);
ReactDOM.createRoot(document.getElementById("root")!).render(
<RouterProvider router={router} />,
);1.2 嵌套路由(Outlet)
// 父路由布局组件:子路由内容渲染在 <Outlet /> 位置
import { Outlet, Link } from "react-router-dom";
function Layout() {
return (
<div>
<nav>
<Link to="/">首页</Link>
<Link to="/user/1">用户</Link>
</nav>
<main>
<Outlet /> {/* 子路由渲染出口 */}
</main>
</div>
);
}
// 路由配置
const router = createBrowserRouter([
{
path: "/",
element: <Layout />,
children: [
{ index: true, element: <Home /> }, // index:父路径的默认子路由
{ path: "user/:id", element: <UserDetail /> },
{ path: "search", element: <Search /> },
],
},
]);旧式写法
BrowserRouter+Routes+Route仍然可用,功能等价,createBrowserRouter额外支持 loader/action 数据 API。
2. 路由跳转的几种方式
React Router 中的跳转方式可以归纳为 四种:
| 方式 | 适用场景 | 是否可传参 |
|---|---|---|
<Link> 声明式链接 | 页面内普通导航(菜单、卡片、文字链) | ✅ |
<NavLink> 带激活态链接 | 导航栏,需要高亮当前项 | ✅ |
useNavigate 编程式跳转 | 事件回调中跳转(登录成功、表单提交后) | ✅ |
<Navigate> 组件重定向 | 渲染期条件跳转(路由守卫、鉴权拦截) | ✅ |
2.1 <Link> 声明式跳转
最基础的跳转,渲染为 <a> 标签,点击不刷新页面。
import { Link } from "react-router-dom";
function Home() {
return (
<div>
{/* 基础跳转 */}
<Link to="/user/1">查看用户 1</Link>
{/* 带查询参数 */}
<Link to="/search?keyword=react&page=2">搜索结果</Link>
{/* 带 state 隐式传参 */}
<Link to="/user/1" state={{ from: "home", tab: "posts" }}>
用户详情(带 state)
</Link>
{/* 替换历史记录(不留下浏览记录) */}
<Link to="/login" replace>
去登录
</Link>
</div>
);
}2.2 <NavLink> 带激活态的导航链接
与 <Link> 用法相同,但会自动感知"当前是否处于该路由",常用于菜单高亮。
import { NavLink } from "react-router-dom";
function Menu() {
return (
<nav>
<NavLink
to="/"
// className 接收函数,isActive 表示是否激活
className={({ isActive }) =>
isActive ? "menu-item active" : "menu-item"
}
>
首页
</NavLink>
<NavLink
to="/search"
// style 也支持函数形式
style={({ isActive }) => ({
color: isActive ? "#1677ff" : "#333",
fontWeight: isActive ? "bold" : "normal",
})}
>
搜索
</NavLink>
</nav>
);
}2.3 useNavigate 编程式跳转(重点,下一节详解)
const navigate = useNavigate();
navigate("/user/1"); // 普通跳转
navigate(-1); // 后退2.4 <Navigate> 组件重定向
在渲染期间跳转,典型用途是路由守卫:
import { Navigate } from "react-router-dom";
function ProtectedPage() {
const isLoggedIn = checkLogin();
// 未登录:渲染时直接重定向到登录页,并记录来源
if (!isLoggedIn) {
return <Navigate to="/login" replace state={{ from: "/protected" }} />;
}
return <div>需要登录才能看到的内容</div>;
}3. useNavigate 编程式跳转详解
useNavigate 返回一个跳转函数,用于事件处理、异步回调中跳转页面——这是与 <Link> 的本质区别:<Link> 靠用户点击,navigate 靠代码触发。
import { useNavigate } from "react-router-dom";
function LoginPage() {
const navigate = useNavigate();
const handleLogin = async () => {
await loginApi();
// 登录成功后跳转,replace 防止用户点"后退"又回到登录页
navigate("/", { replace: true });
};
return <button onClick={handleLogin}>登录</button>;
}3.1 基本用法
const navigate = useNavigate();
// 跳转到指定路径(会向 history 压入一条记录)
navigate("/user/1");
// 带查询参数
navigate("/search?keyword=react&page=2");
// 相对路径跳转(基于当前路由)
navigate("detail"); // /user → /user/detail
navigate("../list"); // 回到上一级
// 数字参数:前进 / 后退
navigate(-1); // 后退一步(相当于浏览器后退键)
navigate(-2); // 后退两步
navigate(1); // 前进一步3.2 第二个参数:跳转选项
navigate("/user/1", {
// ✅ state:隐式传参,不出现在 URL 中
state: { userName: "张三", from: "list" },
// ✅ replace:替换当前历史记录,而不是新增(跳转后无法"后退"回来)
replace: true,
});3.3 完整示例:登录跳转并回跳来源页
function LoginPage() {
const navigate = useNavigate();
const location = useLocation();
const handleLogin = async () => {
await loginApi();
// 从守卫传过来的 state 里取来源路径,回跳;没有则回首页
const from = location.state?.from ?? "/";
navigate(from, { replace: true });
};
return <button onClick={handleLogin}>登录</button>;
}⚠️
useNavigate必须在路由上下文中调用(组件必须被<RouterProvider>/<BrowserRouter>包裹),在普通工具函数里无法使用。组件外想跳转需通过 router 实例(router.navigate())。
4. useParams 接收路径参数
用于接收 URL 路径中的动态段,即路由配置里 :xxx 声明的部分。
4.1 定义与接收
// 路由配置:定义动态段 :id
{ path: "/user/:id", element: <UserDetail /> }// 访问 /user/42 时
import { useParams } from "react-router-dom";
function UserDetail() {
// params 是一个对象,key 对应路由中的 :id
const { id } = useParams();
return <h1>用户 ID:{id}</h1>; // 输出:用户 ID:42
}4.2 多个参数
// 路由配置
{ path: "/user/:userId/post/:postId", element: <PostDetail /> }// 访问 /user/7/post/99 时
function PostDetail() {
const { userId, postId } = useParams();
// userId = "7",postId = "99"
return (
<p>
用户 {userId} 的文章 {postId}
</p>
);
}4.3 注意点
const { id } = useParams();
// ⚠️ 1. 参数值永远是 string 类型
console.log(typeof id); // "string"
const numId = Number(id); // 需要数字时手动转换
// ⚠️ 2. TypeScript 中建议标注类型(key 可能不存在,值为 string | undefined)
const { id } = useParams<{ id: string }>();
// ⚠️ 3. 参数变化会触发组件重渲染,可用 useEffect 监听
useEffect(() => {
fetchUser(id); // 从 /user/1 切到 /user/2 时重新请求
}, [id]);特点总结:
- 参数体现在 URL 路径中:
/user/42 - 刷新页面、分享链接、收藏书签后参数依然有效
- 适合传标识性、必需的参数(资源 ID、分类名)
5. useSearchParams 接收查询参数
用于接收 URL 中 ? 后面的查询字符串(query string),用法类似 useState。
5.1 基本读取
// 访问 /search?keyword=react&page=2 时
import { useSearchParams } from "react-router-dom";
function SearchPage() {
// searchParams 是一个 URLSearchParams 实例
const [searchParams] = useSearchParams();
const keyword = searchParams.get("keyword"); // "react"
const page = searchParams.get("page"); // "2"(⚠️ 字符串)
return (
<p>
搜索「{keyword}」第 {page} 页
</p>
);
}5.2 常用读取方法
const [searchParams] = useSearchParams();
searchParams.get("keyword"); // 取单个值,不存在返回 null
searchParams.getAll("tag"); // 取多个同名参数:?tag=a&tag=b → ["a", "b"]
searchParams.has("keyword"); // 是否存在 → true / false
searchParams.get("page") ?? "1"; // 提供默认值
Number(searchParams.get("page")); // 转数字5.3 修改查询参数(setSearchParams)
const [searchParams, setSearchParams] = useSearchParams();
// 整体替换查询参数(会触发路由变化,URL 同步更新)
setSearchParams({ keyword: "vue", page: "1" });
// URL 变为 /search?keyword=vue&page=1
// 在现有参数基础上修改(推荐写法)
setSearchParams((prev) => {
prev.set("page", "3"); // 只改 page,保留其他参数
return prev;
});
// 替换历史记录而不是新增
setSearchParams({ keyword: "vue" }, { replace: true });5.4 完整示例:搜索 + 分页
function SearchPage() {
const [searchParams, setSearchParams] = useSearchParams();
const keyword = searchParams.get("keyword") ?? "";
const page = Number(searchParams.get("page") ?? "1");
return (
<div>
<input
value={keyword}
onChange={(e) =>
// 输入变化同步到 URL,页码重置为 1
setSearchParams({ keyword: e.target.value, page: "1" })
}
/>
<button
onClick={() => setSearchParams({ keyword, page: String(page + 1) })}
>
下一页(当前第 {page} 页)
</button>
</div>
);
}特点总结:
- 参数体现在 URL 查询串中:
/search?keyword=react&page=2 - 刷新、分享、收藏后参数依然有效
- URL 即状态:参数变化驱动组件更新,天然支持前进/后退
- 适合传可选的、过滤类参数(搜索词、页码、筛选条件、排序方式)
6. useLocation 接收 state 参数
useLocation 返回当前路由的位置对象,既能拿 URL 信息,也能拿跳转时通过 state 传入的隐式参数。
6.1 location 对象结构
import { useLocation } from "react-router-dom";
function AnyPage() {
const location = useLocation();
console.log(location.pathname); // "/user/1" —— 路径
console.log(location.search); // "?tab=posts" —— 查询串(字符串)
console.log(location.hash); // "#section1" —— 锚点
console.log(location.state); // { from: "home" } —— 跳转时传的隐式参数
console.log(location.key); // "abc123" —— 该条历史记录的唯一 key
}6.2 发送方:通过 state 传参
// 方式一:Link
<Link to="/user/1" state={{ userName: "张三", role: "admin" }}>
查看用户
</Link>;
// 方式二:useNavigate
navigate("/user/1", { state: { userName: "张三", role: "admin" } });6.3 接收方:通过 location.state 取参
function UserDetail() {
const location = useLocation();
const state = location.state as { userName?: string; role?: string } | null;
return (
<div>
{/* ⚠️ state 可能为 null(比如用户直接输入 URL 访问),要做空值处理 */}
<p>用户名:{state?.userName ?? "未知"}</p>
<p>角色:{state?.role ?? "无"}</p>
</div>
);
}6.4 典型应用:详情页带列表数据预览
// 列表页:跳转时把整个 item 塞过去,详情页秒开不用等请求
function UserList({ users }: { users: User[] }) {
const navigate = useNavigate();
return (
<ul>
{users.map((u) => (
<li
key={u.id}
onClick={() => navigate(`/user/${u.id}`, { state: { preview: u } })}
>
{u.name}
</li>
))}
</ul>
);
}
// 详情页:先用 state 里的预览数据渲染,再用 id 拉取完整数据
function UserDetail() {
const { id } = useParams();
const { state } = useLocation();
const preview = (state as { preview?: User } | null)?.preview;
const [detail, setDetail] = useState<User | null>(preview ?? null);
useEffect(() => {
fetchUser(id).then(setDetail); // 后台补全数据
}, [id]);
if (!detail) return <p>加载中...</p>;
return <h1>{detail.name}</h1>;
}特点总结:
- 参数不出现在 URL 中,用户看不到、改不了
- 适合传敏感的、临时的、结构化的数据(对象、数组都能传,不用拼字符串)
- ⚠️ 刷新页面后 state 会保留(history API 会保存),但新开标签页 / 直接输入 URL 访问时为 null,所以 state 只能作为"增强",不能作为唯一数据来源
- 页面间不共享 URL 也能传参
7. 三种传参方式对比与选型
| 维度 | useParams 路径参数 | useSearchParams 查询参数 | useLocation state 参数 |
|---|---|---|---|
| 参数位置 | URL 路径:/user/42 | URL 查询串:?page=2 | 不在 URL 中,存于 history |
| 值类型 | 永远 string | 永远 string | 任意类型(对象、数组) |
| 刷新后有效 | ✅ | ✅ | ✅(但新开标签/直输 URL 失效) |
| 可分享/收藏 | ✅ | ✅ | ❌ |
| 用户可见可改 | ✅ | ✅ | ❌(相对隐蔽) |
| 适合场景 | 资源 ID、必需的标识 | 搜索词、分页、筛选、排序 | 敏感数据、临时状态、来源标记、预览数据 |
| 典型例子 | /user/:id /post/:slug | /list?page=2&sort=hot | 登录后回跳来源、防直访标记 |
选型口诀:
- 这个参数决定页面显示谁 → 用路径参数(useParams)
- 这个参数影响页面怎么显示(过滤、排序、翻页) → 用查询参数(useSearchParams)
- 这个参数不该出现在地址栏(敏感、临时、结构化) → 用 state(useLocation)
实际项目中经常组合使用:
// URL:/user/42?tab=posts,state 带预览数据
navigate(`/user/42?tab=posts`, { state: { preview: user } });
function UserDetail() {
const { id } = useParams(); // 42 → 拉取数据
const [searchParams] = useSearchParams(); // tab=posts → 控制 Tab 显示
const { state } = useLocation(); // preview → 秒开预览
}8. 注意事项与常见坑
8.1 参数都是字符串,记得转换
const { id } = useParams();
id + 1; // ❌ "42" + 1 = "421"
Number(id) + 1; // ✅ 438.2 state 可能为 null,务必做空值兜底
const { state } = useLocation();
console.log(state.from); // ❌ 直接访问 URL 进入时 state 为 null,直接报错
const from = state?.from ?? "/"; // ✅8.3 useSearchParams 的 set 是整体替换
// 当前 URL:?keyword=react&page=2
setSearchParams({ page: "3" });
// ❌ URL 变成 ?page=3 —— keyword 丢了!
// ✅ 基于旧值修改
setSearchParams((prev) => {
prev.set("page", "3");
return prev;
});8.4 监听参数变化要放进依赖数组
const { id } = useParams();
useEffect(() => {
fetchUser(id);
}, []); // ❌ 只在挂载时执行,/user/1 → /user/2 不会重新请求
useEffect(() => {
fetchUser(id);
}, [id]); // ✅8.5 Hooks 必须在路由上下文内使用
useNavigate / useParams / useLocation / useSearchParams 都依赖路由 Context,在未被 Router 包裹的组件或普通工具函数中调用会直接报错。组件外跳转请用 router 实例:
import { router } from "./router";
// 请求拦截器等组件外场景
router.navigate("/login");8.6 replace 的正确使用场景
- 登录成功跳转首页:
replace: true(防止后退回到登录页) - 重定向/守卫:
replace: true(历史里不留中间页) - 轮播、向导步骤:视产品需求,一般步骤页用
replace避免历史堆栈污染
8.7 v6 与 v5 的差异提醒
如果看的是旧教程,注意这些 API 已废弃:
| v5 写法 | v6 写法 |
|---|---|
useHistory() / history.push() | useNavigate() / navigate() |
useHistory() / history.replace() | navigate(path, { replace: true }) |
<Switch> | <Routes> |
<Route component={...}> | <Route element={...}> |
<Redirect> | <Navigate> |
withRouter 高阶组件 | 已移除,直接用 Hooks |
useRouteMatch | 用相对路径或 useMatch |
附:完整最小可运行示例
// main.tsx
import React from "react";
import ReactDOM from "react-dom/client";
import {
createBrowserRouter,
RouterProvider,
Link,
NavLink,
Outlet,
useNavigate,
useParams,
useLocation,
useSearchParams,
} from "react-router-dom";
// ---------- 布局 ----------
function Layout() {
return (
<div style={{ fontFamily: "sans-serif", padding: 24 }}>
<nav style={{ display: "flex", gap: 12, marginBottom: 16 }}>
<NavLink
to="/"
style={({ isActive }) => ({
fontWeight: isActive ? "bold" : "normal",
})}
>
首页
</NavLink>
<NavLink
to="/search"
style={({ isActive }) => ({
fontWeight: isActive ? "bold" : "normal",
})}
>
搜索
</NavLink>
</nav>
<Outlet />
</div>
);
}
// ---------- 首页:演示各种跳转 ----------
function Home() {
const navigate = useNavigate();
return (
<div>
<h2>首页</h2>
{/* 1. Link 跳转 */}
<p>
<Link to="/user/42">Link → /user/42</Link>
</p>
{/* 2. Link 带 state */}
<p>
<Link to="/user/42" state={{ from: "home" }}>
Link + state → /user/42
</Link>
</p>
{/* 3. navigate 跳转 */}
<button
onClick={() =>
navigate("/user/42?tab=posts", { state: { from: "home" } })
}
>
navigate → /user/42?tab=posts(带 state)
</button>
<button onClick={() => navigate(-1)} style={{ marginLeft: 8 }}>
后退
</button>
</div>
);
}
// ---------- 用户详情:演示三种接收参数 ----------
function UserDetail() {
const { id } = useParams(); // ① 路径参数
const [searchParams] = useSearchParams(); // ② 查询参数
const { state } = useLocation() as { state: { from?: string } | null }; // ③ state 参数
return (
<div>
<h2>用户详情</h2>
<p>useParams → id:{id}</p>
<p>useSearchParams → tab:{searchParams.get("tab") ?? "未指定"}</p>
<p>
useLocation → state.from:
{state?.from ?? "无(可能是直接输入 URL 进入)"}
</p>
</div>
);
}
// ---------- 搜索页:演示 useSearchParams 读写 ----------
function Search() {
const [searchParams, setSearchParams] = useSearchParams();
const keyword = searchParams.get("keyword") ?? "";
return (
<div>
<h2>搜索</h2>
<input
value={keyword}
placeholder="输入关键词"
onChange={(e) => setSearchParams({ keyword: e.target.value })}
/>
<p>当前 URL 查询串:{searchParams.toString() || "(空)"}</p>
</div>
);
}
// ---------- 路由表 ----------
const router = createBrowserRouter([
{
path: "/",
element: <Layout />,
children: [
{ index: true, element: <Home /> },
{ path: "user/:id", element: <UserDetail /> },
{ path: "search", element: <Search /> },
{ path: "*", element: <h2>404 页面不存在</h2> },
],
},
]);
ReactDOM.createRoot(document.getElementById("root")!).render(
<RouterProvider router={router} />,
);