Saltar al contenido principal

useSuspense()

High performance async data rendering without overfetching.

Usa await con useSuspense() en los componentes de Vue. Esto significa que el resto del componente solo se ejecuta después de que los datos se hayan cargado, lo que evita la complejidad de gestionar las condiciones de carga y de error. En su lugar, el manejo de los fallbacks se centraliza con el Suspense integrado de Vue.

useSuspense() reacciona a las mutaciones de los datos y vuelve a renderizar solo cuando es necesario.

Uso​

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

Comportamiento​

La política de caché es Stale-While-Revalidate de forma predeterminada, pero también es configurable.

Estado de caducidadFetchSuspendeErrorCondiciones
Inválidosí1sínono está en el store, eliminación, invalidación, invalidIfStale
Obsoletosí1nono(primer render, cambio de argumentos) & caducidad < ahora
Válidononoquizá2finalización del fetch
nonononull usado como segundo argumento
nota
  1. Los fetches idénticos se deduplican automáticamente
  2. Los errores duros deben ser capturados por onErrorCaptured()
Dependencias condicionales

Usar null como segundo argumento de cualquier hook de Data Client significa "no hacer nada".

// todo could be undefined if id is undefined
const todo = await useSuspense(
TodoResource.get,
computed(() => (id.value ? { id: id.value } : null)),
);

Tipos​

function useSuspense(
endpoint: ReadEndpoint,
...args: MaybeRefsOrGetters<Parameters<typeof endpoint>> | [null]
): Promise<DeepReadonly<ComputedRef<Denormalize<typeof endpoint.schema>>>>;

Los argumentos pueden ser valores simples, refs (incluidos los computed) o funciones getter como () => ({ id: props.id }). Un objeto simple como { id: props.id } se lee una sola vez y no seguirá los cambios de props ni de ruta, así que usa un getter o computed cuando un argumento pueda cambiar.

El resultado se actualiza cuando cambian los argumentos. Mientras se cargan los datos de los nuevos argumentos, el resultado conserva los datos anteriores en lugar de volverse undefined. Si ese fetch falla, al leer el resultado se lanza el error (según su política de errores), de modo que llega a onErrorCaptured().

Ejemplos​

Lista​

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

Paginación​

La paginación reactiva se logra con schemas mutables

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

Secuencial​

Cuando los parámetros del fetch dependen de datos de otro recurso.

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

Condicional​

null evitará enlazar y obtener los datos

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

Datos incrustados​

Cuando las entidades se almacenan en estructuras anidadas, esa estructura se conserva.

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