useSuspense()
High performance async data rendering without overfetching.
在 Vue 组件中 await useSuspense()。这意味着组件的其余部分只会在数据加载完成后才运行,从而免去了处理加载和错误状态的复杂性。fallback 处理则通过 Vue 内置的 Suspense
集中管理。
useSuspense() 会响应数据变更,并且只在必要时重新渲染。
用法
- Rest
- Promise
ProfileResource
ProfileDetail.vue
<script setup lang="ts"> import { useSuspense } from '@data-client/vue'; import { ProfileResource } from './ProfileResource'; const profile = await useSuspense(ProfileResource.get, { id: 1 }); </script> <template> <div class="listItem"> <Avatar :src="profile.avatar" /> <div> <h4>{{ profile.fullName }}</h4> <p>{{ profile.bio }}</p> </div> </div> </template>
Profile
ProfileDetail.vue
<script setup lang="ts"> import { useSuspense } from '@data-client/vue'; import { getProfile } from './Profile'; const profile = await useSuspense(getProfile, 1); </script> <template> <div class="listItem"> <Avatar :src="profile.avatar" /> <div> <h4>{{ profile.fullName }}</h4> <p>{{ profile.bio }}</p> </div> </div> </template>
行为
缓存策略默认为 Stale-While-Revalidate,但也可以配置。
| 过期状态 | 获取 | 挂起 | 错误 | 条件 |
|---|---|---|---|---|
| 无效 | 是1 | 是 | 否 | 不在 store 中、删除、失效、 invalidIfStale |
| 过时 | 是1 | 否 | 否 | (首次渲染、参数变化)且 expiry < now |
| 有效 | 否 | 否 | 可能2 | 获取完成 |
| 否 | 否 | 否 | 第二个参数传入 null |
备注
- 相同的获取请求会自动去重
- 硬错误会被 onErrorCaptured() 捕获
条件依赖
将 null 作为任意 Data Client hook 的第二个参数,表示“什么都不做”。
// todo could be undefined if id is undefined
const todo = await useSuspense(
TodoResource.get,
computed(() => (id.value ? { id: id.value } : null)),
);
类型
function useSuspense(
endpoint: ReadEndpoint,
...args: MaybeRefsOrGetters<Parameters<typeof endpoint>> | [null]
): Promise<DeepReadonly<ComputedRef<Denormalize<typeof endpoint.schema>>>>;
参数可以是普通值、refs(包括 computed),或者像 () => ({ id: props.id }) 这样的 getter
函数。像 { id: props.id } 这样的普通对象只会被读取一次,不会跟随 prop 或路由的变化,因此当参数可能变化时,请使用 getter 或 computed。
参数变化时,结果会随之更新。在新参数对应的数据加载期间,结果会保留之前的数据,而不会变成 undefined。如果这次获取失败,读取结果时会抛出该错误(取决于其错误策略),从而让错误到达
onErrorCaptured()。
示例
列表
ProfileResource
ProfileList.vue
<script setup lang="ts"> import { useSuspense } from '@data-client/vue'; import { ProfileResource } from './ProfileResource'; const profiles = await useSuspense(ProfileResource.getList); </script> <template> <div> <div class="listItem" v-for="profile in profiles" :key="profile.pk()"> <Avatar :src="profile.avatar" /> <div> <h4>{{ profile.fullName }}</h4> <p>{{ profile.bio }}</p> </div> </div> </div> </template>
分页
User
Post
PostItem.vue
LoadMore.vue
PostList.vue
<script setup lang="ts"> import { useSuspense } from '@data-client/vue'; import PostItem from './PostItem.vue'; import LoadMore from './LoadMore.vue'; import { PostResource } from './Post'; const data = await useSuspense(PostResource.getList); </script> <template> <div> <PostItem v-for="post in data.posts" :key="post.pk()" :post="post" /> <LoadMore v-if="data.cursor" :cursor="data.cursor" /> </div> </template>
顺序请求
当获取参数依赖于另一个资源的数据时。
<script setup lang="ts">
import { computed } from 'vue';
import { useSuspense } from '@data-client/vue';
import { PostResource, UserResource } from './Resources';
const props = defineProps<{ id: string }>();
const post = await useSuspense(PostResource.get, () => ({ id: props.id }));
const author = await useSuspense(UserResource.get, () => ({
id: post.value.userId,
}));
</script>
条件请求
传入 null 可以避免绑定和获取数据
▶Resources
▶PostWithAuthor.vue
<script setup lang="ts"> import { computed } from 'vue'; import { useSuspense } from '@data-client/vue'; import { PostResource, UserResource } from './Resources'; const props = defineProps<{ id: string }>(); const post = await useSuspense(PostResource.get, () => ({ id: props.id })); const author = await useSuspense( UserResource.get, computed(() => post.value.userId ? { id: post.value.userId, } : null, ), ); // author as ComputedRef<User | undefined> </script> <template> <div v-if="author"> <!-- render author --> </div> </template>
嵌入数据
当 entity 存储在嵌套结构中时,该结构会被保留。
▶api/Post
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: '', }, });
▶ArticleList.vue
<script setup lang="ts"> import { useSuspense } from '@data-client/vue'; import { getPosts } from './api/Post'; const props = defineProps<{ page: string }>(); const data = await useSuspense(getPosts, () => ({ page: props.page })); </script> <template> <div> <div v-for="post in data.posts" :key="post.pk()">{{ post.title }}</div> </div> </template>