O que é server state (e por que não cabe em useState)
9 min de leitura
A maior parte do "estado" numa app real não vive no seu componente.
Vive num banco, numa API, num servidor que você não controla. Esse
estado - que a gente chama de server state - tem regras muito
diferentes do useState que você já conhece: pode mudar sem você
perceber, pode ficar desatualizado, pode ser compartilhado entre
vários usuários, e pode falhar por motivos que não são bugs do
seu código.
Quando você tenta guardar server state em useState, você acaba
reescrevendo o TanStack Query de um jeito pior. Este nó é sobre
entender o paradigma, pra chegar no useQuery com o modelo mental
certo.
O essencial 🟢
Os três tipos de estado na UI. Antes do TanStack Query, vale parar e separar:
client state- o estado que você controla, e que existe só na sessão do usuário. Tema dark/light, toggle de um menu, valor de um input antes do submit, qual aba está aberta. Vive no React, some quando a aba fecha. Ferramenta certa:useState,useReducer, Context API, Zustand.server state- o estado que vive num lugar que você não controla (banco, API de terceiros, microserviço). Você só tem uma "foto" dele, e essa foto pode ficar velha. Lista de usuários, contagem de likes, saldo da conta, dados de um formulário que outra pessoa editou. Ferramenta certa: TanStack Query, SWR, RTK Query, Apollo (esse último pra GraphQL).URL state- o estado que vive na própria URL. Filtros, busca, paginação, parâmetros de um modal. Ferramenta certa:useSearchParams(Next),useParams(React Router), ousearchParamsem Server Components.
// client state - "qual aba está ativa?". Você controla.
const [aba, setAba] = useState<"home" | "perfil" | "config">("home");
// server state - "lista de usuários". Você só tem uma cópia.
const usuarios = await fetch("/api/usuarios").then((r) => r.json());
// URL state - "qual página está aberta?". Vive no navegador.
const searchParams = new URLSearchParams(location.search);
const page = Number(searchParams.get("page") ?? "1");
Por que useState quebra com server state. O useState é
feito pra estado síncrono, controlado e local. Server state
não tem nenhuma dessas três propriedades:
// Código que parece certo mas quebra em produção
function Perfil({ userId }: { userId: string }) {
const [usuario, setUsuario] = useState(null);
const [loading, setLoading] = useState(false);
const [error, setError] = useState(null);
useEffect(() => {
setLoading(true);
fetch(`/api/usuarios/${userId}`)
.then((r) => r.json())
.then((data) => {
setUsuario(data);
setLoading(false);
})
.catch((e) => {
setError(e);
setLoading(false);
});
}, [userId]);
if (loading) return <Spinner />;
if (error) return <Erro />;
return <Nome valor={usuario.nome} />;
}
Os problemas que aparecem rápido:
- Sem cache. Cada componente que busca o mesmo
userIdfaz a mesma request. Se você tem 5 cards de perfil na mesma tela, são 5 requests idênticas. - Sem retry. Caiu a rede por 200ms? A request falha, e o usuário vê um erro que sumiu sozinho.
- Sem refetch quando o foco volta. Usuário trocou de aba por
5 minutos, voltou, e os dados estão velhos. Você precisa
escrever
refetchOnFocusna mão (e nunca vai). - Race conditions. Se
userIdmuda rápido (digitando numa busca), a request antiga pode voltar depois da nova, e você mostra dados errados. OuseEffectcleanup raramente cancela a request - você precisa deAbortController. - Estado compartilhado. Dois componentes que precisam do mesmo dado duplicam o estado. Não tem como saber se são a "mesma" informação.
- Loading vs. fetching. Tem diferença entre "primeira vez"
(loading) e "atualizando em background" (fetching). Com
useState, você só tem umloadinge perde a nuance.
O que TanStack Query resolve. É uma lib de server state
cache - ela guarda a resposta da request em memória, e expõe
um hook (useQuery) que te dá: o dado, o estado de loading,
o estado de erro, e o poder de revalidar quando você quiser.
Comparado com useEffect + useState, o "Hello World" do
TanStack Query é mais curto e te dá mais:
// Mesmo caso, com TanStack Query
import { useQuery } from "@tanstack/react-query";
function Perfil({ userId }: { userId: string }) {
const { data: usuario, isLoading, isError, error } = useQuery({
queryKey: ["usuario", userId],
queryFn: () => fetch(`/api/usuarios/${userId}`).then((r) => r.json()),
});
if (isLoading) return <Spinner />;
if (isError) return <Erro mensagem={error.message} />;
return <Nome valor={usuario.nome} />;
}
Esse useQuery sozinho te dá:
- Cache - se outro componente já pediu esse
userId, o dado vem do cache, sem request. - Deduplicação - se dois componentes pedem o mesmo
userIdno mesmo render, é uma request, não duas. - Retry automático - falha de rede? TanStack Query tenta de novo, com backoff exponencial.
- Refetch on focus - volta pra aba? O dado é revalidado em background.
- Refetch on reconnect - a rede caiu e voltou? O dado é revalidado.
- Cancelamento - se o componente desmonta no meio da request, ela é cancelada (sem race condition).
- Loading vs. fetching - tem
isLoading(primeira vez) eisFetching(atualizando), separados.
O que TanStack Query NÃO resolve. Saber o que ela não faz é tão importante quanto saber o que faz. Senão você força o uso e fica confuso quando algo não funciona.
- Não substitui
useStatepra client state. Cor do tema, toggle de modal, valor de um input - isso continua sendouseState(ou Context, ou Zustand). Misturar os dois paradigmas é o erro mais comum. - Não substitui a lógica de autenticação. Token, refresh token, logout - isso é do seu auth provider (Auth.js, Clerk, Lucia). TanStack Query busca dados autenticados, mas não gerencia a auth.
- Não é real-time por padrão. Quando outro usuário edita
um registro, sua UI não atualiza sozinha. Pra isso você
precisa de WebSocket / SSE, ou fazer polling com
refetchInterval. Pertence à trilhareal-time-websockets-sse(issue #52). - Não persiste entre reloads por padrão. Recarregou a
página? O cache some. Pra persistir (offline-first,
multi-tab sync), você precisa de
persistQueryClient- storage. Menção no projeto final; aprofundamento
fica pra
pwa-offline-first(issue #51).
- storage. Menção no projeto final; aprofundamento
fica pra
- Não faz GraphQL ou REST - faz ambos. A camada de
transporte é a
queryFnque você passa. Você pode usarfetch,axios,graphql-request, ou qualquer cliente HTTP. TanStack Query cuida do cache e do estado, não do request.
O ganho real: separar paradigmas. O ponto principal não é "usar TanStack Query em vez de fetch". É parar de misturar paradigmas na mesma variável. Se o dado vem do servidor, ele tem regras diferentes (cache, invalidação, retry). Se é estado local, ele tem regras diferentes (síncrono, sem cache, sem retry). Quando você separa, o código fica menor e mais previsível.
Aprofundamento 🟡
A regra de ouro pra decidir o que vai onde. Quando você
abre um componente e não sabe se um pedaço de estado vai em
useState ou useQuery, faça duas perguntas:
- Esse valor existe se o usuário recarregar a página?
- Se sim (lista de produtos, perfil do usuário logado,
contagem de likes), é server state →
useQuery. - Se não (toggle do menu, valor de um input antes do
submit, scroll position), é client state →
useState.
- Se sim (lista de produtos, perfil do usuário logado,
contagem de likes), é server state →
- Outro componente pode precisar do mesmo valor ao mesmo
tempo?
- Se sim (dois componentes mostram o mesmo perfil,
três cards puxam o mesmo produto), é server state →
useQuery(compartilha o cache). - Se não (estado de hover de um botão, ref de um
input), é client state →
useStateouuseRef.
- Se sim (dois componentes mostram o mesmo perfil,
três cards puxam o mesmo produto), é server state →
Em caso de dúvida, comece com useState. Promova pra
useQuery quando você sentir dor (request duplicada, dados
velhos, race condition).
O que TanStack Query é, de verdade, por dentro. O
useQuery é, internamente, um cache em memória (um Map
chaveado por queryKey) + um sistema de subscrição. Quando
você chama useQuery({ queryKey: ["usuario", 1], ... }):
- Ele checa o cache: "tenho dados pra essa chave?".
- Se sim, e o dado é "fresco" (dentro do
staleTime), retorna o dado sem fazer request. - Se sim, mas o dado está "stale" (passou do
staleTime), retorna o cache e dispara uma request em background pra revalidar. - Se não tem no cache, dispara a request, e retorna
isLoading: trueenquanto espera. - Quando a request volta, atualiza o cache, e todos os componentes subscritos re-renderizam com o dado novo.
Isso é a base de tudo que vem a partir do próximo nó. Se você entender esse ciclo, o resto da trilha fica "como eu configuro cada parte desse fluxo".
Stale-while-revalidate, o padrão de UX. A ideia de "retornar cache stale e revalidar em background" tem um nome: stale-while-revalidate (SWR). É o mesmo padrão que HTTP define pra cache de CDN: o usuário vê o dado velho (rápido), e em paralelo o sistema busca o dado novo (que aparece na próxima interação ou na próxima revalidação). TanStack Query aplica esse padrão a qualquer fonte de dados (client, não só CDN).
useQuery é só o começo. TanStack Query tem uma família
de hooks:
useQuery- read (buscar dado).useMutation- write (criar, atualizar, deletar).useInfiniteQuery- listas paginadas com "carregar mais".useSuspenseQuery- integração com<Suspense>do React 18+.useQueries- múltiplas queries em paralelo, sem precisar de hook manual.
Esta trilha cobre o essencial dos 4 primeiros. O 5º fica
como aprofundamento no resources do nó 5.
Como TanStack Query se compara a outras libs. Vale saber que existem alternativas, e em que cada uma brilha:
- SWR (Vercel) - irmã mais nova e mais simples da
TanStack Query. Menos features (não tem
useInfiniteQuerybuilt-in, não temsetQueryData), mas a API é menor e o bundle é menor. Boa pra SPAs pequenas que não precisam de mutations complexas. - RTK Query (Redux Toolkit) - faz parte do ecossistema Redux. Vantagem: se você já tem Redux na app, a integração é natural. Desvantagem: se você não tem Redux, é peso desnecessário.
- Apollo Client - pra GraphQL. Tem cache inteligente
baseado em
__typenameeid, e faz normalização automática. Se sua API é GraphQL, Apollo é a escolha padrão; pra REST, TanStack Query é mais ergonômica. fetch+useEffect- o caminho "vanilla". Funciona pra protótipo, mas exige que você reimplemente tudo que TanStack Query te dá de graça (cache, retry, refetch, cancelamento). É a comparação implícita desta trilha: a partir de agora, a gente considera o caminho vanilla como "o caso ruim" e TanStack Query como "o caminho certo" pra apps de produção.
Pra quem quer ir além 🔴
Como TanStack Query pensa cache invalidation. O
mantenedor da lib, Dominik Dorfmeister (TkDodo), escreveu
um post só sobre isso: "Mastering React Query Mutations".
A ideia central: invalidação é o problema difícil de
server state. Não é buscar (que é fácil - é um fetch),
é garantir que o cache bate com o servidor depois de
uma escrita. O TanStack Query te dá as ferramentas
(invalidateQueries, setQueryData, removeQueries,
cancelQueries), mas a estratégia (qual usar, quando)
é decisão sua. Veremos isso a fundo nos nós
mutations e cache-invalidation.
React Query e Suspense, a tendência. O React 18+
trouxe <Suspense> como first-class pra data fetching.
TanStack Query tem useSuspenseQuery (que joga promise
em vez de retornar isLoading: true), e o time da lib
está migrando o restante da API pra Suspense-first. Em
2026, a recomendação oficial é: use useSuspenseQuery
em código novo, a menos que você tenha um motivo forte
pra usar useQuery (ex: precisa do isLoading separado
de isError). Esta trilha mostra o caminho com
useQuery porque é mais ergonômico pra começar; a
migração pra useSuspenseQuery é direta.
Inferência de tipo avançada com TanStack Query. A
lib usa generics (useQuery<TQueryFnData, TError, TData, TQueryKey>) que, em apps grandes, viram
verbosos. Existem padrões com queryOptions (que
encapsula a query e seus tipos) e com inferência de tipo via query factory que eliminam a repetição. É
padrão de produção, mas além do escopo introdutório
desta trilha - a typescript-frontend (quando sair)
cobre com mais profundidade.
Leitura recomendada:
- Practical React Query - TkDodo, o mantenedor. A referência.
- Mastering React Query Mutations - invalidação depois de escrita.
- Server State - React Query (vídeo introdutório, EN) - overview de 15 minutos.
Dica: a tentação aqui é pular direto pra "como usar useQuery com minha API". Não pule. O paradigma de server state é a fundação - se você internalizar que server state tem regras diferentes de client state, o resto da trilha flui. Se você pular, vai usar TanStack Query como se fosse
useStatecom loading, e perder 80% do ganho.
No próximo nó, vamos abrir o useQuery de verdade:
a anatomia do hook, o que cada campo do retorno significa
(data, isLoading, isFetching, isError, error,
refetch), e como configurar staleTime, gcTime e
enabled pra moldar o comportamento.
// Quiz
Qual a melhor forma de decidir se um pedaço de estado vai em useState ou useQuery?