人人都会AI编程

12.6 路由懒加载与代码分割

更新时间:2026-07-10

从 React Router v6.4 开始,引入了数据路由(Data Router)模式,核心思想是将数据获取与导航动作深度集成到路由定义中,通过 loaderaction 两个关键 API,实现在路由切换时自动加载数据、处理表单提交,并自动处理加载状态、错误边界和数据重新验证。

这种模式让组件无需自行管理请求时机和 loading 状态,而是直接从路由提供的 API 中消费数据,组件职责更加纯粹——只负责渲染。

12.6.1 数据路由的核心概念

数据路由的核心结构基于 createBrowserRouter,路由定义中新增了两个关键属性:

  • loader:在导航到该路由时自动调用的函数,用于获取页面所需数据。数据返回后,组件可通过 useLoaderData 获取。
  • action:当该路由上发生表单提交(如 <Form>useSubmit)时调用的函数,用于处理数据变更(新增、编辑、删除)。组件可通过 useActionData 获取 action 返回的结果。

这种设计将数据流与路由绑定,实现了声明式数据加载,同时 React Router 内部自动处理了竞态条件、重新验证和错误边界。

12.6.2 定义路由:从 createBrowserRouter 开始

首先,使用 createBrowserRouter 创建路由实例,并在每个路由中定义 loaderaction

// router.js
import { createBrowserRouter } from 'react-router-dom';
import RootLayout from './pages/Root';
import ProductsPage from './pages/Products';
import ProductDetailPage, { loader as productLoader } from './pages/ProductDetail';
import NewProductPage from './pages/NewProduct';
import { addProductAction } from './actions/productActions';

export const router = createBrowserRouter([
  {
    path: '/',
    element: <RootLayout />,
    children: [
      {
        index: true,
        element: <ProductsPage />,
        loader: async () => {
          const res = await fetch('/api/products');
          return { products: await res.json() };
        },
      },
      {
        path: 'products/:id',
        element: <ProductDetailPage />,
        loader: async ({ params }) => {
          const res = await fetch(`/api/products/${params.id}`);
          if (!res.ok) throw new Response('未找到产品', { status: 404 });
          return { product: await res.json() };
        },
      },
      {
        path: 'products/new',
        element: <NewProductPage />,
        action: addProductAction,
      },
    ],
  },
]);
  • loader 接收一个对象参数,包含路由参数(params)、请求对象(request)等。
  • loader 抛出 Response(如 404)时,React Router 会渲染最近的 errorElement

12.6.3 在组件中使用 Loader 数据

组件通过 useLoaderData 获取对应路由 loader 的返回结果,不再需要自行管理 useState + useEffect 的数据请求模式。

// pages/Products.jsx
import { useLoaderData, Link } from 'react-router-dom';

function ProductsPage() {
  const { products } = useLoaderData();

  return (
    <ul>
      {products.map((product) => (
        <li key={product.id}>
          <Link to={`/products/${product.id}`}>{product.name}</Link>
        </li>
      ))}
    </ul>
  );
}
// pages/ProductDetail.jsx
import { useLoaderData } from 'react-router-dom';

export async function loader({ params }) {
  const res = await fetch(`/api/products/${params.id}`);
  if (!res.ok) throw new Response('Not Found', { status: 404 });
  return { product: await res.json() };
}

function ProductDetailPage() {
  const { product } = useLoaderData();

  return (
    <div>
      <h1>{product.name}</h1>
      <p>价格: ¥{product.price}</p>
    </div>
  );
}

组件中没有 loading 状态判断?因为数据路由在 loader 执行期间会自动挂起渲染(利用 React Suspense),并在父路由中统一处理加载态和错误态。你可以在顶层路由中设置 errorElement 和等待指示器。

12.6.4 Action:处理表单提交与数据变更

action<Form> 提交时触发,它是实现数据变更的标准方式。React Router 提供了 <Form> 组件,它会自动触发路由对应的 action,而不会发生浏览器默认的页面跳转。

定义 action:

// actions/productActions.js
export async function addProductAction({ request }) {
  // 从表单数据获取输入
  const formData = await request.formData();
  const productData = Object.fromEntries(formData);

  const res = await fetch('/api/products', {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify(productData),
  });

  if (!res.ok) {
    // 返回错误数据,组件可通过 useActionData 接收
    const errors = await res.json();
    return { errors };
  }

  // 成功后可重定向到产品列表
  return redirect('/products');
}

