数据获取

@feoe/fs-router 中提供了开箱即用的数据获取能力,开发者可以通过这些 API,在项目中获取数据。帮助开发者更好地管理数据,提升项目的性能。

兼容性

仅支持 React-Router v6+ 的 DataRouter 数据路由模式

什么是 Data Loader

每个路由组件(layout.tspage.ts$.tsx)都可以有一个同名的 .data 文件。这些文件可以导出一个 loader 函数,我们称为 Data Loader,它会在对应的路由组件渲染之前执行,为组件提供数据。如下面示例:

1.
2└── routes
3    ├── layout.tsx
4    └── user
5        ├── layout.tsx
6        ├── layout.data.ts
7        ├── page.tsx
8        └── page.data.ts

routes/user/page.data.ts 文件中,可以导出一个 loader 函数:

routes/user/page.data.ts
1export type ProfileData = {
2  /*  some types */
3};
4
5export const loader = async (): Promise<ProfileData> => {
6  const res = await fetch('https://api/user/profile');
7  return await res.json();
8};
兼容性
  • 在之前的版本中,Data Loader 是定义在 .loader 文件中的。当前版本中,我们推荐定义在 .data 文件中,同时我们会保持对 .loader 文件的兼容。
  • .loader 文件中,Data Loader 可以默认导出。但在 data 文件中,Data Loader 需要以 loader 具名导出。
1// xxx.loader.ts
2export default () => {}
3
4// xxx.data.ts
5export const loader = () => {}

在路由组件中,你可以通过 useLoaderData 函数获取数据:

routes/user/page.tsx
1import { useLoaderData } from 'react-router-dom';
2import type { ProfileData } from './page.data.ts';
3
4export default function UserPage() {
5  const profileData = useLoaderData() as ProfileData;
6  return <div>{profileData}</div>;
7}
CAUTION

路由组件和 .data 文件共享类型,要使用 import type 语法,避免引入预期之外的副作用。

在 CSR 项目中,loader 函数会在客户端执行,loader 函数内可以使用浏览器的 API(但通常不需要,也不推荐)。

当在浏览器端导航时,基于约定式路由,react-router 能够支持所有的 loader 函数并行执行(请求)。即当访问 /user/profile 时,/user/user/profile 下的 loader 函数都会并行执行(请求),这种方式解决了部分请求、渲染瀑布流的问题,较大的提升了页面性能

loader 函数

loader 函数有两个入参,分别用于获取路由参数和请求信息。

params

params 是当路由为动态路由时的动态路由片段,会作为参数传入 loader 函数:

routes/user/[id]/page.data.ts
1import { LoaderFunctionArgs } from 'react-router-dom';
2
3// 访问 /user/123 时,函数的参数为 `{ params: { id: '123' } }`
4export const loader = async ({ params }: LoaderFunctionArgs) => {
5  const { id } = params;
6  const res = await fetch(`https://api/user/${id}`);
7  return res.json();
8};

request

request 是一个 Fetch Request 实例。一个常见的使用场景是通过 request 获取查询参数:

1import { LoaderFunctionArgs } from 'react-router-dom';
2
3export const loader = async ({ request }: LoaderFunctionArgs) => {
4  const url = new URL(request.url);
5  const userId = url.searchParams.get('id');
6  return queryUser(userId);
7};

返回值

loader 函数的返回值只能是两种数据结构之一,可序列化的数据对象或者 Fetch Response 实例。

1const loader = async (): Promise<ProfileData> => {
2  return {
3    message: 'hello world',
4  };
5};
6export default loader;

默认情况下,loader 返回的响应 Content-typeapplication/jsonstatus 为 200,你可以通过自定义 Response 来设置:

1const loader = async (): Promise<ProfileData> => {
2  const data = { message: 'hello world' };
3  return new Response(JSON.stringify(data), {
4    status: 200,
5    headers: {
6      'Content-Type': 'application/json; utf-8',
7    },
8  });
9};

错误处理

基本用法

loader 函数中,可以通过 throw error 或者 throw response 的方式处理错误,当 loader 函数中有错误被抛出时,react-router 会停止执行当前 loader 中的代码,并将前端 UI 切换到定义的 ErrorBoundary 组件:

1// routes/user/profile/page.data.ts
2export async function loader() {
3  const res = await fetch('https://api/user/profile');
4  if (!res.ok) {
5    throw res;
6  }
7  return res.json();
8}
9
10// routes/user/profile/error.tsx
11import { useRouteError } from 'react-router-dom';
12const ErrorBoundary = () => {
13  const error = useRouteError() as Response;
14  return (
15    <div>
16      <h1>{error.status}</h1>
17      <h2>{error.statusText}</h2>
18    </div>
19  );
20};
21
22export default ErrorBoundary;

修改 HTTP 状态码

在 SSR 项目中你可以通过在 loader 函数中 throw response 的方式,控制页面的状态码,展示对应的 UI。

如以下示例,页面的状态码将与这个 response 保持一致,页面也会展示为 ErrorBoundary 的 UI:

