Skip to main content

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​

<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>

Behavior​

Cache policy is Stale-While-Revalidate by default but also configurable.

Expiry StatusFetchSuspendErrorConditions
Invalidyes1yesnonot in store, deletion, invalidation, invalidIfStale
Staleyes1nono(first-render, arg change) & expiry < now
Validnonomaybe2fetch completion
nonononull used as second argument
note
  1. Identical fetches are automatically deduplicated
  2. Hard errors to be caught by onErrorCaptured()

:::

Conditional Dependencies

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

▶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>

Embedded data​

When entities are stored in nested structures, that structure will remain.

▶api/Post
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>