在页面中使用 <Form>

// pages/NewProduct.jsx
import { Form, useActionData, useNavigation } from 'react-router-dom';

function NewProductPage() {
  const actionData = useActionData();   // action 返回的数据(如校验错误)
  const navigation = useNavigation();
  const isSubmitting = navigation.state === 'submitting';

  return (
    <Form method="post">
      <p>
        <label>产品名称</label>
        <input type="text" name="name" />
        {actionData?.errors?.name && (
          <span style={{ color: 'red' }}>{actionData.errors.name}</span>
        )}
      </p>
      <p>
        <label>价格</label>
        <input type="number" name="price" />
      </p>
      <button type="submit" disabled={isSubmitting}>
        {isSubmitting ? '提交中...' : '创建产品'}
      </button>
    </Form>
  );
}

useNavigation().state 会反映当前 action 的执行状态(submitting),可用来控制按钮禁用和提示文案。

12.6.5 延迟数据加载:defer 与 Await

当 loader 中有部分数据获取较慢时,可以使用 defer 返回一个包含 Promise 的对象,配合 <Await><Suspense> 实现非关键数据的延迟加载,让页面更快呈现。

// 路由 loader
loader: async () => {
  // 快速数据先返回,慢数据放入 Promise
  const userPromise = fetchUser(); // 返回 Promise
  return defer({
    products: await fetchProducts(), // 关键数据,等待完成
    user: userPromise,               // 非关键,延迟加载
  });
}

在组件中:

import { Await, useLoaderData, useAsyncValue } from 'react-router-dom';
import { Suspense } from 'react';

function ProductsPage() {
  const { products, user } = useLoaderData();

  return (
    <div>
      <ProductList products={products} />
      <Suspense fallback={<p>加载用户信息...</p>}>
        <Await resolve={user}>
          <UserInfo />
        </Await>
      </Suspense>
    </div>
  );
}

function UserInfo() {
  const user = useAsyncValue(); // 获取 Await 解析后的数据
  return <div>{user.name}</div>;
}

12.6.6 数据重新验证

当 action 执行后(新增/编辑/删除),React Router 会自动无效化所有已激活路由的 loader 数据并重新加载,以确保 UI 与服务器数据同步。这称为自动重新验证

你可以通过 shouldRevalidate 函数精细控制某个路由是否需要重新加载,避免不必要的请求。

{
  path: 'products',
  loader: productsLoader,
  shouldRevalidate: ({ currentUrl, nextUrl }) => {
    // 只在路径不同时重新验证
    return currentUrl.pathname !== nextUrl.pathname;
  },
}

12.6.7 错误处理与 ErrorElement

每个路由可以定义 errorElement,当 loader 或 action 中抛出异常(或返回错误响应)时,React Router 会自动渲染该错误界面,实现声明式错误边界。

const router = createBrowserRouter([
  {
    path: '/',
    element: <RootLayout />,
    errorElement: <ErrorPage />, // 通用错误处理
    children: [
      {
        path: 'products/:id',
        loader: productLoader,
        errorElement: <ProductError />, // 针对该子路由的错误处理
      },
    ],
  },
]);

错误信息可通过 useRouteError 在错误组件中获取。

12.6.8 与 React Query 等的配合

数据路由内置的数据加载能力适合中等复杂度的场景。当需要更强大的缓存、乐观更新、分页滚动等功能时,可以结合 React Query 或 SWR 使用。通常将 loader 作为初始数据获取渠道,React Query 在客户端接管后续的数据同步。

一个常见模式是在 loader 中预取数据并放入 React Query 缓存:

loader: async () => {
  const queryClient = getQueryClient();
  await queryClient.prefetchQuery(['products'], fetchProducts);
  return null;
}

这样页面组件仍可通过 useQuery 直接消费缓存,兼顾首屏速度和客户端灵活性。

总结

  • loader 负责(GET),自动获取数据;action 负责(POST/PUT/DELETE),处理表单提交。
  • 数据路由让组件脱离了手动请求逻辑,useLoaderDatauseActionData 让数据源变得明确。
  • 通过 deferSuspense 可优化加载体验。
  • 自动重新验证和错误边界让应用更加健壮。
  • 结合外部缓存库可以在复杂场景中保持灵活性。

数据路由模式正在成为 React Router 官方推荐的标准做法,大幅简化了数据流与路由的协调,是构建数据密集型 SPA 的高效手段。