1// routes/user/profile/page.data.ts
2export async function loader() {
3  const user = await fetchUser();
4  if(!user){
5    throw new Response('The user was not found', { status: 404 });
6  }
7  return user;
8}
9
10// routes/error.tsx
11import { useRouteError } from 'react-router-dom';
12const ErrorBoundary = () => {
13  const error = useRouteError() as { data: string };
14  return <div className="error">{error.data}</div>;
15};
16
17export default ErrorBoundary;

获取上层组件的数据

很多场景下,子组件需要获取到上层组件 loader 中的数据,你可以通过 useRouteLoaderData 方便地获取到上层组件的数据:

1// routes/user/profile/page.tsx
2import { useRouteLoaderData } from 'react-router-dom';
3
4export function UserLayout() {
5  // 获取 routes/user/layout.data.ts 中 `loader` 返回的数据
6  const data = useRouteLoaderData('user/layout');
7  return (
8    <div>
9      <h1>{data.name}</h1>
10      <h2>{data.age}</h2>
11    </div>
12  );
13}

userRouteLoaderData 接受一个参数 routeId。在使用约定式路由时,react-router 会为你自动生成 routeIdrouteId 的值是对应组件相对于 src/routes 的路径,如上面的例子中,子组件想要获取 routes/user/layout.tsx 中 loader 返回的数据,routeId 的值就是 user/layout

在多入口场景下,routeId 的值需要加上对应入口的名称,入口名称非指定情况下一般是入口的目录名,如以下目录结构:

1.
2└── src
3    ├── entry1
4    │     └── routes
5    │           └── layout.tsx
6    └── entry2
7          └── routes
8                └── layout.tsx

如果想获取 entry1/routes/layout.tsxloader 返回的数据,routeId 的值就是 entry1_layout

Loading UI

创建 user/layout.data.ts,并添加以下代码:

routes/user/layout.data.ts
1import { defer } from 'react-router-dom';
2
3export const loader = () =>
4  defer({
5    userInfo: new Promise(resolve => {
6      setTimeout(() => {
7        resolve({
8          age: 1,
9          name: 'user layout',
10        });
11      }, 1000);
12    }),
13  });

user/loading.tsx 中添加以下代码:

routes/user/loading.tsx
1import { useRouteLoaderData } from 'react-router-dom';
2
3export default function UserLoading() {
4  return (
5    <LoadingSkeleton>Loading user profile</LoadingSkeleton>
6  );
7}

user/error.tsx 中添加以下代码:

routes/user/loading.tsx
1import { useRouteLoaderData } from 'react-router-dom';
2
3export default function UserErrorBoundary() {
4  const error = useRouteError()
5
6  return <Error message={error.message}>;
7}

渲染结果

user/layout.tsx 最终的渲染结果:

routes/user/layout.tsx
1import { Await, defer, useLoaderData, Outlet } from 'react-router-dom';
2
3export default function UserLayout() {
4  const { userInfo } = useLoaderData() as { userInfo: Promise<UserInfo> };
5
6  return (
7    <UserErrorBoundary>
8      <Suspense fallback={<UserLoading />}>
9        <Await
10          resolve={userInfo}
11          children={userInfo => (
12            <div>
13              <span>{userInfo.name}</span>
14              <span>{userInfo.age}</span>
15              <Outlet />
16            </div>
17          )}
18        ></Await>
19      </Suspense>
20    </UserErrorBoundary>
21  );
22}
TIP

<Await> 组件的具体用法请查看 Awaitdefer 的具体用法请查看 defer

错误用法

  1. loader 中只能返回可序列化的数据,不能返回不可序列化的数据(如函数)。
WARNING

虽然 CSR 下没有这个限制,但强烈推荐遵循该限制。不要引入额外的副作用,loader 应该只做 loader 该做的事情。

1// This won't work!
2export default () => {
3  return {
4    user: {},
5    method: () => {},
6  };
7};
  1. react-router 会帮你调用 loader 函数,你不应该自己调用 loader 函数:
1// This won't work!
2export const loader = async () => {
3  const res = fetch('https://api/user/profile');
4  return res.json();
5};
6
7import { loader } from './page.data.ts';
8
9export default function RouteComp() {
10  const data = loader();
11}
  1. 不能从路由组件中引入 loader 文件,也不能从 loader 文件引入路由组件中的变量,如果需要共享类型的话,应该使用 import type
1// Not allowed
2// routes/layout.tsx
3import { useLoaderData } from 'react-router-dom';
4import { ProfileData } from './page.data.ts'; // should use "import type" instead
5
6export const fetch = wrapFetch(fetch);
7
8export default function UserPage() {
9  const profileData = useLoaderData() as ProfileData;
10  return <div>{profileData}</div>;
11}
12
13// routes/layout.data.ts
14import { fetch } from './layout.tsx'; // should not be imported from the routing component
15export type ProfileData = {
16  /*  some types */
17};
18
19export const loader = async (): Promise<ProfileData> => {
20  const res = await fetch('https://api/user/profile');
21  return await res.json();
22};