
一句话:v6 有两种配置方式——声明式(
<Routes>JSX) 简单直观、适合中小项目;数据路由(createBrowserRouter) 支持loader/action/errorElement与「路由级懒加载」,更适合现代项目。
一、声明式配置(JSX)
// App.tsx
import { Routes, Route } from 'react-router-dom';
function App() {
return (
<Routes>
<Route path="/" element={<Layout />}>
<Route index element={<Home />} />
<Route path="about" element={<About />} />
<Route path="users/:id" element={<UserDetail />} />
<Route path="*" element={<NotFound />} />
</Route>
</Routes>
);
}
// main.tsx —— 需要自己套一层 Router
<BrowserRouter>
<App />
</BrowserRouter>
| 优点 | 缺点 |
|---|---|
| 直观(就是 JSX,与组件写法一致) | 无法在「渲染之前」预取数据(只能靠组件里的 useEffect) |
灵活(可以在 element 里做任意逻辑) |
没有内置的错误边界与导航阻塞 |
| 迁移成本低(从 v5 过来改动小) | 路由懒加载要自己配 React.lazy |
二、数据路由(createBrowserRouter)
// router.tsx
import { createBrowserRouter, RouterProvider } from 'react-router-dom';
const router = createBrowserRouter([
{
path: '/',
element: <Layout />,
errorElement: <ErrorPage />, // ⭐ 这一层路由的错误边界
children: [
{ index: true, element: <Home /> },
{
path: 'about',
element: <About />,
loader: async () => { // ⭐ 渲染前预取数据
return fetchAbout();
},
},
{
path: 'users/:id',
element: <UserDetail />,
loader: async ({ params }) => {
const user = await fetchUser(params.id!);
if (!user) throw new Response('Not Found', { status: 404 });
return user;
},
errorElement: <UserError />,
},
{
path: 'settings',
lazy: () => import('./pages/Settings'), // ⭐ 路由级懒加载
},
{ path: '*', element: <NotFound /> },
],
},
]);
// main.tsx
createRoot(document.getElementById('root')!).render(
<RouterProvider router={router} /> {/* ⭐ 不需要再套 BrowserRouter */}
);
// 组件里读取 loader 的数据
import { useLoaderData } from 'react-router-dom';
function UserDetail() {
const user = useLoaderData() as User; // ⭐ 数据已经在「渲染前」就绪
return <div>{user.name}</div>;
}
// 懒加载模块的约定:导出 Component / loader / action / ErrorBoundary
// pages/Settings.tsx
export function Component() {
const data = useLoaderData() as Settings;
return <div>{data.theme}</div>;
}
export async function loader() {
return fetchSettings();
}
export function ErrorBoundary() {
return <div>设置页出错了</div>;
}
// 也可以一次导出多个:export { Component, loader }
三、对比
| 维度 | 声明式 <Routes> |
数据路由 createBrowserRouter |
|---|---|---|
| 配置形式 | JSX | 对象数组 |
| Router 包裹 | 需要 BrowserRouter |
用 RouterProvider |
| 数据预取 | ❌(只能用 useEffect) |
✅ loader |
| 数据变更 | ❌ | ✅ action |
| 错误边界 | 自己写 ErrorBoundary |
✅ errorElement |
| 路由级懒加载 | React.lazy + Suspense |
✅ lazy: () => import(...) |
导航状态(useNavigation) |
❌ | ✅(可以显示「提交中」) |
表单集成(<Form>) |
❌ | ✅ |
导航阻塞(useBlocker) |
❌ | ✅(如「有未保存改动」拦截) |
| 上手成本 | 低 | 中 |
四、数据路由的额外能力
1) loader:渲染前取数据
// ✅ loader 在「路由匹配后、渲染前」执行
{
path: 'users/:id',
element: <UserDetail />,
loader: async ({ params, request, signal }) => {
const url = new URL(request.url);
const user = await fetchUser(params.id!, { signal }); // ⭐ 支持取消
return user;
},
}
// 组件里同步读取(不需要 loading 状态)
function UserDetail() {
const user = useLoaderData() as User;
return <div>{user.name}</div>;
}
对比「组件里 useEffect 请求」:
· useEffect:渲染 → 显示骨架 → 请求 → 再渲染(瀑布 + 闪烁)
· loader: 请求 → (等待)→ 渲染(数据已在手上)
⚠️ 代价:导航会「等到数据就绪」才切换页面。
如果需要「立刻切换 + 显示加载态」,用 useNavigation 的 state 显示进度。
2) action:处理表单提交
// ✅ action 处理「写操作」,可以返回数据或重定向
{
path: 'todos/new',
element: <NewTodo />,
action: async ({ request }) => {
const formData = await request.formData();
const todo = await createTodo({ text: formData.get('text') as string });
return redirect('/todos'); // ⭐ 提交后重定向
},
}
// 组件里用 <Form>(React Router 的,不是原生 form)
import { Form, useActionData, useNavigation } from 'react-router-dom';
function NewTodo() {
const actionData = useActionData() as { error?: string } | undefined;
const navigation = useNavigation();
const isSubmitting = navigation.state === 'submitting'; // ⭐ 提交状态
return (
<Form method="post">
<input name="text" required />
<button disabled={isSubmitting}>{isSubmitting ? '提交中…' : '添加'}</button>
{actionData?.error && <p role="alert">{actionData.error}</p>}
</Form>
);
}
3) errorElement:路由级错误边界
// ✅ 路由抛出的 Response 会被最近的 errorElement 捕获
{
path: 'users/:id',
element: <UserDetail />,
loader: async ({ params }) => {
const user = await fetchUser(params.id!);
if (!user) throw new Response('Not Found', { status: 404 });
return user;
},
errorElement: <UserError />,
}
// 错误组件里用 useRouteError 读取错误
import { useRouteError, isRouteErrorResponse } from 'react-router-dom';
function UserError() {
const error = useRouteError();
if (isRouteErrorResponse(error)) {
return <div>{error.status} {error.statusText}</div>; // ⭐ 处理 Response 类错误
}
return <div>出错了:{String(error)}</div>;
}
4) 路由级懒加载
{
path: 'settings',
lazy: () => import('./pages/Settings'),
// ⭐ 模块里导出 Component / loader / ErrorBoundary 等
// 加载期间用「上层」的 fallback(或 HydrateFallback)
}
五、RouterProvider 的嵌套与上下文
// ✅ RouterProvider 可以放在任意位置(不需要包在最外层)
createRoot(root).render(
<ThemeProvider>
<RouterProvider router={router} />
</ThemeProvider>
);
// ⚠️ 但「路由组件」内部不能用外层 Router 的 Hooks(因为没有嵌套)
// ✅ 需要「其它上下文」时,用「布局路由」包起来
const router = createBrowserRouter([
{
path: '/',
element: (
<AppProviders> {/* ⭐ 把 Provider 放在布局里 */}
<Outlet />
</AppProviders>
),
children: [
{ index: true, element: <Home /> },
],
},
]);
六、选型建议
选「声明式 Routes」:
① 中小项目、路由逻辑简单
② 数据获取已经很成熟(TanStack Query)→ 不需要 loader
③ 从 v5 迁移过来的存量项目(改动小)
选「数据路由 createBrowserRouter」:
① 新项目、想用官方推荐的「数据 API」
② 需要「渲染前预取数据」(避免瀑布与闪烁)
③ 需要「表单 action + 提交状态 + 自动失效」
④ 需要「路由级错误边界」(errorElement)
⑤ 需要「导航阻塞」(useBlocker,如「未保存提示」)
⚠️ 但要注意:如果项目已经用 TanStack Query 管理数据,
loader 与 Query 的职责会重叠(两者都在「取数据 + 缓存」)。
常见做法是「二选一」或「loader 里调 queryClient.ensureQueryData」。
七、常见坑
// ① 用了 createBrowserRouter 又套 BrowserRouter(重复)
<BrowserRouter><RouterProvider router={router} /></BrowserRouter> // ❌
// ② 用了 RouterProvider 但忘了它「自带 Router」
// ③ loader 里返回「不可序列化」的东西(如类实例、函数)
loader: () => new UserEntity(data) // ⚠️ 建议返回普通对象
// ④ loader 抛出「非 Response 错误」导致 errorElement 拿不到 status
throw new Error('not found'); // ⚠️ 用 isRouteErrorResponse 判断会失败
throw new Response('Not Found', { status: 404 }); // ✅
// ⑤ 用 <form> 而不是 React Router 的 <Form>(action 不生效)
<form method="post">...</form> // ⚠️ 会走浏览器原生提交
<Form method="post">...</Form> // ✅
// ⑥ 在 loader 里做「副作用」(它应该是「取数据」的)
// ⑦ 期待 loader「在路由切换时立刻取消」→ 需要自己把 signal 传给 fetch
// ⑧ 懒加载的模块忘了导出 Component
export default function Settings() {} // ⚠️ 需要 export function Component
export function Component() {} // ✅
// ⑨ 声明式配置里用 loader(不生效)
// ⑩ errorElement 放在「太外层」→ 整个应用都被替换(应该分层配置)
// ⑪ 认为「数据路由能替代 TanStack Query」(职责重叠,要选清楚)
// ⑫ 在「数组配置」里写 JSX 时忘了导入组件
// ⑬ 路由级懒加载与 React.lazy 混用(重复)
// ⑭ 忘记「loader 在导航时会阻塞页面切换」(需要 loading 指示)
// ⑭ 的完整处理
function Layout() {
const navigation = useNavigation();
const isLoading = navigation.state === 'loading';
return (
<div>
{isLoading && <TopProgressBar />} {/* ⭐ 顶部进度条 */}
<Outlet />
</div>
);
}
// ⑪ 的「桥接」做法:loader 里预热 Query 缓存
const queryClient = new QueryClient();
const router = createBrowserRouter([
{
path: 'users/:id',
element: <UserDetail />,
loader: ({ params }) => {
return queryClient.ensureQueryData({
queryKey: ['user', params.id],
queryFn: () => fetchUser(params.id!),
});
},
},
]);
// 组件里仍用 useQuery(它会命中已预热的缓存)
function UserDetail() {
const { id } = useParams();
const { data } = useQuery({ queryKey: ['user', id], queryFn: () => fetchUser(id!) });
return <div>{data?.name}</div>;
}
八、面试延伸
- 「React Router 有哪些配置方式?」
两种:① 声明式——用 JSX 写<Routes>/<Route>,外面套BrowserRouter;② 数据路由——用createBrowserRouter([...])配置对象数组,再用<RouterProvider router={router} />渲染。前者简单直观、适合中小项目;后者支持loader(渲染前取数据)、action(表单提交)、errorElement(路由级错误边界)、路由级懒加载、useNavigation(导航状态)、useBlocker(导航阻塞)。 -
「
loader和组件里的useEffect请求有什么区别?」
① 时机——loader在「路由匹配后、渲染前」执行(数据就绪才渲染);useEffect在「渲染后」执行(会先渲染一次「无数据」的状态)。② 体验——loader避免了「骨架屏闪烁 + 请求瀑布」;useEffect需要自己管理loading/error状态与竞态处理。③ 代价——loader会让导航阻塞(等数据就绪才切换页面),如果数据慢,用户会感觉「点击没反应」(需要用useNavigation显示进度)。 -
「
errorElement和ErrorBoundary有什么区别?」
ErrorBoundary是 React 本身的机制(只能类组件实现,捕获渲染、生命周期、构造函数中的错误);errorElement是 React Router 数据路由的能力——它除了捕获渲染错误外,还能捕获loader/action抛出的错误(尤其是throw new Response(..., { status: 404 })这类「路由级错误」),并且可以按路由层级配置(最近的errorElement生效)。所以「路由层级的错误」应该用errorElement,「组件内部的错误」用ErrorBoundary。 -
「
action是做什么的?」
action处理「写操作」(表单提交、数据变更),与loader对应。它的特点是:① 与 React Router 的<Form method="post">配合(提交时自动调用对应路由的action);② 可以返回数据(用useActionData读取)或redirect()(提交后跳转);③ 提交过程中可以用useNavigation().state === 'submitting'显示「提交中」;④ 默认行为是「提交完成后自动重新执行该路由的 loader」,从而刷新列表数据。它把「表单 + 提交 + 加载态 + 失效」这一套统一起来了。 -
「数据路由能替代 TanStack Query 吗?」
部分重叠但不能完全替代。重叠的部分是「取数据 + 缓存」(loadervsuseQuery)。数据路由独有的是「与路由生命周期绑定」(导航前预取、渲染前就绪、导航状态、导航阻塞)。TanStack Query 更强的是「缓存策略的精细控制」(staleTime/gcTime/invalidateQueries/乐观更新/无限滚动/窗口聚焦重验证)。实践中常见两种方案:① 二选一(用数据路由就尽量用它的 loader/action);② 桥接——在loader里调用queryClient.ensureQueryData(...)预热缓存,组件里仍用useQuery读取。 -
「为什么用
createBrowserRouter就不需要BrowserRouter了?」
因为RouterProvider自带 Router——它内部会创建并管理 history 实例。如果再套一层BrowserRouter,就会变成「Router 里再套 Router」,导致两套 history 实例、路由状态不一致(表现为「导航不生效」或「useNavigate 报错」)。所以用数据路由时,只需<RouterProvider router={router} />,外层不要再套任何 Router 组件。 -
「路由级懒加载怎么写?」
数据路由下直接在配置里写lazy: () => import('./pages/Settings'),并在模块里按约定导出:export function Component()(必需)、以及可选的loader/action/ErrorBoundary。它与React.lazy的区别是:①React.lazy只懒加载「组件代码」,需要自己配Suspense;② 路由级lazy还会自动加载该路由的loader/action,并且由 Router 管理加载状态(不用手写Suspense)。所以数据路由下优先用它。 -
「从声明式迁到数据路由要注意什么?」
四点:① 去掉BrowserRouter(改用RouterProvider);② 把 JSX 结构改写成对象数组(element保留,children→children数组);③ 把「组件里的请求」逐步迁到loader,把「表单提交」迁到action(或者用「桥接」方式先只迁路由结构);④ 补上errorElement(否则loader抛错时会显示默认错误页)。如果项目已有成熟的请求层(TanStack Query),建议先只迁「路由结构 + 懒加载 + errorElement」,数据获取保持原样。
一句话速记
v6 有两种配置方式:声明式
<Routes>(简单、需自己套BrowserRouter、数据靠useEffect)与数据路由createBrowserRouter+<RouterProvider>(自带 Router,提供loader(渲染前预取)、action(表单提交 +useNavigation状态)、errorElement(路由级错误边界 +useRouteError/isRouteErrorResponse)、lazy(路由级懒加载,模块导出Component)、useBlocker);errorElement 捕获 loader/action 抛出的Response(ErrorBoundary只捕获渲染错误);loadervsuseEffect的区别是「阻塞导航换无闪烁」;数据路由与 TanStack Query 职责重叠,可「二选一」或「loader里ensureQueryData桥接」。



最新评论
读过书不知道欧·亨利的人少。教科书上选文有
这小生活不错呀
不错,必须顶一下!
看着你还在坚持,很好
看来忙了也没时间更新博客了
NIce。学习了。。。。
网站不错!!!!
简洁实用,好文章!