从 React Router v6.4 开始,引入了数据路由(Data Router)模式,核心思想是将数据获取与导航动作深度集成到路由定义中,通过 loader 和 action 两个关键 API,实现在路由切换时自动加载数据、处理表单提交,并自动处理加载状态、错误边界和数据重新验证。
这种模式让组件无需自行管理请求时机和 loading 状态,而是直接从路由提供的 API 中消费数据,组件职责更加纯粹——只负责渲染。
12.6.1 数据路由的核心概念
数据路由的核心结构基于 createBrowserRouter,路由定义中新增了两个关键属性:
loader:在导航到该路由时自动调用的函数,用于获取页面所需数据。数据返回后,组件可通过useLoaderData获取。action:当该路由上发生表单提交(如<Form>或useSubmit)时调用的函数,用于处理数据变更(新增、编辑、删除)。组件可通过useActionData获取 action 返回的结果。
这种设计将数据流与路由绑定,实现了声明式数据加载,同时 React Router 内部自动处理了竞态条件、重新验证和错误边界。
12.6.2 定义路由:从 createBrowserRouter 开始
首先,使用 createBrowserRouter 创建路由实例,并在每个路由中定义 loader 和 action。
// 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),处理表单提交。
- 数据路由让组件脱离了手动请求逻辑,
useLoaderData和useActionData让数据源变得明确。 - 通过
defer和Suspense可优化加载体验。 - 自动重新验证和错误边界让应用更加健壮。
- 结合外部缓存库可以在复杂场景中保持灵活性。
数据路由模式正在成为 React Router 官方推荐的标准做法,大幅简化了数据流与路由的协调,是构建数据密集型 SPA 的高效手段。