Pular para o conteúdo principal

useSuspense()

High performance async data rendering without overfetching.

useSuspense() é como o await para componentes React. Isso significa que o restante do componente só é executado depois que os dados foram carregados, evitando a complexidade de tratar condições de carregamento e de erro. Em vez disso, o tratamento de fallback é centralizado em um único AsyncBoundary.

useSuspense() reage às mutações de dados, renderizando novamente somente quando necessário.

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▶

Comportamento​

A política de cache é Stale-While-Revalidate por padrão, mas também configurável.

Status de expiraçãoFetchSuspendErrorCondições
Inválidosim1simnãonão está na store, exclusão, invalidação, invalidIfStale
Desatualizadosim1nãonão(primeira renderização, mudança de args) & expiração < agora
Válidonãonãotalvez2conclusão do fetch
nãonãonãonull usado como segundo argumento
observação
  1. Fetches idênticos são automaticamente deduplicados
  2. Erros hard devem ser capturados por Error Boundaries
React Native

Ao usar o React Navigation, useSuspense() dispara fetches ao receber foco se os dados forem considerados desatualizados.

Dependências condicionais

Usar null como segundo argumento de qualquer hook do Data Client significa "não fazer 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>;

Exemplos​

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▶

Paginação​

A paginação reativa é obtida com schemas mutáveis

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▶

Sequencial​

Quando os parâmetros do fetch dependem de dados de outro resource.

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 evita vincular e buscar os dados

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

Dados incorporados​

Quando as entidades são armazenadas em estruturas aninhadas, essa estrutura é mantida.

▶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>
  );
}

Renderização no servidor​

A renderização no servidor (Server Side Rendering) transmite o HTML de forma incremental, reduzindo bastante o TTFB. A hidratação automática da store do SSR do Reactive Data Client significa interatividade imediata para o usuário, com zero fetches no cliente no primeiro carregamento.

Explore o exemplo nextjs

More Demos

O uso nos componentes é idêntico, o que significa que você pode compartilhar componentes com facilidade entre aplicações com e sem SSR, além de migrar para SSR sem precisar alterar o código do data-client.

Modo concorrente​

No React 18, navegar com startTransition permite que os AsyncBoundaries continuem exibindo a tela anterior enquanto os novos dados carregam. Combinado com a renderização no servidor com streaming, isso elimina a necessidade de exibir indicadores de carregamento irritantes, melhorando a experiência do usuário.

Clique em um dos nomes para navegar até as tarefas dessa pessoa. Aqui, estados de carregamento longos são indicados pela barra de carregamento, menos intrusiva, como a usada pelo YouTube e pelo Robinhood.

Explore o exemplo todo-app

More Demos

Se precisar de ajuda para adicionar isso ao seu próprio roteador personalizado, consulte o guia oficial do React