路由(React Router v6)
React Router 是 React 事实标准的路由库。v6 带来了显著 API 变化:Routes 取代 Switch、element prop 替代 children 写法、Hooks 优先。
官方文档:React Router 安装:
npm install react-router-dom
基础概念
| 术语 | 说明 |
|---|---|
| BrowserRouter | 使用 HTML5 History API 的路由器 |
| Routes | 路由规则容器,匹配第一个相符的路由 |
| Route | 单条路由规则(路径 → 组件映射) |
| Outlet | 嵌套路由的子路由渲染出口 |
| Link / NavLink | 声明式导航链接 |
快速开始
import { BrowserRouter, Routes, Route, Link } from 'react-router-dom';
function App() {
return (
<BrowserRouter>
<nav>
<Link to="/">首页</Link>
<Link to="/about">关于</Link>
</nav>
<Routes>
<Route path="/" element={<Home />} />
<Route path="/about" element={<About />} />
<Route path="/user/:id" element={<UserProfile />} />
<Route path="*" element={<NotFound />} />
</Routes>
</BrowserRouter>
);
}⚠️ 与 v5 的关键区别:
<Route>的组件用elementprop(不是 children),<Routes>取代了<Switch>。
路由参数
路径参数(:param)
// 定义
<Route path="/user/:id" element={<UserProfile />} />
// 使用
import { useParams } from 'react-router-dom';
function UserProfile() {
const { id } = useParams();
}查询字符串
// /search?q=react&page=2
import { useSearchParams } from 'react-router-dom';
function SearchPage() {
const [searchParams, setSearchParams] = useSearchParams();
const query = searchParams.get('q');
const page = searchParams.get('page') || 1;
// 修改查询参数
setSearchParams({ q: 'vue', page: 1 });
}通配符(*)
<Route path="/files/*" element={<FileViewer />} />
// 在 FileViewer 中使用 useParams() 的 '*' 获取剩余路径
const { '*': restPath } = useParams(); // 如 "2024/report.pdf"编程式导航
import { useNavigate } from 'react-router-dom';
function LoginButton() {
const navigate = useNavigate();
const handleLogin = async () => {
await loginAPI();
navigate('/dashboard'); // 前进
navigate(-1); // 后退
navigate('/result', { state: { from: 'login' } }); // 携带 state
navigate('/target', { replace: true }); // 替换当前历史记录
};
}读取路由 State
import { useLocation } from 'react-router-dom';
function Result() {
const location = useLocation();
const { from } = location.state || {}; // navigate 时传递的 state
}🚨 陷阱:通过
state传递的数据在页面刷新后会丢失(它存在于浏览器内存的 history 中,不持久化)。需要持久化时用 query 参数或 localStorage。
嵌套路由
// 父组件(Layout)
function Layout() {
return (
<div>
<Header />
<Outlet /> {/* 子路由在此渲染 */}
<Footer />
</div>
);
}
// 路由配置
<Routes>
<Route path="/" element={<Layout />}>
<Route index element={<Home />} /> {/* index = path="/" */}
<Route path="products" element={<Products />}>
<Route path=":id" element={<ProductDetail />} />
</Route>
<Route path="about" element={<About />} />
</Route>
</Routes>| 渲染路径 | 组件 |
|---|---|
/ |
Layout → Home |
/products |
Layout → Products |
/products/123 |
Layout → Products → ProductDetail |
Link vs NavLink
<Link to="/" replace>首页</Link>
// NavLink 提供激活状态
<NavLink
to="/dashboard"
className={({ isActive, isPending }) =>
isActive ? 'active' : isPending ? 'pending' : ''
}
end // 精确匹配(/dashboard 不匹配 /dashboard/settings)
>
仪表盘
</NavLink>路由守卫
function ProtectedRoute({ children }) {
const [user] = useAuth(); // 自定义认证 hook
if (!user) {
return <Navigate to="/login" replace />;
}
return children;
}
// 使用
<Route path="/admin" element={
<ProtectedRoute>
<AdminPanel />
</ProtectedRoute>
} />💡 最佳实践:路由守卫作为布局路由(Layout Route)更简洁:
<Route element={<ProtectedRoute />}>,将其包裹的所有子路由自动受保护。
Lazy Loading(路由级别的代码分割)
import { lazy, Suspense } from 'react';
const Dashboard = lazy(() => import('./pages/Dashboard'));
const Settings = lazy(() => import('./pages/Settings'));
function App() {
return (
<Suspense fallback={<Loading />}>
<Routes>
<Route path="/dashboard" element={<Dashboard />} />
<Route path="/settings" element={<Settings />} />
</Routes>
</Suspense>
);
}v6 Data Router(v6.4+)
React Router v6.4 引入了 Data Router,将路由配置与数据加载/变更绑定:
import { createBrowserRouter, RouterProvider } from 'react-router-dom';
const router = createBrowserRouter([
{
path: '/',
element: <Layout />,
errorElement: <ErrorPage />,
children: [
{ index: true, element: <Home /> },
{
path: 'products/:id',
loader: async ({ params }) => {
return fetch(`/api/products/${params.id}`);
},
element: <ProductDetail />,
},
{
path: 'submit',
action: async ({ request }) => {
const data = await request.formData();
return submitForm(data);
},
},
],
},
]);
function App() {
return <RouterProvider router={router} />;
}
// 使用 loader 数据
function ProductDetail() {
const data = useLoaderData();
}Data Router 的 loader/action 优势
| 特性 | 说明 |
|---|---|
| 在渲染前加载数据 | loader 在路由匹配后、组件渲染前执行 |
| 并行加载 | 多个嵌套路由的 loader 并行执行 |
| 错误处理 | errorElement 自动捕获 loader 错误 |
| 表单处理 | <Form> 组件自动触发 action |
常用 Hooks 速查
| Hook | 返回 | 用途 |
|---|---|---|
useParams() |
{ id: '123' } |
路径参数 |
useSearchParams() |
[URLSearchParams, setter] |
查询字符串 |
useNavigate() |
函数 | 编程式导航 |
useLocation() |
{ pathname, search, state } |
当前路由信息 |
useLoaderData() |
loader 返回的数据 | Data Router |
useRouteError() |
errorElement 错误 | 错误处理 |
useMatches() |
匹配的路由链 | 面包屑等 |
路由配置 vs 组件声明
// 方式 1:JSX 声明式(组件中)
<Routes>
<Route path="/" element={<Home />} />
</Routes>
// 方式 2:配置式(createBrowserRouter + RouterProvider)
const router = createBrowserRouter([
{ path: '/', element: <Home /> }
]);💡 最佳实践:中小型项目用声明式(直观),中大型项目用配置式(loader/action 更清晰地组织数据流)。
🚨 常见陷阱
- BrowserRouter 忘记包裹 — 所有路由组件必须包裹在该组件内
- 嵌套路由缺少 Outlet — 子路由不会自动渲染;缺少
<Outlet />子组件不可见 - path 写错 — v6 中路径默认相对,不以
/开头是相对父路由;*通配符必须是最后一个路由 - index vs path="/" — 索引路由用
index,显式/在某些嵌套场景行为不一致 - state 在刷新后丢失 — 通过
navigate('/a', { state: {...} })传递的数据刷新即无 - 在 v6 中使用 v5 组件 —
<Switch>、<Redirect>、useHistory等已被移除