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
- 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>
Comportamiento
La política de caché es Stale-While-Revalidate de forma predeterminada, pero también es configurable.
| Estado de caducidad | Fetch | Suspende | Error | Condiciones |
|---|---|---|---|---|
| Inválido | sí1 | sí | no | no está en el store, eliminación, invalidación, invalidIfStale |
| Obsoleto | sí1 | no | no | (primer render, cambio de argumentos) & caducidad < ahora |
| Válido | no | no | quizá2 | finalización del fetch |
| no | no | no | null usado como segundo argumento |
- Los fetches idénticos se deduplican automáticamente
- Los errores duros deben ser capturados por onErrorCaptured()
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
<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.
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: '', }, });
<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>