Query (TanStack Query)
Wrappers finos pra padronizar tempos de cache, chaves de query e o QueryClient. Você continua usando o @tanstack/react-query de sempre — o SDK só entrega os defaults bem calibrados e um factory de keys tipado.
Por que esses wrappers existem?
Sem padronização, cada tela escolhe um staleTime no chute e cada domínio escreve queryKey: ["user", id] à mão. Isso gera invalidações que não pegam (key montada diferente em dois lugares) e refetch agressivo demais. O SDK centraliza ambos: presets nomeados e um factory que garante a mesma key em todo lugar.
Provider
Envolva a árvore do app uma única vez, normalmente no main.tsx (ou dentro de <AppProviders>):
import { StrictMode } from "react";
import { createRoot } from "react-dom/client";
import { QueryProvider } from "tempest-react-sdk";
import "tempest-react-sdk/styles.css";
import { App } from "./App";
createRoot(document.getElementById("root")!).render(
<StrictMode>
<QueryProvider>
<App />
</QueryProvider>
</StrictMode>,
);
Defaults aplicados quando você não passa um client:
staleTime: 5 min (STALE_TIME.DEFAULT)gcTime: 30 min (CACHE_TIME.DEFAULT)retry:shouldRetryQuery(queries) / 0 (mutations)refetchOnWindowFocus: false
4xx não é retentado — e isso mudou
O default era retry: 1 chapado, que replicava um 403 numa listagem admin-only e um 404 de registro apagado. O servidor recusou de propósito nos dois casos: a segunda tentativa devolve a mesma resposta, dobra o log de rede e segura o spinner na tela por mais um round trip.
shouldRetryQuery retenta uma vez apenas o que pode mudar sozinho: falha de rede (status === 0), 5xx, 408, 425 e 429 (que é uma recusa cujo significado literal é "mais tarde"), e erro de formato desconhecido — que pode ser falha de transporte. Todo o resto do 4xx falha de primeira.
A classificação vem de isRetriableStatus, a mesma que createApiClient({ retry: true }) e o helper retry() usam. Até a v0.44.0 era uma cópia própria aqui, sem o 425 — então o mesmo 425 Too Early era retentado pelo cliente e não pela query.
Se o seu app dependia de retry em 4xx, o override é o de sempre: defaultOptions={{ queries: { retry: 1 } }}.
Para sobrescrever, passe defaultOptions (mesclado por cima dos defaults) ou um client pronto (ignora os defaults do SDK):
import { QueryClient } from "@tanstack/react-query";
import { QueryProvider, STALE_TIME } from "tempest-react-sdk";
// Opção A — só ajustar alguns defaults
<QueryProvider defaultOptions={{ queries: { staleTime: STALE_TIME.LONG } }}>
<App />
</QueryProvider>;
// Opção B — trazer seu próprio client (compartilhar entre roots, plugar devtools, etc.)
const client = new QueryClient();
<QueryProvider client={client}>
<App />
</QueryProvider>;
Um único QueryClient por app
Não aninhe dois QueryProvider sem passar o mesmo client. Cada provider cria um cache isolado, e queries de subárvores diferentes deixam de compartilhar dados. Para múltiplos roots, crie o QueryClient uma vez e passe via prop client.
\"No QueryClient set\" com o provider montado = duas cópias do react-query
O client que você passa é o único ponto onde a cópia do app e a do SDK se encostam. Se o npm aninhou uma segunda cópia de @tanstack/react-query dentro do SDK, o QueryClientProvider publica o seu client no contexto daquela cópia — e todo useQuery do app lê o contexto da outra, não acha nada, e lança:
No QueryClient set, use QueryClientProvider to set one
Com o provider visivelmente montado três linhas acima. Nada nessa mensagem aponta pra duplicata, e é por isso que ela custa uma tarde.
A partir da v0.52.1 o SDK detecta e avisa em desenvolvimento: um client de outra cópia faz duck-type perfeito mas falha o instanceof, que é a discriminação exata. O conserto é npm dedupe, e npx tempest doctor lista toda dependência duplicada.
Presets de tempo
import { useQuery } from "@tanstack/react-query";
import { STALE_TIME, CACHE_TIME, REFETCH_TIME } from "tempest-react-sdk";
useQuery({
queryKey: ["dashboard"],
queryFn: fetchDashboard,
staleTime: STALE_TIME.LONG, // 30 min — dado muda pouco
gcTime: CACHE_TIME.LONG, // 1 h
refetchInterval: REFETCH_TIME.FAST, // 30 s — polling de status
});
STALE_TIME:SHORT30s,DEFAULT5min,LONG30min,INFINITE∞CACHE_TIME:SHORT5min,DEFAULT30min,LONG1hREFETCH_TIME:REALTIME5s,FAST30s,DEFAULT60s,SLOW5min
Quando usar INFINITE
STALE_TIME.INFINITE marca o dado como nunca obsoleto — o TanStack só refaz o fetch via invalidação manual. Ideal para listas estáticas (categorias, cidades) que mudam por deploy, não por uso.
Query keys tipadas
createQueryKeys recebe um scope (prefixo do domínio) e um mapa de builders. Cada saída já vem com o scope na frente, então a mesma key é montada de forma idêntica em toda a base de código:
import { createQueryKeys } from "tempest-react-sdk";
export const userKeys = createQueryKeys("user", {
me: () => ["me"] as const,
byId: (id: string) => [id] as const,
list: (filters: { page: number; size: number }) => ["list", filters] as const,
});
userKeys.all; // ["user"]
userKeys.me(); // ["user", "me"]
userKeys.byId("42"); // ["user", "42"]
userKeys.list({ page: 1, size: 20 }); // ["user", "list", { page: 1, size: 20 }]
Note o all: ele é gerado automaticamente e é a key mais ampla do domínio — invalidá-lo derruba todas as queries user/* de uma vez.
Exemplo completo — query + mutation + invalidação
Esse componente lê o perfil com useQuery, atualiza com useMutation e invalida só as keys afetadas no sucesso:
import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query";
import { createApiClient, createQueryKeys } from "tempest-react-sdk";
interface User {
id: string;
name: string;
}
const api = createApiClient({ baseURL: import.meta.env.VITE_API_URL });
export const userKeys = createQueryKeys("user", {
me: () => ["me"] as const,
byId: (id: string) => [id] as const,
});
export function ProfileCard() {
const queryClient = useQueryClient();
const { data: user, isLoading } = useQuery({
queryKey: userKeys.me(),
queryFn: () => api.get<User>("/users/me"),
});
const rename = useMutation({
mutationFn: (name: string) => api.patch<User>("/users/me", { body: { name } }),
onSuccess: (updated) => {
// Atualiza o cache local sem novo fetch...
queryClient.setQueryData(userKeys.me(), updated);
// ...e invalida o registro por id, caso outra tela o use.
queryClient.invalidateQueries({ queryKey: userKeys.byId(updated.id) });
},
});
if (isLoading) return <p>Carregando…</p>;
return (
<div>
<h2>{user?.name}</h2>
<button disabled={rename.isPending} onClick={() => rename.mutate("Novo nome")}>
Renomear
</button>
</div>
);
}
Por que setQueryData + invalidateQueries?
setQueryData aplica a resposta da mutation no cache imediatamente (UI sem flicker), enquanto invalidateQueries marca queries relacionadas como obsoletas pra revalidar no background. Usar uma key factory garante que a key invalidada seja exatamente a mesma que a query consultou.
Padrão de organização: cada domínio em src/constants/query-keys/<dominio>.ts, agrupados num barrel.
useOfflineMutation
Quando o app é offline-first, uma mutation não deve bater na rede direto — ela grava no outbox do createOfflineSync e sincroniza depois. useOfflineMutation faz a ponte entre o motor de sync e o TanStack Query: no mutate ele enfileira a entrada, atualiza o cache da query otimisticamente, dá o flush e faz rollback do cache se o enqueue falhar.
import { useOfflineMutation } from "tempest-react-sdk";
import { notesSync } from "@/sync/engine";
import type { Note } from "@/sync/types";
function useAddNote() {
return useOfflineMutation<Note, Note[], Note>({
sync: notesSync,
queryKey: ["notes"],
toEntry: (note) => ({ op: "create", recordId: note.id, payload: note }),
applyOptimistic: (current = [], note) => [...current, note],
});
}
// const addNote = useAddNote();
// addNote.mutate({ id: crypto.randomUUID(), text: "offline!" });
toEntrymapeia as variáveis pra{ op, recordId, payload }do outbox.applyOptimisticproduz o próximo valor do cache; o anterior é restaurado se o enqueue lançar.flush(defaulttrue→"after-mutation") dispara a sincronização;falsedeixa isso prouseOfflineSync.invalidate(defaultfalse) revalida aqueryKeynoonSettled.
Helpers de cache de lista
Para o caso comum (cache é uma lista), use upsertById() / removeById() no lugar de escrever o spread na mão:
import { upsertById, removeById } from "tempest-react-sdk";
applyOptimistic: upsertById(); // insere ou faz merge por `id`
applyOptimistic: removeById(); // remove por `id` (op "delete")
// campo custom: upsertById("uuid")
A entrega ao servidor acontece no flush
O mutate resolve com o id da entrada no outbox, não com a resposta do servidor — a entrega real roda no loop de flush do motor, então a UI atualiza na hora e sobrevive a reloads e a períodos offline.
persistQueryClientOffline
Persiste o cache do QueryClient no IndexedDB e restaura no boot — um reload ou início offline mostra os últimos dados conhecidos em vez de telas vazias. Self-contained: usa dehydrate/hydrate do próprio @tanstack/react-query, sem precisar do @tanstack/react-query-persist-client.
import { persistQueryClientOffline } from "tempest-react-sdk";
import { queryClient } from "@/lib/query";
const persistence = persistQueryClientOffline({ queryClient });
await persistence.restore(); // antes do primeiro render
// no logout:
await persistence.clear();
// no teardown:
persistence.unsubscribe();
As gravações são throttled (throttleMs, default 1s) e assinam o cache. flush() grava na hora; clear() apaga o snapshot; unsubscribe() para de persistir. Dexie é peer dependency do store offline — instale (npm i dexie).
Recap
<QueryProvider>na raiz — um por app — entrega defaults calibrados; sobrescreva viadefaultOptionsouclient.STALE_TIME/CACHE_TIME/REFETCH_TIMEsubstituem números mágicos por presets nomeados.createQueryKeys(scope, builders)gera keys tipadas e consistentes, com umallautomático pra invalidação ampla.- Combine
setQueryData(resposta imediata) cominvalidateQueries(revalidação) usando a mesma key factory. useOfflineMutationliga o motor offline ao cache: enqueue + optimistic update + flush + rollback.
Veja também
- HTTP — o
createApiClientque alimenta asqueryFn - Offline — combinar com
initialDatapra fallback offline - PWA & Offline-First — service worker, background sync, status UI