QueryBoundary
Com dados
Últimas notas
- Clínica São LucasPaga
- Ótica CentralAberta
Carregando
Últimas notas
Carregando com o molde da tela
Últimas notas
Erro
Não foi possível carregar as notas
A prefeitura não respondeu. Tente de novo em alguns minutos.
Vazio
Nenhuma nota por aqui
Quando você emitir a primeira, ela aparece nesta lista.
Resposta que não é lista
Nenhuma nota no período
Amplie o intervalo de datas para ver notas mais antigas.
Os quatro finais
Uma folha, e não uma lista
- Cliente
- Clínica São Lucas
- CNPJ
- 12.345.678/0001-90
- Cidade
- João Pessoa, PB
import { QueryBoundary } from '@rivocode/ui'Quando usar
Os quatro finais de uma consulta (carregando, erro, vazio e dados) em volta de qualquer conteúdo.
O DataTable e o ChartContainer já os têm embutidos, e o resto da tela não:
cada cartão de resumo, cada folha de detalhes e cada lista desenhada à mão
reescreve a mesma escada de if. Esta peça é essa escada, com os mesmos nomes
de prop das duas: isLoading, isError, onRetry, errorTitle,
errorMessage e empty.
<QueryBoundary
data={query.data}
isLoading={query.isLoading}
isError={query.isError}
onRetry={query.refetch}
empty={{
title: 'Nenhuma nota por aqui',
description: 'Quando você emitir a primeira, ela aparece nesta lista.',
}}
>
{(invoices) => <Invoices invoices={invoices} />}
</QueryBoundary>
Não conhece React Query, e isso é de propósito: entram três booleanos e uma
resposta, e funciona igual com fetch na mão, com SWR ou com server component.
A ordem é a mesma das duas irmãs: erro vence carregando, e vazio só vale depois que a resposta chegou. Sem isso, uma nova busca sobre um erro pisca "nenhum resultado" antes de mostrar o problema.
O filho pode ser função
Como função, ela só é chamada depois que o dado chegou, e recebe o data sem o
undefined, que é exatamente o ! que toda tela escrevia aqui. Como nó,
serve a quem não precisa do dado para desenhar:
<QueryBoundary data={customer}>
{(customer) => <DescriptionItem label="Cliente">{customer.name}</DescriptionItem>}
</QueryBoundary>
Com filho em função, data indefinido continua sendo espera mesmo com
isLoading={false}. Não há o que entregar à função, e é a regra do
DataTable. Com filho em nó, isLoading manda sozinho quando você o passa: o
nó não depende do dado para existir.
A função não atravessa a fronteira de um server component: ela não é serializável. De um server component, passe o filho como nó.
Os filhos saem como você os escreveu, sem embrulho. A peça não põe uma
<div> em volta do que ela devolve: uma moldura invisível quebraria o grid
ou o flex de quem está por fora, e o defeito só apareceria no navegador.
A espera
O padrão são três linhas de Skeleton, e elas seguram altura sem prometer
forma nenhuma. skeletonRows muda quantas, o mesmo nome do DataTable.
Quando a forma do que vem importa (e ela quase sempre importa), passe o seu
molde em skeleton:
<QueryBoundary
isLoading={query.isLoading}
skeleton={
<div className="flex flex-col gap-2">
<Skeleton className="h-4 w-40" />
<Skeleton className="h-4 w-24" />
</div>
}
>
Não é Spinner de propósito, e é a mesma escolha das duas irmãs: o giro no meio
do vazio não reserva altura, então a página pula quando o conteúdo chega. O
Spinner continua sendo o certo para a espera que não tem forma: o botão que
envia, a ação que não desenha nada.
O nó do carregando sai com aria-busy="true". O Skeleton se esconde do leitor
de tela de propósito, e é no contêiner que o aviso de carregamento pertence.
A espera se anuncia em voz alta. aria-busy num nó sem papel não é lido por
leitor de tela nenhum: ele descreve o estado de uma região, e só chega a quem já
está dentro dela. Quem esperava ouvia silêncio, e a chegada do dado, que troca a
tela inteira, também não dizia nada. As quatro irmãs publicam a mesma região viva
(role="status" aria-live="polite", marcada com data-rc-status), que diz
"Carregando…" enquanto a consulta não volta e "Conteúdo carregado" quando ela
volta. Ela existe antes de o texto mudar e é o mesmo nó do primeiro ao último
estado: região que nasce já com o texto dentro não dispara anúncio nenhum.
O que ela nao trata: dado velho enquanto revalida
isLoading e isError descrevem duas situacoes, e o caso mais comum de tela
real e uma terceira: ja ha dado na tela e uma nova busca esta correndo. Se
voce passar isFetching em isLoading, o esqueleto cobre o que a pessoa
estava lendo; se nao passar nada, a atualizacao acontece sem sinal nenhum.
A peca nao resolve isso hoje, e a escolha e sua:
<QueryBoundary isLoading={query.isLoading} isError={query.isError} data={query.data}>
{(rows) => (
<div aria-busy={query.isFetching}>
<DataTable rows={rows} />
</div>
)}
</QueryBoundary>
isLoading do TanStack Query e verdadeiro so na primeira busca, que e o que a
peca espera. isFetching e verdadeiro em toda busca, inclusive a que revalida
- entao ele nao serve para
isLoading, e serve paraaria-busy.
Quem decide o vazio
A peça decide sozinha, pelo data: lista de tamanho zero e null são
vazio, e undefined é "ainda não chegou". Sem isLoading, é ele quem liga o
carregando.
Para a resposta que não é uma lista ({ items: [], total: 0 } é a paginada de
sempre), quem responde é isEmpty, que vence a contagem quando vem:
<QueryBoundary data={page} isEmpty={page.total === 0} empty={{ ... }}>
Se empty chegar sem que nada consiga decidir, a peça avisa no console em
desenvolvimento. O estado vazio que nunca aparece é silencioso: os filhos
desenham sobre o nada, e ninguém descobre até um cliente abrir a tela sem dados.
Sem empty, não há estado vazio. A resposta vazia cai nos filhos, e eles
desenham o vazio deles. A exceção é o null com filho em função: sem dado para
entregar e sem vazio configurado, a peça não desenha nada.
A descrição é obrigatória pelo mesmo motivo do DataTable: "nenhum resultado"
transfere para a pessoa o trabalho de descobrir por quê, e ela quase nunca
descobre.
Os textos que a peça escreve
errorTitle (padrão "Não foi possível carregar") e errorMessage (padrão
"Tente de novo em alguns minutos.") são o par do estado de erro, com os mesmos
nomes e o mesmo papel que têm no DataTable e no ChartContainer: uma tela que
carrega três blocos precisa dizer qual deles falhou, e um produto que não fala
português precisa dizer isso em outra língua.
retryLabel (padrão "Tentar de novo") é o nome do botão que executa o
onRetry, e existe pelo mesmo motivo: sem ele, a tela traduzida saía com o
título em inglês e o botão em português, que é pior do que tudo em português. O
nome e o padrão são os mesmos nas quatro peças de consulta.
Sem onRetry não há botão de nova tentativa. Aviso com botão que não leva a
lugar nenhum é pior que aviso sem botão.
Movimento
O Alert do erro e o EmptyState do vazio entram pelo movimento deles. O conteúdo que você entrega não ganha entrada daqui: a moldura não embrulha os seus filhos numa caixa, porque uma caixa a mais muda o layout de quem usa (o filho que era flex-1, o item de grade). Quem tem caixa própria entra sozinho: o DataTable esmaece o corpo, e o gráfico se desenha.
Partes
classNames veste cada final: loading, error, empty. O className veste
os três de uma vez, que é onde mora a moldura que reserva a altura
(className="min-h-40"). E não veste os filhos, que são seus.
Quando não usar
Não embrulhe DataTable nem ChartContainer. As duas já recebem os quatro
finais, com estas mesmas props, e desenham a espera no formato do que elas
mesmas mostram: linhas falsas de tabela, barras falsas de gráfico. Por fora,
uma moldura só teria um esqueleto genérico para oferecer, e os dois estados de
erro empilhados apareceriam juntos no dia em que a consulta falhasse.
Para o estado vazio sozinho (uma tela que nunca carrega nada, um resultado que
já está na mão), use EmptyState. Para o aviso de erro que não é o fim de uma
consulta, Alert. Para a marca de lugar solta dentro de um bloco que já tem os
outros finais tratados, Skeleton. Esta peça existe para os quatro juntos, e na
ordem; um só deles não paga o embrulho.
Ela também não captura exceção de renderização: QueryBoundary mostra o erro
que a consulta reportou em isError, e não o que estourou dentro dos filhos.
Para esse, o que existe é o error boundary do React.
No React Native
Traduz com os mesmos nomes de prop e a mesma ordem: erro vence carregando, e vazio só vale depois que a resposta chegou. O children também aceita função aqui, que é o que justifica a peça existir: ela entrega o dado já sem undefined, e mata o ! que a tela escrevia.
Cinco diferenças de tipo, todas porque texto no nativo mora dentro de um Text: errorTitle, errorMessage, retryLabel, empty.title e empty.description são string. O empty.icon atravessa, e aceita também a função do EmptyState nativo, que entrega a cor e o tamanho. É a mesma nota que o ChartContainer já carrega.
classNames não porta, e a razão não é preguiça: a prop existe no web para que ninguém alcance o nó interno por [&_div] e acople a tela à árvore da peça. No React Native não há seletor de descendente, então essa escotilha não existe e a prop não teria o que evitar. O className veste os três finais, como no web.
O esqueleto genérico fica na peça, e não vem de quem chama: sem ele, isLoading sem skeleton colapsaria a tela para altura zero e ela pularia quando o dado chegasse. No celular isso dói mais, porque não há barra de rolagem nem indicador de rede para explicar a espera.
API
| Prop | Tipo |
|---|---|
classNamesClasse por parte: `loading`, `error`, `empty`. | Partial<Record<"empty""error""loading", string>> |
dataA resposta da consulta. | Data |
emptyO que aparece quando a consulta volta vazia. | { title: ReactNode; description: ReactNode; action?: ReactNode; icon?: ReactNode; } |
errorMessage | ReactNode |
errorTitleO titulo do aviso de erro. | ReactNode |
isEmptyDiz o vazio no lugar do `data`, para a resposta que nao e uma lista: `isEmpty={page.total === 0}`. | boolean |
isError | boolean |
isLoading | boolean |
onRetrySem isto, o erro nao oferece nova tentativa. | (() => void) |
retryLabelO nome do botao que executa o `onRetry`. | ReactNode |
skeletonO desenho da espera, no formato do que vem depois - a lista de tres linhas, o cartao, a folha de campos. | ReactNode |
skeletonRowsQuantas linhas falsas a espera generica mostra. | number |
Além destas, a peça aceita className, style, id e children, repassados ao elemento de baixo.