Saltar al contenido principal

useSuspense()

High performance async data rendering without overfetching.

useSuspense() es como await para componentes de React. 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 en un único AsyncBoundary.

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

Uso​

import { useSuspense } from '@data-client/react';
import { ProfileResource } from './ProfileResource';

function ProfileDetail() {
  const profile = useSuspense(ProfileResource.get, { id: 1 });
  return (
    <div className="listItem">
      <Avatar src={profile.avatar} />
      <div>
        <h4>{profile.fullName}</h4>
        <p>{profile.bio}</p>
      </div>
    </div>
  );
}
render(<ProfileDetail />);
Resultado
Store▶

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 Error Boundaries
React Native

Al usar React Navigation, useSuspense() lanzará fetches al recibir el foco si los datos se consideran obsoletos.

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 = useSuspense(TodoResource.get, id ? { id } : null);

Tipos​

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

Ejemplos​

Lista​

import { useSuspense } from '@data-client/react';
import { ProfileResource } from './ProfileResource';

function ProfileList() {
  const profiles = useSuspense(ProfileResource.getList);
  return (
    <div>
      {profiles.map(profile => (
        <div className="listItem" key={profile.pk()}>
          <Avatar src={profile.avatar} />
          <div>
            <h4>{profile.fullName}</h4>
            <p>{profile.bio}</p>
          </div>
        </div>
      ))}
    </div>
  );
}
render(<ProfileList />);
Resultado
Store▶

Paginación​

La paginación reactiva se logra con schemas mutables

import { useSuspense } from '@data-client/react';
import PostItem from './PostItem';
import LoadMore from './LoadMore';
import { PostResource } from './Post';

export default function PostList() {
  const { posts, cursor } = useSuspense(PostResource.getList);
  return (
    <div>
      {posts.map(post => (
        <PostItem key={post.pk()} post={post} />
      ))}
      {cursor ? <LoadMore cursor={cursor} /> : null}
    </div>
  );
}
render(<PostList />);
Resultado
Store▶

Secuencial​

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

import { useSuspense } from '@data-client/react';
import { PostResource, UserResource } from './resources';

function PostWithAuthor({ id }: { id: string }) {
const post = useSuspense(PostResource.get, { id });
const author = useSuspense(UserResource.get, {
id: post.userId,
});
}

Condicional​

null evitará enlazar y obtener los datos

▶Resources
▶PostWithAuthor
import { useSuspense } from '@data-client/react';
import { PostResource, UserResource } from './Resources';

export default function PostWithAuthor({ id }: { id: string }) {
  const post = useSuspense(PostResource.get, { id });
  const author = useSuspense(
    UserResource.get,
    post.userId
      ? {
          id: post.userId,
        }
      : null,
  );
  // author as User | undefined
  if (!author) return;
}

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
import { useSuspense } from '@data-client/react';
import { getPosts } from './api/Post';

export default function ArticleList({ page }: { page: string }) {
  const {
    posts,
    nextPage,
    lastPage,
  } = useSuspense(getPosts, { page });
  return (
    <div>
      {posts.map(post => (
        <div key={post.pk()}>{post.title}</div>
      ))}
    </div>
  );
}

Renderizado del lado del servidor​

El renderizado del lado del servidor permite enviar el HTML en streaming de forma incremental, lo que reduce enormemente el TTFB. La hidratación automática del store del SSR de Reactive Data Client significa interactividad inmediata para el usuario con cero fetches del lado del cliente en la primera carga.

Explora el ejemplo nextjs

More Demos

El uso en los componentes es idéntico, lo que significa que puedes compartir fácilmente componentes entre aplicaciones con y sin SSR, así como migrar a SSR sin necesidad de cambiar el código de data-client.

Modo concurrente​

En React 18, navegar con startTransition permite que los AsyncBoundaries sigan mostrando la pantalla anterior mientras se cargan los nuevos datos. Combinado con el renderizado del lado del servidor con streaming, elimina la necesidad de mostrar indicadores de carga molestos y mejora la experiencia de usuario.

Haz clic en uno de los nombres para navegar a sus tareas. Aquí los estados de carga largos se indican con una barra de carga menos intrusiva, como la que usan YouTube y Robinhood.

Explora el ejemplo todo-app

More Demos

Si necesitas ayuda para añadir esto a tu propio router personalizado, consulta la guía oficial de React