React Router v6 数据加载

FreeGuideOnline 最新 2026-07-13

bash npm install react-router-dom@6


创建一个使用 `createBrowserRouter` 和 `RouterProvider` 的基本路由结构:

```jsx
// main.jsx
import { createBrowserRouter, RouterProvider } from 'react-router-dom';
import HomePage from './pages/Home';
import PostPage from './pages/Post';
import RootLayout from './layouts/Root';

const router = createBrowserRouter([
  {
    path: '/',
    element: <RootLayout />,
    children: [
      { index: true, element: <HomePage /> },
      { path: 'posts/:postId', element: <PostPage /> }
    ]
  }
]);

function App() {
  return <RouterProvider router={router} />;
}

export default App;

3. 定义数据加载器(Loader)

3.1 为路由添加 loader 函数

每个路由都可以定义一个 loader 函数,该函数在路由被匹配时执行,并返回组件需要的数据。

// 在路由定义中注入 loader
{
  path: 'posts/:postId',
  element: <PostPage />,
  loader: async ({ params }) => {
    const response = await fetch(`/api/posts/${params.postId}`);
    if (!response.ok) throw new Error('文章加载失败');
    return response.json();
  }
}
  • loader 接收一个对象参数,包含 params(路径参数)、request
  • 可以执行任意异步操作(fetch、数据库查询等)
  • 返回的数据会被自动传递给组件

3.2 在组件中获取数据:useLoaderData

在渲染组件内部,使用 useLoaderData 钩子直接获取 loader 返回的数据。

// pages/Post.jsx
import { useLoaderData } from 'react-router-dom';

export default function PostPage() {
  const post = useLoaderData(); // 即 loader 返回的数据
  return (
    <article>
      <h1>{post.title}</h1>
      <p>{post.content}</p>
    </article>
  );
}

从此你不再需要在组件中用 useStateuseEffect 管理请求状态,数据已经同步就绪。

4. 错误处理

4.1 路由级错误边界:errorElement

当 loader 抛出错误时,React Router 会自动渲染当前路由(或父路由)配置的 errorElement,实现类似错误边界的效果。

{
  path: 'posts/:postId',
  element: <PostPage />,
  loader: postLoader,
  errorElement: <PostErrorPage />
}

4.2 获取错误信息:useRouteError

在错误元素中,通过 useRouteError 获取被抛出的错误对象。

import { useRouteError, isRouteErrorResponse } from 'react-router-dom';

function PostErrorPage() {
  const error = useRouteError();

  if (isRouteErrorResponse(error)) {
    if (error.status === 404) {
      return <p>文章未找到</p>;
    }
    return <p>服务器错误 {error.status}</p>;
  }
  return <p>未知错误发生</p>;
}

这样你能优雅地处理 404、500 等情况,而不会导致整个页面崩溃。

5. 延迟数据加载与 Suspense

5.1 为什么要延迟加载?

有些接口响应很快,有些却很慢。如果所有数据都等待完毕才渲染页面,慢接口会阻塞整个页面展示。React Router v6 支持使用 defer 延迟部分非关键数据的加载,并配合 React 的 <Suspense><Await> 实现渐进式渲染。

5.2 使用 defer 返回部分数据

在 loader 中,用 defer 包装返回值,可以将某些数据标记为“延迟”。

import { defer } from 'react-router-dom';

const loader = async () => {
  // 关键数据:立即加载
  const userPromise = fetchUser(); // 但假设它也很慢
  // 非关键数据:延迟加载
  const commentsPromise = fetchComments();

  return defer({
    user: userPromise,       // 就算慢,也需要等
    comments: commentsPromise // 可以用 Suspense 分别处理
  });
};

或者更典型的场景:一部分数据必须等待,另一部分可以稍后展示。

const loader = async ({ params }) => {
  const post = await fetchPost(params.postId); // 核心内容必须加载完毕
  const relatedPosts = fetchRelatedPosts(); // 可以稍后

  return defer({ post, relatedPosts });
};

5.3 在组件中使用 Await 和 Suspense

组件内用 <Await> 组件包裹延迟数据,并用 <Suspense> 提供 fallback。

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

export default function PostPage() {
  const { post, relatedPosts } = useLoaderData();

  return (
    <div>
      <article>{post.title} - {post.content}</article>

      <Suspense fallback={<p>加载推荐文章...</p>}>
        <Await resolve={relatedPosts}>
          {(posts) => (
            <ul>
              {posts.map(p => <li key={p.id}>{p.title}</li>)}
            </ul>
          )}
        </Await>
      </Suspense>
    </div>
  );
}
  • Awaitresolve 属性接收一个 Promise,当 Promise 状态变为 resolved 后渲染子组件
  • 多个延迟数据可以搭配多个 Await,每个都可拥有独立的 Suspense 边界,实现细粒度的加载控制

6. 数据加载的并行与请求瀑布

React Router v6 的 loader 会自动并行执行同级路由的 loader,但父子路由的 loader 默认是并行的(除非一个依赖另一个)。不过你可以通过 paramsrequest 来手动优化。

示例:同时加载用户和用户帖子
两个 loader 是并行的,无需担心瀑布流。

{
  path: 'users/:userId',
  loader: userLoader,
  children: [
    {
      path: 'posts',
      loader: postsLoader
    }
  ]
}

7. 表单操作与数据提交(Action)

虽然本教程聚焦数据加载,但路由还支持 action 来处理表单提交、数据变更。它与 loader 配合,形成完整的数据流闭环。简单示例:

{
  path: 'new-post',
  element: <NewPostPage />,
  action: async ({ request }) => {
    const formData = await request.formData();
    const post = Object.fromEntries(formData);
    await fetch('/api/posts', { method: 'POST', body: JSON.stringify(post) });
    return redirect('/');
  }
}