O que é TanStack Query?
TanStack Query, anteriormente conhecido como React Query, é uma biblioteca de gerenciamento de estado de servidor. Ela reconhece que dados buscados de uma API são fundamentalmente diferentes do estado que você mantém em useState. Dados de servidor são compartilhados, cacheados, podem ficar obsoletos e precisam ser buscados novamente. Gerenciar isso com effects e flags é tedioso e propenso a bugs.
O TanStack Query fornece um cache adequado para dados de servidor. Você descreve como buscar um dado e atribui a ele uma chave, e a biblioteca cuida do caching, deduplicação, estados de carregamento e erro, refetching em segundo plano e invalidação. É a camada de dados que faltava para o React, e combina com qualquer biblioteca de estado de cliente.
Queries
Uma query é definida por uma key (chave) e uma query function. A key identifica a entrada no cache; a função busca os dados.
// api.ts
async function fetchPost(id: string) {
const res = await fetch(`/api/posts/${id}`);
if (!res.ok) throw new Error(`HTTP ${res.status}`);
return res.json();
}
// Post.tsx
import { useQuery } from "@tanstack/react-query";
export function Post({ id }: { id: string }) {
const { data, isLoading, isError, error } = useQuery({
queryKey: ["post", id],
queryFn: () => fetchPost(id),
});
if (isLoading) return <p>Loading…</p>;
if (isError) return <p>Error: {error.message}</p>;
return <article>{data.title}</article>;
}
Todo componente que utiliza a mesma key compartilha a mesma entrada de cache, portanto, montar o componente duas vezes resulta em apenas uma requisição. As flags de status são fornecidas automaticamente, para que você nunca precise escrevê-las manualmente.
Query keys e o cache
A chave é a identidade da entrada do cache e deve incluir todas as variáveis das quais a busca depende.
// keys.ts
useQuery({ queryKey: ["posts"], queryFn: fetchPosts });
useQuery({ queryKey: ["post", id], queryFn: () => fetchPost(id) });
useQuery({
queryKey: ["posts", { page, status }],
queryFn: () => fetchPosts({ page, status }),
});
Como as chaves são arrays, você pode invalidar toda uma família usando um prefixo. Invalidar ["posts"] também invalida ["posts", { page: 1 }], o que torna as atualizações de cache previsíveis.
Atualização e refetching
Duas opções controlam quando os dados são buscados novamente (refetched).
// freshness.ts
useQuery({
queryKey: ["posts"],
queryFn: fetchPosts,
staleTime: 60_000, // fresh for one minute
gcTime: 5 * 60_000, // keep unused data for five minutes
});
staleTime define por quanto tempo os dados são considerados “frescos” (fresh); enquanto estiverem frescos, nenhum refetch em segundo plano acontece. gcTime define por quanto tempo uma entrada não utilizada permanece na memória antes de ser removida pelo garbage collector. Por padrão, as queries fazem o refetch ao montar (mount), ao focar na janela (window focus) e ao reconectar, o que mantém a UI atualizada sem a necessidade de trabalho manual.
Mutations
useMutation lida com as escritas: criação, atualização e exclusão.
// AddPost.tsx
import { useMutation, useQueryClient } from "@tanstack/react-query";
export function AddPost() {
const queryClient = useQueryClient();
const mutation = useMutation({
mutationFn: (title: string) =>
fetch("/api/posts", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ title }),
}).then((r) => r.json()),
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ["posts"] });
},
});
return (
<button
onClick={() => mutation.mutate("New post")}
disabled={mutation.isPending}
>
{mutation.isPending ? "Saving…" : "Add post"}
</button>
);
}
Após uma mutation bem-sucedida, invalidar as queries afetadas informa a todos os consumidores que devem refazer a busca (refetch). Isso mantém o servidor como a fonte da verdade e evita a manipulação manual do cache.
Updates otimistas
Para ações que devem parecer instantâneas, atualize o cache antes que o servidor responda e reverta a alteração em caso de falha.
// optimistic.ts
useMutation({
mutationFn: toggleLike,
onMutate: async (postId) => {
await queryClient.cancelQueries({ queryKey: ["post", postId] });
const previous = queryClient.getQueryData(["post", postId]);
queryClient.setQueryData(["post", postId], (old) => ({
...old,
likes: old.likes + 1,
}));
return { previous };
},
onError: (_err, postId, context) => {
queryClient.setQueryData(["post", postId], context.previous);
},
onSettled: (_data, _err, postId) => {
queryClient.invalidateQueries({ queryKey: ["post", postId] });
},
});
onMutate aplica a alteração otimista e tira um snapshot do valor anterior, onError reverte a alteração e onSettled refaz a busca para sincronizar com o servidor.
Paginação e queries infinitas
O TanStack Query possui suporte nativo para dados paginados e infinitos. useQuery com a opção keepPreviousData torna as transições de página fluidas, e useInfiniteQuery gerencia listas baseadas em cursor com a função fetchNextPage. Como cada página faz parte do cache, a navegação para frente e para trás é instantânea.
Estado do servidor versus estado do cliente
Esta é a distinção que faz o TanStack Query fazer sentido:
- Estado do servidor é controlado pelo servidor, compartilhado, assíncrono e pode ficar desatualizado (stale). Use TanStack Query.
- Estado do cliente é controlado pelo navegador, síncrono e local. Use
useState, Context, Zustand ou Redux.
Misturar os dois em um único store é a fonte de muita complexidade. Separá-los — TanStack Query para dados e um store pequeno para o estado da UI — mantém cada um deles simples.
Melhores práticas
- Inclua todas as variáveis na query key.
- Defina
staleTimedeliberadamente; o valor padrão de zero dispara refetches agressivamente. - Use mutations combinadas com invalidação em vez de atualizar manualmente o cache em todos os lugares.
- Mantenha as funções de query pequenas e colocalizadas com a query.
- Adicione optimistic updates apenas onde o feedback instantâneo for essencial.
- Use as devtools para inspecionar o estado do cache durante o desenvolvimento.
- Mantenha o estado do servidor no TanStack Query e o estado do cliente em outro lugar.
Erros comuns
- Usar a mesma chave para inputs diferentes, resultando em dados obsoletos ou incorretos.
- Deixar o
staleTimeem zero, disparando refetches constantes. - Duplicar dados do servidor em um store global.
- Esquecer de invalidar após uma mutation, exibindo dados desatualizados.
- Escrever optimistic updates sem um caminho de rollback.
- Tratar funções de query como componentes e chamar hooks dentro delas.
Próximos passos
O TanStack Query é a maneira padrão de lidar com dados do servidor em aplicações modernas. Combine-o com a Fetch API para a camada de requisições, um store de cliente como Zustand para o estado da UI, e Redux Toolkit se você precisar de um estado global rigoroso. Para Vue, o Pinia gerencia o estado do cliente enquanto o TanStack Query cuida do lado do servidor.