useSuspense()
High performance async data rendering without overfetching.
useSuspense() 就像是 React 组件的 await。这意味着组件的其余部分只会在数据加载完成后才运行,从而免去了处理加载和错误状态的复杂性。fallback 处理则通过单个 AsyncBoundary
集中管理。
useSuspense() 会响应数据变更,并且只在必要时重新渲染。
用法
- Rest
- Promise
import { useSuspense } from '@data-client/react'; import { ProfileResource } from './ProfileResource'; function ProfileDetail() { const profile = useSuspense(ProfileResource.get, { id: 1 }); return ( <div className="listItem"> <Avatar src={profile.avatar} /> <div> <h4>{profile.fullName}</h4> <p>{profile.bio}</p> </div> </div> ); } render(<ProfileDetail />);
import { useSuspense } from '@data-client/react'; import { getProfile } from './Profile'; function ProfileDetail() { const profile = useSuspense(getProfile, 1); return ( <div className="listItem"> <Avatar src={profile.avatar} /> <div> <h4>{profile.fullName}</h4> <p>{profile.bio}</p> </div> </div> ); } render(<ProfileDetail />);
行为
缓存策略默认为 Stale-While-Revalidate,但也可以配置。
| 过期状态 | 获取 | 挂起 | 错误 | 条件 |
|---|---|---|---|---|
| 无效 | 是1 | 是 | 否 | 不在 store 中、删除、失效、 invalidIfStale |
| 过时 | 是1 | 否 | 否 | (首次渲染、参数变化)且 expiry < now |
| 有效 | 否 | 否 | 可能2 | 获取完成 |
| 否 | 否 | 否 | 第二个参数传入 null |
- 相同的获取请求会自动去重
- 硬错误会被 Error Boundary 捕获
使用 React Navigation 时,如果数据被视为过时,useSuspense() 会在获得焦点时触发获取。
将 null 作为任意 Data Client hook 的第二个参数,表示“什么都不做”。
// todo could be undefined if id is undefined
const todo = useSuspense(TodoResource.get, id ? { id } : null);
类型
- 类型
- 使用泛型
function useSuspense(
endpoint: ReadEndpoint,
...args: Parameters<typeof endpoint> | [null]
): Denormalize<typeof endpoint.schema>;
function useSuspense<
E extends EndpointInterface<
FetchFunction,
Schema | undefined,
undefined
>,
Args extends readonly [...Parameters<E>] | readonly [null],
>(
endpoint: E,
...args: Args
): E['schema'] extends Exclude<Schema, null>
? Denormalize<E['schema']>
: ReturnType<E>;
示例
列表
import { useSuspense } from '@data-client/react'; import { ProfileResource } from './ProfileResource'; function ProfileList() { const profiles = useSuspense(ProfileResource.getList); return ( <div> {profiles.map(profile => ( <div className="listItem" key={profile.pk()}> <Avatar src={profile.avatar} /> <div> <h4>{profile.fullName}</h4> <p>{profile.bio}</p> </div> </div> ))} </div> ); } render(<ProfileList />);
分页
import { useSuspense } from '@data-client/react'; import PostItem from './PostItem'; import LoadMore from './LoadMore'; import { PostResource } from './Post'; export default function PostList() { const { posts, cursor } = useSuspense(PostResource.getList); return ( <div> {posts.map(post => ( <PostItem key={post.pk()} post={post} /> ))} {cursor ? <LoadMore cursor={cursor} /> : null} </div> ); } render(<PostList />);
顺序请求
当获取参数依赖于另一个资源的数据时。
import { useSuspense } from '@data-client/react';
import { PostResource, UserResource } from './resources';
function PostWithAuthor({ id }: { id: string }) {
const post = useSuspense(PostResource.get, { id });
const author = useSuspense(UserResource.get, {
id: post.userId,
});
}
条件请求
传入 null 可以避免绑定和获取数据
import { useSuspense } from '@data-client/react'; import { PostResource, UserResource } from './Resources'; export default function PostWithAuthor({ id }: { id: string }) { const post = useSuspense(PostResource.get, { id }); const author = useSuspense( UserResource.get, post.userId ? { id: post.userId, } : null, ); // author as User | undefined if (!author) return; }
嵌入数据
当 entity 存储在嵌套结构中时,该结构会被保留。
import { Entity, RestEndpoint, Collection } from '@data-client/rest'; export class PaginatedPost extends Entity { id = ''; title = ''; content = ''; static key = 'PaginatedPost'; } export const getPosts = new RestEndpoint({ path: '/post', searchParams: { page: '' }, schema: { posts: new Collection([PaginatedPost]), nextPage: '', lastPage: '', }, });
import { useSuspense } from '@data-client/react'; import { getPosts } from './api/Post'; export default function ArticleList({ page }: { page: string }) { const { posts, nextPage, lastPage, } = useSuspense(getPosts, { page }); return ( <div> {posts.map(post => ( <div key={post.pk()}>{post.title}</div> ))} </div> ); }
服务端渲染
服务端渲染可以增量地流式传输 HTML,大幅降低 TTFB。Reactive Data Client SSR 会自动完成 store 注水,这意味着用户可以立即交互,且首次加载时客户端获取请求为零。
探索 nextjs 示例
组件中的用法完全相同,这意味着你可以轻松地在 SSR 和非 SSR 应用之间共享组件,也可以迁移到 SSR 而无需修改 data-client 相关代码。
并发模式
在 React 18 中,使用 startTransition 进行导航,可以让 AsyncBoundary 在新数据加载期间继续显示之前的画面。结合流式服务端渲染,就不再需要闪现恼人的加载指示器,从而改善用户体验。
点击其中一个名字,即可导航到该用户的待办事项。这里较长的加载状态由干扰更少的加载条 来提示,就像 YouTube 和 Robinhood 所做的那样。
探索 todo-app 示例
如果你需要在自己的自定义路由中加入这一功能,请查看 React 官方指南