数据获取
@feoe/fs-router
中提供了开箱即用的数据获取能力,开发者可以通过这些 API,在项目中获取数据。帮助开发者更好地管理数据,提升项目的性能。
兼容性
仅支持 React-Router v6+ 的 DataRouter 数据路由模式
什么是 Data Loader
每个路由组件(layout.ts
,page.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-type
是 application/json
,status
为 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 会为你自动生成 routeId
,routeId
的值是对应组件相对于 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.tsx
中 loader
返回的数据,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}
错误用法
loader
中只能返回可序列化的数据,不能返回不可序列化的数据(如函数)。
WARNING
虽然 CSR 下没有这个限制,但强烈推荐遵循该限制。不要引入额外的副作用,loader 应该只做 loader 该做的事情。
1// This won't work!
2export default () => {
3 return {
4 user: {},
5 method: () => {},
6 };
7};
- 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}
- 不能从路由组件中引入
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};