陌上人如玉
公子世无双

6.2 路由配置方式

6.2 路由配置方式

一句话: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>;
}

八、面试延伸

  1. 「React Router 有哪些配置方式?」
    两种:① 声明式——用 JSX 写 <Routes> / <Route>,外面套 BrowserRouter;② 数据路由——用 createBrowserRouter([...]) 配置对象数组,再用 <RouterProvider router={router} /> 渲染。前者简单直观、适合中小项目;后者支持 loader(渲染前取数据)、action(表单提交)、errorElement(路由级错误边界)、路由级懒加载、useNavigation(导航状态)、useBlocker(导航阻塞)。

  2. 「loader 和组件里的 useEffect 请求有什么区别?」
    ① 时机——loader 在「路由匹配后、渲染前」执行(数据就绪才渲染);useEffect 在「渲染后」执行(会先渲染一次「无数据」的状态)。② 体验——loader 避免了「骨架屏闪烁 + 请求瀑布」;useEffect 需要自己管理 loading/error 状态与竞态处理。③ 代价——loader 会让导航阻塞(等数据就绪才切换页面),如果数据慢,用户会感觉「点击没反应」(需要用 useNavigation 显示进度)。

  3. 「errorElement 和 ErrorBoundary 有什么区别?」
    ErrorBoundary 是 React 本身的机制(只能类组件实现,捕获渲染、生命周期、构造函数中的错误);errorElement 是 React Router 数据路由的能力——它除了捕获渲染错误外,还能捕获 loader / action 抛出的错误(尤其是 throw new Response(..., { status: 404 }) 这类「路由级错误」),并且可以按路由层级配置(最近的 errorElement 生效)。所以「路由层级的错误」应该用 errorElement,「组件内部的错误」用 ErrorBoundary。

  4. 「action 是做什么的?」
    action 处理「写操作」(表单提交、数据变更),与 loader 对应。它的特点是:① 与 React Router 的 <Form method="post"> 配合(提交时自动调用对应路由的 action);② 可以返回数据(用 useActionData 读取)或 redirect()(提交后跳转);③ 提交过程中可以用 useNavigation().state === 'submitting' 显示「提交中」;④ 默认行为是「提交完成后自动重新执行该路由的 loader」,从而刷新列表数据。它把「表单 + 提交 + 加载态 + 失效」这一套统一起来了。

  5. 「数据路由能替代 TanStack Query 吗?」
    部分重叠但不能完全替代。重叠的部分是「取数据 + 缓存」(loader vs useQuery)。数据路由独有的是「与路由生命周期绑定」(导航前预取、渲染前就绪、导航状态、导航阻塞)。TanStack Query 更强的是「缓存策略的精细控制」(staleTime/gcTime/invalidateQueries/乐观更新/无限滚动/窗口聚焦重验证)。实践中常见两种方案:① 二选一(用数据路由就尽量用它的 loader/action);② 桥接——在 loader 里调用 queryClient.ensureQueryData(...) 预热缓存,组件里仍用 useQuery 读取。

  6. 「为什么用 createBrowserRouter 就不需要 BrowserRouter 了?」
    因为 RouterProvider 自带 Router——它内部会创建并管理 history 实例。如果再套一层 BrowserRouter,就会变成「Router 里再套 Router」,导致两套 history 实例、路由状态不一致(表现为「导航不生效」或「useNavigate 报错」)。所以用数据路由时,只需 <RouterProvider router={router} />,外层不要再套任何 Router 组件。

  7. 「路由级懒加载怎么写?」
    数据路由下直接在配置里写 lazy: () => import('./pages/Settings'),并在模块里按约定导出:export function Component()(必需)、以及可选的 loader / action / ErrorBoundary。它与 React.lazy 的区别是:① React.lazy 只懒加载「组件代码」,需要自己配 Suspense;② 路由级 lazy 还会自动加载该路由的 loader/action,并且由 Router 管理加载状态(不用手写 Suspense)。所以数据路由下优先用它。

  8. 「从声明式迁到数据路由要注意什么?」
    四点:① 去掉 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 只捕获渲染错误);loader vs useEffect 的区别是「阻塞导航换无闪烁」;数据路由与 TanStack Query 职责重叠,可「二选一」或「loader 里 ensureQueryData 桥接」。

赞(0) 打赏
未经允许不得转载:陌上寒 » 6.2 路由配置方式

评论 抢沙发

觉得文章有用就打赏一下文章作者

非常感谢你的打赏,我们将继续给力更多优质内容,让我们一起创建更加美好的网络世界!

微信扫一扫

支付宝扫一扫