路由是现代单页应用(SPA)的骨架。React Router 是 React 生态中最流行的路由库,从 v6 开始彻底重构了 API,引入了 loader/action、嵌套路由、错误边界等现代模式。本文覆盖从基础到生产级的完整路由实践。
一、为什么 React Router 仍然是 2025 年的标准
| 特性 | React Router v6 | Next.js App Router | TanStack Router |
|---|---|---|---|
| 框架绑定 | 无(任何 React 应用) | Next.js 专属 | 无(但生态较小) |
| 数据获取 | ✅ loader/action(v6.4+) | ✅ Server Components | ✅ loader 类似 |
| 嵌套路由 | ✅ 声明式 | ✅ 文件系统约定 | ✅ 声明式 |
| 类型安全 | 弱(需插件) | 弱 | ✅ 路径类型推导 |
| 社区生态 | 最大 | 大(Next.js 内) | 小 |
| React Native | ✅ React Navigation 模型 | ❌ | ❌ |
结论:如果不使用 Next.js,React Router 是 React SPA 路由的唯一标准选择。如果已在 Next.js 生态中,App Router 的文件系统路由更方便。
二、基础路由配置
2.1 声明式路由
import { createBrowserRouter, RouterProvider } from 'react-router-dom';
const router = createBrowserRouter([
{
path: '/',
element: <Layout />,
children: [
{ index: true, element: <Home /> },
{ path: 'about', element: <About /> },
{ path: 'contact', element: <Contact /> },
],
},
]);
function App() {
return <RouterProvider router={router} />;
}
createBrowserRouter vs createHashRouter:
BrowserRouter:使用 HTML5 History API,/users/123,需要服务器配置回退到 index.htmlHashRouter:使用#/users/123,不需要服务器配置,但 URL 不美观- 生产环境推荐 BrowserRouter,Dev 或 Electron 可用 HashRouter
2.2 路由组件三要素
| 组件/Hook | 用途 | 示例 |
|---|---|---|
<Link> | 声明式导航 | <Link to="/about">About</Link> |
<NavLink> | 带激活状态的导航 | <NavLink to="/dashboard" className={({ isActive }) => isActive ? 'active' : ''}> |
useNavigate() | 编程式导航 | const navigate = useNavigate(); navigate('/login') |
useParams() | 读取 URL 参数 | const { userId } = useParams() |
useSearchParams() | 读取/修改查询参数 | const [searchParams, setSearchParams] = useSearchParams() |
useLocation() | 获取当前位置对象 | const { pathname, search, state } = useLocation() |
2.3 动态路由与 URL 参数
// 路由定义
{ path: 'users/:userId/posts/:postId', element: <PostDetail /> }
// 组件中读取
function PostDetail() {
const { userId, postId } = useParams<{ userId: string; postId: string }>();
// URL: /users/123/posts/456 → userId='123', postId='456'
}
// 可选参数(v6 需显式定义两条路由或统一处理)
{ path: 'products/:category?/:productId?', element: <ProductList /> }
2.4 查询参数管理
import { useSearchParams } from 'react-router-dom';
function ProductFilter() {
const [searchParams, setSearchParams] = useSearchParams();
const category = searchParams.get('category');
const page = parseInt(searchParams.get('page') || '1', 10);
const sort = searchParams.get('sort') || 'newest';
const setFilter = (key: string, value: string) => {
const newParams = new URLSearchParams(searchParams);
if (value) {
newParams.set(key, value);
} else {
newParams.delete(key);
}
setSearchParams(newParams);
};
return (
<div>
<button onClick={() => setFilter('category', 'electronics')}>
Electronics
</button>
<button onClick={() => setFilter('page', String(page + 1))}>
Next Page
</button>
</div>
);
}
推荐做法:URL 作为状态来源。筛选条件、页码、排序等应反映在 URL 上,这样用户刷新页面后状态不丢失,也支持分享链接。
三、嵌套路由与 Outlet
嵌套路由是 React Router v6 的核心设计之一,它解决了传统路由"一个路由组件加载所有子组件"导致的重复渲染问题。
3.1 嵌套结构
const router = createBrowserRouter([
{
path: '/',
element: <RootLayout />, // 包含 <Outlet /> 和导航
errorElement: <ErrorPage />, // 错误边界
children: [
{ index: true, element: <Home /> },
{
path: 'dashboard',
element: <DashboardLayout />, // 嵌套的布局
children: [
{ index: true, element: <DashboardHome /> },
{ path: 'analytics', element: <Analytics /> },
{ path: 'settings', element: <Settings /> },
],
},
{
path: 'users/:userId',
element: <UserProfile />,
children: [
{ index: true, element: <UserOverview /> },
{ path: 'posts', element: <UserPosts /> },
{ path: 'settings', element: <UserSettings /> },
],
},
],
},
]);
// RootLayout.tsx —— 根布局
function RootLayout() {
return (
<div className="app">
<NavBar />
<main>
<Outlet /> {/* 子路由组件渲染位置 */}
</main>
<Footer />
</div>
);
}
// DashboardLayout.tsx —— 仪表板布局
function DashboardLayout() {
return (
<div className="dashboard">
<Sidebar />
<div className="content">
<Outlet /> {/* 渲染 DashboardHome / Analytics / Settings */}
</div>
</div>
);
}
Outlet 的优势:子路由切换时,父布局(导航栏、侧边栏)不重新挂载。这在传统路由中需要手动用 Switch / Routes + 状态管理实现,v6 原生支持。
3.2 索引路由(Index Routes)
index: true 表示父路径的默认子路由:
/dashboard→ 渲染<DashboardLayout>+<DashboardHome>/dashboard/analytics→ 渲染<DashboardLayout>+<Analytics>
四、数据 API:Loader 与 Action(v6.4+)
React Router v6.4 引入了数据 API,让路由与数据获取解耦,支持并行加载、优雅降级和乐观 UI。
4.1 Loader:路由数据预取
// types.ts
interface Post {
id: string;
title: string;
content: string;
author: { name: string; avatar: string };
}
// api.ts
export async function getPost(postId: string): Promise<Post> {
const res = await fetch(`/api/posts/${postId}`);
if (!res.ok) throw new Response('Not Found', { status: 404 });
return res.json();
}
// route.tsx
import { LoaderFunctionArgs, useLoaderData } from 'react-router-dom';
import { getPost } from './api';
export async function postLoader({ params }: LoaderFunctionArgs) {
if (!params.postId) throw new Response('Post ID required', { status: 400 });
return getPost(params.postId);
}
export function PostPage() {
const post = useLoaderData() as Post; // loader 返回的数据
return (
<article>
<h1>{post.title}</h1>
<p>{post.content}</p>
</article>
);
}
// router.tsx
{ path: 'posts/:postId', loader: postLoader, element: <PostPage /> }
Loader 的执行时机:
- 用户导航到该路由时并行执行
- 多个子路由的 loader 同时执行(非串行)
- 有缓存时不会重复执行
4.2 Action:表单提交与变更
import { ActionFunctionArgs, Form, useActionData, useNavigation } from 'react-router-dom';
export async function loginAction({ request }: ActionFunctionArgs) {
const formData = await request.formData();
const email = formData.get('email') as string;
const password = formData.get('password') as string;
const res = await fetch('/api/login', {
method: 'POST',
body: JSON.stringify({ email, password }),
headers: { 'Content-Type': 'application/json' },
});
if (!res.ok) {
return { error: 'Invalid credentials' };
}
const { token } = await res.json();
localStorage.setItem('token', token);
return redirect('/dashboard');
}
export function LoginPage() {
const actionData = useActionData<{ error?: string }>();
const navigation = useNavigation();
const isSubmitting = navigation.state === 'submitting';
return (
<Form method="post">
<input name="email" type="email" required />
<input name="password" type="password" required />
<button type="submit" disabled={isSubmitting}>
{isSubmitting ? 'Logging in...' : 'Login'}
</button>
{actionData?.error && <p className="error">{actionData.error}</p>}
</Form>
);
}
4.3 错误处理:Error Boundary
// 全局错误页
export function ErrorPage() {
const error = useRouteError();
if (isRouteErrorResponse(error)) {
return (
<div>
<h1>{error.status} {error.statusText}</h1>
<p>{error.data}</p>
</div>
);
}
return (
<div>
<h1>Oops!</h1>
<p>{(error as Error).message}</p>
</div>
);
}
// router.tsx
{
path: '/',
element: <RootLayout />,
errorElement: <ErrorPage />, // 全局错误边界
children: [
{
path: 'users/:userId',
element: <UserProfile />,
errorElement: <UserNotFound />, // 局部错误边界
loader: userLoader,
},
],
}
4.4 数据 API 流程图
用户点击 <Link to="/posts/123">
↓
React Router 匹配到 /posts/:postId 路由
↓
并行执行该路由及其父路由的 loader 函数
↓
显示父布局(过渡状态,已有数据不变)
↓
loader 完成后,渲染目标组件
↓
用户提交表单 → action 执行 → 成功后重取 loader 数据 / 跳转
五、路由守卫与权限控制
5.1 基础的受保护路由
import { Navigate, Outlet, useLocation } from 'react-router-dom';
function RequireAuth({ children }: { children: React.ReactNode }) {
const { user } = useAuth();
const location = useLocation();
if (!user) {
// 保存尝试访问的路径,登录后重定向回来
return <Navigate to="/login" state={{ from: location }} replace />;
}
return <>{children}</>;
}
// router.tsx
{
path: 'dashboard',
element: (
<RequireAuth>
<DashboardLayout />
</RequireAuth>
),
},```
### 5.2 角色权限路由
```tsx
function RequireRole({ allowedRoles, children }: {
allowedRoles: string[];
children: React.ReactNode;
}) {
const { user } = useAuth();
const location = useLocation();
if (!user) {
return <Navigate to="/login" state={{ from: location }} replace />;
}
if (!allowedRoles.includes(user.role)) {
return <Navigate to="/unauthorized" replace />;
}
return <>{children}</>;
}
// 使用
{
path: 'admin',
element: (
<RequireRole allowedRoles={['admin', 'moderator']}>
<AdminPanel />
</RequireRole>
),
},
5.3 基于 Loader 的权限检查
export async function adminLoader() {
const user = await getCurrentUser();
if (!user) {
throw redirect('/login');
}
if (!['admin', 'moderator'].includes(user.role)) {
throw redirect('/unauthorized');
}
return user;
}
六、高级模式
6.1 懒加载与代码分割
import { lazy, Suspense } from 'react';
const Dashboard = lazy(() => import('./pages/Dashboard'));
const Analytics = lazy(() => import('./pages/Analytics'));
{
path: 'dashboard',
element: (
<Suspense fallback={<LoadingSpinner />}>
<Dashboard />
</Suspense>
),
},
数据 API 与懒加载的结合:
// 路由懒加载 + loader 也懒加载
const userRoute = {
path: 'users/:userId',
async lazy() {
const { UserPage, userLoader } = await import('./pages/UserPage');
return { Component: UserPage, loader: userLoader };
},
};
6.2 滚动恢复
import { ScrollRestoration } from 'react-router-dom';
function RootLayout() {
return (
<div>
<ScrollRestoration /> {/* 自动恢复滚动位置 */}
<Outlet />
</div>
);
}
自定义滚动行为:
const router = createBrowserRouter(routes, {
scrollBehavior: 'auto', // 默认:自动恢复
});
// 或不恢复(SPA 常见)
const router = createBrowserRouter(routes);
6.3 过渡导航(Deferred Data)
import { Await, defer, useLoaderData } from 'react-router-dom';
export async function dashboardLoader() {
const userPromise = getCurrentUser(); // 立即需要
const statsPromise = getStats(); // 可以延后
const notificationsPromise = getNotifications(); // 可以延后
return defer({
user: await userPromise, // 等待(阻塞渲染)
stats: statsPromise, // 流式传输(不阻塞)
notifications: notificationsPromise,
});
}
export function Dashboard() {
const { user, stats, notifications } = useLoaderData() as {
user: User;
stats: Promise<Stats>;
notifications: Promise<Notification[]>;
};
return (
<div>
<h1>Welcome, {user.name}</h1>
<Suspense fallback={<StatsSkeleton />}>
<Await resolve={stats}>
{(statsData) => <StatsWidget data={statsData} />}
</Await>
</Suspense>
<Suspense fallback={<NotificationsSkeleton />}>
<Await resolve={notifications}>
{(notifData) => <NotificationList data={notifData} />}
</Await>
</Suspense>
</div>
);
}
七、从 v5 迁移到 v6 要点
| v5 API | v6 等价 | 变化说明 |
|---|---|---|
<Switch> | <Routes> | 语义更清晰 |
<Route exact> | <Route> | v6 路由默认精确匹配 |
<Route component={Home}> | <Route element={<Home />}> | element 接收 JSX |
useHistory() | useNavigate() | 新 hook |
history.push('/path') | navigate('/path') | 新 API |
history.replace('/path') | navigate('/path', { replace: true }) | 选项对象 |
<Redirect to="/" /> | <Navigate to="/" replace /> | 新组件 |
useRouteMatch() | useMatch() | 简化 |
component={Comp} | element={<Comp />} | 不再支持 render/component 属性 |
常见问题(FAQ)
React Router 和 Next.js App Router 怎么选?
- 已用 Next.js:选 App Router,文件系统路由、Server Components、流式渲染集成更好
- 纯 React SPA / Create React App / Vite:React Router 是唯一标准选择
- 需要同时支持 Web 和 Native:React Router 的声明式模式与 React Navigation 概念相同
路由守卫放在 loader 还是组件里?
- 简单权限(登录检查):放在组件里(
<RequireAuth>),UI 控制更灵活 - 复杂权限(角色、权限码):放在 loader 里,可以在渲染前就做重定向,用户体验更好
怎么处理 404 Not Found?
// 在路由配置末尾添加通配符路由
{
path: '*',
element: <NotFoundPage />,
}
loader 中抛出的错误怎么处理?
使用 errorElement 捕获Loader 抛出的错误会在最近的 errorElement 中捕获。如果是 Response 类型的抛出,会转为 RouteErrorResponse。
相关阅读
- React 详解 — SPA 架构与 React Router 集成
- React Hooks 完全指南 —
useNavigate、useParams等 Router Hooks - React + TypeScript 实战指南 — 类型安全的路由参数处理
- React 状态管理指南 — URL 状态与全局状态的配合策略
- Next.js 完全指南专题 — Next.js App Router 对比
- TanStack Router 官方文档
- React Router 官方文档
继续阅读
探索更多技术文章
浏览归档,发现更多关于系统设计、工具链和工程实践的内容。