useSuspense()
High performance async data rendering without overfetching.
await useSuspense() in Vue components. This means the remainder of the component only runs after the data has loaded, avoiding the complexity of handling loading and error conditions. Instead, fallback handling is
centralized with Vue's built-in Suspense.
useSuspense() is reactive to data mutations; rerendering only when necessary.
Usage
- Rest
- Promise
<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>
<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>
Behavior
Cache policy is Stale-While-Revalidate by default but also configurable.
| Expiry Status | Fetch | Suspend | Error | Conditions |
|---|---|---|---|---|
| Invalid | yes1 | yes | no | not in store, deletion, invalidation, invalidIfStale |
| Stale | yes1 | no | no | (first-render, arg change) & expiry < now |
| Valid | no | no | maybe2 | fetch completion |
| no | no | no | null used as second argument |
- Identical fetches are automatically deduplicated
- Hard errors to be caught by onErrorCaptured()
:::
Use null as the second argument to any Data Client hook means "do nothing."
// todo could be undefined if id is undefined
const todo = await useSuspense(
TodoResource.get,
computed(() => (id.value ? { id: id.value } : null)),
);
Types
function useSuspense(
endpoint: ReadEndpoint,
...args: MaybeRefsOrGetters<Parameters<typeof endpoint>> | [null]
): Promise<DeepReadonly<ComputedRef<Denormalize<typeof endpoint.schema>>>>;
Arguments can be plain values or refs (including computed); the result updates when they change.
Examples
List
<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>
Pagination
Reactive pagination is achieved with mutable schemas
<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>
Sequential
When fetch parameters depend on data from another resource.
<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>
Conditional
null will avoid binding and fetching data
<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>
Embedded data
When entities are stored in nested structures, that structure will remain.
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: '', }, });
<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>