深入理解 React 路由
React Router v7、TanStack Router 基础使用、Loader/Action 模式、类型安全路由与数据预加载
什么是前端路由?
定义:前端路由是在不刷新页面的情况下,根据 URL 变化渲染不同组件的机制。React 生态中主流路由方案是 React Router(v7 已与 Remix 合并)和 TanStack Router(类型安全优先)。
涉及场景:
- SPA 页面导航:用户点击链接切换页面,不触发整页刷新
- 嵌套路由:侧边栏布局、标签页切换、多级导航
- 数据预加载:路由切换前预取数据(Loader),减少白屏时间
- 表单提交:路由级 Action 处理表单(POST/PUT/DELETE)
- 权限守卫:根据登录状态重定向到登录页
- URL 状态:搜索条件、分页等序列化到 URL(可分享、可后退)
作用:
- 用户体验:即时导航、浏览器前进/后退正常工作
- 数据与路由绑定:Loader/Action 模式消除了组件内手动请求的样板代码
- 代码分割:按路由懒加载,减小首屏 bundle
- 面试考点:路由原理(History API)、嵌套路由、数据加载策略是常见题目
一、React Router v7
React Router v7 是 React Router 与 Remix 合并后的版本,同时支持 SPA 模式和全栈模式。
基础使用
bash
npm install react-routerjsx
import { createBrowserRouter, RouterProvider, Link, Outlet } from 'react-router';
// 定义路由
const router = createBrowserRouter([
{
path: '/',
element: <RootLayout />,
errorElement: <ErrorPage />,
children: [
{ index: true, element: <Home /> },
{
path: 'posts',
element: <PostsLayout />,
children: [
{ index: true, element: <PostList /> },
{ path: ':postId', element: <PostDetail /> },
{ path: 'new', element: <NewPost /> },
],
},
{ path: 'about', element: <About /> },
{ path: '*', element: <NotFound /> },
],
},
]);
function App() {
return <RouterProvider router={router} />;
}
// 布局组件
function RootLayout() {
return (
<div>
<nav>
<Link to="/">首页</Link>
<Link to="/posts">文章</Link>
<Link to="/about">关于</Link>
</nav>
<main>
<Outlet /> {/* 子路由在此渲染 */}
</main>
</div>
);
}Loader —— 路由级数据加载
jsx
const router = createBrowserRouter([
{
path: 'posts',
element: <PostList />,
// Loader 在路由匹配时自动执行(导航前/并行加载)
loader: async () => {
const res = await fetch('/api/posts');
if (!res.ok) throw new Response('加载失败', { status: res.status });
return res.json();
},
},
{
path: 'posts/:postId',
element: <PostDetail />,
loader: async ({ params }) => {
const res = await fetch(`/api/posts/${params.postId}`);
if (!res.ok) throw new Response('文章不存在', { status: 404 });
return res.json();
},
},
]);
// 组件中获取 Loader 数据
import { useLoaderData } from 'react-router';
function PostList() {
const posts = useLoaderData(); // 类型:loader 的返回值
return (
<ul>
{posts.map(post => (
<li key={post.id}>
<Link to={`/posts/${post.id}`}>{post.title}</Link>
</li>
))}
</ul>
);
}Action —— 路由级数据变更
jsx
const router = createBrowserRouter([
{
path: 'posts/new',
element: <NewPost />,
action: async ({ request }) => {
const formData = await request.formData();
const title = formData.get('title');
const content = formData.get('content');
const res = await fetch('/api/posts', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ title, content }),
});
if (!res.ok) {
return { error: '创建失败' }; // 返回错误给组件
}
return redirect('/posts'); // 成功后跳转
},
},
]);
// 组件中使用 Form(自动触发 action)
import { Form, useActionData, useNavigation } from 'react-router';
function NewPost() {
const actionData = useActionData(); // action 返回的数据
const navigation = useNavigation();
const isSubmitting = navigation.state === 'submitting';
return (
<Form method="post">
<input name="title" required />
<textarea name="content" required />
{actionData?.error && <p className="error">{actionData.error}</p>}
<button disabled={isSubmitting}>
{isSubmitting ? '提交中...' : '发布文章'}
</button>
</Form>
);
}常用 Hooks
jsx
import {
useParams, // 获取路由参数 { postId: '123' }
useSearchParams, // 获取/设置查询参数
useNavigate, // 编程式导航
useLocation, // 获取当前 location
useNavigation, // 获取导航状态(loading/submitting/idle)
useMatches, // 获取匹配的路由层级
useRouteError, // 在 errorElement 中获取错误
} from 'react-router';
// 查询参数
function SearchPage() {
const [searchParams, setSearchParams] = useSearchParams();
const query = searchParams.get('q') || '';
const page = Number(searchParams.get('page')) || 1;
return (
<div>
<input
value={query}
onChange={(e) => setSearchParams({ q: e.target.value, page: '1' })}
/>
<button onClick={() => setSearchParams({ q: query, page: String(page + 1) })}>
下一页
</button>
</div>
);
}
// 编程式导航
function LogoutButton() {
const navigate = useNavigate();
const handleLogout = async () => {
await logout();
navigate('/login', { replace: true }); // replace 不留历史记录
};
return <button onClick={handleLogout}>退出</button>;
}错误处理
jsx
const router = createBrowserRouter([
{
path: '/',
element: <RootLayout />,
errorElement: <RootError />, // 捕获子路由的所有错误
children: [
{
path: 'posts/:id',
element: <PostDetail />,
errorElement: <PostError />, // 局部错误边界
loader: async ({ params }) => {
const res = await fetch(`/api/posts/${params.id}`);
if (!res.ok) throw new Response('Not Found', { status: 404 });
return res.json();
},
},
],
},
]);
function PostError() {
const error = useRouteError();
if (error instanceof Response && error.status === 404) {
return <div>文章不存在</div>;
}
return <div>加载出错了</div>;
}二、TanStack Router
TanStack Router 是以类型安全为核心的路由方案,所有路由参数、搜索参数、Loader 数据都有完整的 TypeScript 类型推断。
基础使用
bash
npm install @tanstack/react-routertsx
import {
createRootRoute,
createRoute,
createRouter,
RouterProvider,
Link,
Outlet,
} from '@tanstack/react-router';
// 根路由
const rootRoute = createRootRoute({
component: () => (
<div>
<nav>
<Link to="/">首页</Link>
<Link to="/posts">文章</Link>
</nav>
<Outlet />
</div>
),
});
// 子路由
const indexRoute = createRoute({
getParentRoute: () => rootRoute,
path: '/',
component: () => <h1>首页</h1>,
});
const postsRoute = createRoute({
getParentRoute: () => rootRoute,
path: '/posts',
component: PostList,
loader: async () => {
const posts = await fetchPosts();
return { posts };
},
});
const postRoute = createRoute({
getParentRoute: () => postsRoute,
path: '$postId', // $ 前缀表示动态参数
component: PostDetail,
loader: async ({ params }) => {
// params.postId 自动推断为 string 类型
const post = await fetchPost(params.postId);
return { post };
},
});
// 创建路由树
const routeTree = rootRoute.addChildren([
indexRoute,
postsRoute.addChildren([postRoute]),
]);
// 创建 router
const router = createRouter({ routeTree });
// 类型声明(让全局类型推断生效)
declare module '@tanstack/react-router' {
interface Register {
router: typeof router;
}
}
function App() {
return <RouterProvider router={router} />;
}类型安全的搜索参数
tsx
import { z } from 'zod';
const postsRoute = createRoute({
getParentRoute: () => rootRoute,
path: '/posts',
// 搜索参数的 schema 验证
validateSearch: z.object({
page: z.number().default(1),
sort: z.enum(['date', 'title']).default('date'),
q: z.string().optional(),
}),
component: PostList,
});
function PostList() {
// search 的类型自动推断:{ page: number; sort: 'date' | 'title'; q?: string }
const { page, sort, q } = postsRoute.useSearch();
const navigate = postsRoute.useNavigate();
return (
<div>
<input
value={q ?? ''}
onChange={(e) => navigate({ search: { q: e.target.value, page: 1, sort } })}
/>
<button onClick={() => navigate({ search: { page: page + 1, sort, q } })}>
下一页(当前第 {page} 页)
</button>
</div>
);
}三、路由原理
History API
javascript
// 前端路由基于 History API
history.pushState(state, '', '/new-url'); // 添加历史记录
history.replaceState(state, '', '/new-url'); // 替换当前记录
history.back(); // 后退
history.forward(); // 前进
// 监听前进/后退
window.addEventListener('popstate', (event) => {
console.log('URL changed to:', location.pathname);
// 根据新 URL 渲染对应组件
});两种路由模式
BrowserRouter(推荐):
URL: https://example.com/posts/123
基于 History API
需要服务端配置(所有路径返回 index.html)
HashRouter:
URL: https://example.com/#/posts/123
基于 hash 变化(hashchange 事件)
无需服务端配置(hash 部分不发送到服务器)
SEO 不友好四、React Router v7 vs TanStack Router
| 维度 | React Router v7 | TanStack Router |
|---|---|---|
| 类型安全 | 基础 | 完整推断 |
| 搜索参数 | 字符串手动解析 | Schema 验证+类型推断 |
| 文件路由 | 支持(框架模式) | 支持(插件) |
| Loader/Action | 内置 | 内置 |
| SSR | 完善 | 支持 |
| 生态成熟度 | 非常成熟 | 快速增长 |
| 学习曲线 | 低 | 中(需要理解类型系统) |
| 适合 | 大多数项目 | 类型安全要求高的项目 |
五、路由最佳实践
路由级代码分割
jsx
import { lazy } from 'react';
const Dashboard = lazy(() => import('./pages/Dashboard'));
const Settings = lazy(() => import('./pages/Settings'));
const router = createBrowserRouter([
{
path: 'dashboard',
element: (
<Suspense fallback={<PageSkeleton />}>
<Dashboard />
</Suspense>
),
loader: dashboardLoader, // loader 和组件并行加载
},
]);权限守卫
jsx
// 方式1:Loader 中检查权限
{
path: 'admin',
loader: async () => {
const user = await getUser();
if (!user || user.role !== 'admin') {
throw redirect('/login');
}
return { user };
},
element: <AdminPanel />,
}
// 方式2:包裹组件
function RequireAuth({ children }) {
const { user } = useAuth();
const location = useLocation();
if (!user) {
return <Navigate to="/login" state={{ from: location }} replace />;
}
return children;
}面包屑导航
jsx
import { useMatches } from 'react-router';
function Breadcrumbs() {
const matches = useMatches();
const crumbs = matches
.filter(match => match.handle?.crumb)
.map(match => match.handle.crumb(match.data));
return (
<nav>
{crumbs.map((crumb, i) => (
<span key={i}>{i > 0 && ' > '}{crumb}</span>
))}
</nav>
);
}
// 路由定义中
{
path: 'posts/:id',
handle: {
crumb: (data) => data.post.title,
},
}面试高频题
Q: React Router 的 Loader 和 useEffect 获取数据有什么区别?
- Loader:在路由匹配时并行执行(与组件加载同时进行),导航完成前数据已就绪,无白屏
- useEffect:组件渲染后才发起请求,产生"渲染 → 请求 → 等待 → 再渲染"的瀑布流
- Loader 还支持表单 Action 提交后自动重新执行,保持数据最新
Q: 前端路由的两种模式?
- History 模式:基于
history.pushState,URL 无#,需服务端配合(所有路径返回 index.html) - Hash 模式:基于 URL 的
#部分,浏览器不发送 hash 到服务器,无需服务端配置,但 SEO 不友好
Q: 嵌套路由的好处?
- 布局复用:共享 Header/Sidebar/Footer,只切换内容区域
- 数据并行加载:父子路由的 Loader 同时执行,不产生瀑布流
- 局部错误处理:子路由出错不影响父路由的 UI
- 渐进式加载:父路由先渲染,子路由加载中显示 Suspense fallback