Usando TanStack Query para Busca de Dados Mais Inteligente no React

Se você já construiu aplicações React que se comunicam com APIs, conhece o padrão: useEffect
para a busca, useState
para os dados, outro useState
para loading, talvez mais um para erros. Antes que você perceba, está gerenciando uma confusão emaranhada de estado apenas para exibir uma lista de usuários.
Essa abordagem manual funciona, mas é frágil. O que acontece quando o usuário navega para outra página e volta? Você refaz a busca? Usa dados desatualizados? E quanto às tentativas quando a rede falha? Estes não são casos extremos—são a realidade de aplicações em produção.
TanStack Query (anteriormente React Query) resolve esses problemas tratando o estado do servidor de forma diferente do estado do cliente. Em vez de buscar dados imperativamente, você declara o que precisa e deixa a biblioteca lidar com cache, sincronização e atualizações. Este artigo mostra como migrar da busca manual de dados para uma abordagem mais inteligente e declarativa que escala.
Principais Pontos
- Substitua padrões manuais de
useEffect
+useState
por hooksuseQuery
declarativos para código mais limpo e sustentável - Aproveite cache automático, refetch em background e deduplicação de requisições para melhorar a experiência do usuário
- Use mutations com atualizações otimistas para criar UIs responsivas que parecem instantâneas
- Implemente estratégias adequadas de invalidação de queries para manter dados sincronizados em toda sua aplicação
- Evite armadilhas comuns como usar TanStack Query para estado local ou esquecer parâmetros dinâmicos nas chaves de query
O Problema com Busca Manual de Dados
Aqui está um componente React típico buscando dados:
import { useState, useEffect } from 'react';
function UserList() {
const [users, setUsers] = useState([]);
const [isLoading, setIsLoading] = useState(false);
const [error, setError] = useState(null);
useEffect(() => {
let cancelled = false;
async function fetchUsers() {
setIsLoading(true);
setError(null);
try {
const response = await fetch('/api/users');
if (!response.ok) throw new Error('Failed to fetch');
const data = await response.json();
if (!cancelled) {
setUsers(data);
}
} catch (err) {
if (!cancelled) {
setError(err.message);
}
} finally {
if (!cancelled) {
setIsLoading(false);
}
}
}
fetchUsers();
return () => { cancelled = true };
}, []);
if (isLoading) return <div>Loading...</div>;
if (error) return <div>Error: {error}</div>;
return (
<ul>
{users.map(user => (
<li key={user.id}>{user.name}</li>
))}
</ul>
);
}
Isso funciona, mas veja o que estamos gerenciando:
- Três peças separadas de estado
- Lógica de cleanup para prevenir atualizações de estado em componentes desmontados
- Sem cache—cada montagem dispara uma nova busca
- Sem lógica de retry para requisições falhas
- Nenhuma forma de saber se nossos dados estão desatualizados
Agora multiplique essa complexidade por dezenas de componentes. É aí que o TanStack Query entra.
Como o TanStack Query Torna a Busca de Dados Mais Inteligente
TanStack Query trata o estado do servidor como um cidadão de primeira classe. Em vez de orquestrar buscas manualmente, você descreve seus requisitos de dados declarativamente:
import { useQuery } from '@tanstack/react-query';
function UserList() {
const { data: users, isLoading, error } = useQuery({
queryKey: ['users'],
queryFn: () => fetch('/api/users').then(res => {
if (!res.ok) throw new Error('Failed to fetch');
return res.json();
}),
});
if (isLoading) return <div>Loading...</div>;
if (error) return <div>Error: {error.message}</div>;
return (
<ul>
{users?.map(user => (
<li key={user.id}>{user.name}</li>
))}
</ul>
);
}
Mesma funcionalidade, mas note o que está faltando:
- Nenhum
useState
para dados, loading ou erros - Nenhum
useEffect
ou lógica de cleanup - Nenhuma sincronização manual de estado
Mas aqui está o que você ganha:
- Cache automático: Navegue para fora e volte, veja resultados instantâneos
- Refetch em background: Dados desatualizados são atualizados silenciosamente
- Deduplicação de requisições: Múltiplos componentes podem compartilhar a mesma query
- Lógica de retry integrada: Requisições falhas fazem retry automaticamente
- Atualizações otimistas: UI atualiza antes da confirmação do servidor
Conceitos Fundamentais para Gerenciamento Inteligente de Dados
Queries: Lendo Estado do Servidor
Queries buscam e fazem cache de dados. O hook useQuery
aceita um objeto de configuração com duas propriedades essenciais:
const { data, isLoading, error } = useQuery({
queryKey: ['todos', userId, { status: 'active' }],
queryFn: () => fetchTodosByUser(userId, { status: 'active' }),
staleTime: 5 * 60 * 1000, // 5 minutos
gcTime: 10 * 60 * 1000, // 10 minutos (anteriormente cacheTime)
});
Query Keys identificam unicamente cada query. São arrays que podem incluir:
- Identificadores estáticos:
['users']
- Parâmetros dinâmicos:
['user', userId]
- Filtros complexos:
['todos', { status, page }]
Quando qualquer parte da query key muda, TanStack Query sabe que deve buscar dados frescos.
Mutations: Alterando Estado do Servidor
Enquanto queries leem dados, mutations os modificam:
import { useMutation, useQueryClient } from '@tanstack/react-query';
function CreateTodo() {
const queryClient = useQueryClient();
const mutation = useMutation({
mutationFn: (newTodo) =>
fetch('/api/todos', {
method: 'POST',
body: JSON.stringify(newTodo),
headers: { 'Content-Type': 'application/json' },
}).then(res => {
if (!res.ok) throw new Error('Failed to create todo');
return res.json();
}),
onSuccess: () => {
// Invalidar e refazer busca
queryClient.invalidateQueries({ queryKey: ['todos'] });
},
});
return (
<button
onClick={() => mutation.mutate({ title: 'New Todo' })}
disabled={mutation.isPending}
>
{mutation.isPending ? 'Creating...' : 'Create Todo'}
</button>
);
}
Mutations lidam com o ciclo de vida completo: estados de loading, tratamento de erros e callbacks de sucesso. O callback onSuccess
é perfeito para atualizar seu cache após mudanças.
Invalidação de Query: Mantendo Dados Frescos
Invalidação marca queries como desatualizadas, disparando refetches em background:
// Invalidar tudo
queryClient.invalidateQueries();
// Invalidar queries específicas
queryClient.invalidateQueries({ queryKey: ['todos'] });
// Invalidar com correspondência exata
queryClient.invalidateQueries({
queryKey: ['todo', 5],
exact: true
});
É assim que TanStack Query mantém sua UI sincronizada após mutations. Alterou um todo? Invalide a lista de todos. Atualizou um usuário? Invalide os dados daquele usuário.
Configurando TanStack Query na Sua Aplicação React
Primeiro, instale o pacote:
npm install @tanstack/react-query
Então envolva sua aplicação com o provider QueryClient:
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
const queryClient = new QueryClient({
defaultOptions: {
queries: {
staleTime: 60 * 1000, // 1 minuto
gcTime: 5 * 60 * 1000, // 5 minutos (anteriormente cacheTime)
retry: 3,
retryDelay: attemptIndex => Math.min(1000 * 2 ** attemptIndex, 30000),
},
},
});
function App() {
return (
<QueryClientProvider client={queryClient}>
<YourApp />
</QueryClientProvider>
);
}
O QueryClient
gerencia todo cache e sincronização. Você tipicamente cria uma instância para toda sua aplicação.
Padrões Avançados para Aplicações em Produção
Atualizações Otimistas
Atualize a UI imediatamente, depois sincronize com o servidor:
const mutation = useMutation({
mutationFn: updateTodo,
onMutate: async (newTodo) => {
// Cancelar refetches pendentes
await queryClient.cancelQueries({ queryKey: ['todos'] });
// Snapshot do valor anterior
const previousTodos = queryClient.getQueryData(['todos']);
// Atualizar otimisticamente
queryClient.setQueryData(['todos'], old =>
old ? [...old, newTodo] : [newTodo]
);
// Retornar contexto para rollback
return { previousTodos };
},
onError: (err, newTodo, context) => {
// Rollback em caso de erro
if (context?.previousTodos) {
queryClient.setQueryData(['todos'], context.previousTodos);
}
},
onSettled: () => {
// Sempre refetch após erro ou sucesso
queryClient.invalidateQueries({ queryKey: ['todos'] });
},
});
Queries Dependentes
Encadeie queries que dependem umas das outras:
const { data: user } = useQuery({
queryKey: ['user', email],
queryFn: () => getUserByEmail(email),
});
const { data: projects } = useQuery({
queryKey: ['projects', user?.id],
queryFn: () => getProjectsByUser(user.id),
enabled: !!user?.id, // Só executa quando user ID existe
});
Queries Infinitas
Lide com dados paginados com scroll infinito:
import { useInfiniteQuery } from '@tanstack/react-query';
const {
data,
fetchNextPage,
hasNextPage,
isFetchingNextPage,
} = useInfiniteQuery({
queryKey: ['projects'],
queryFn: ({ pageParam = 0 }) => fetchProjects({ page: pageParam }),
getNextPageParam: (lastPage, pages) => lastPage.nextCursor,
initialPageParam: 0,
});
Armadilhas Comuns e Soluções
Esquecer o QueryClientProvider
Se suas queries retornam undefined ou não funcionam de forma alguma, verifique se sua aplicação está envolvida com QueryClientProvider
. Este é o erro de configuração mais comum.
Usar TanStack Query para Estado Local
TanStack Query é para estado do servidor. Não use para inputs de formulário, toggles de UI ou outro estado apenas do cliente. Use useState
ou useReducer
para esses casos.
Query Keys Estáticas com Dados Dinâmicos
Sempre inclua parâmetros dinâmicos nas suas query keys:
// ❌ Errado - mostrará os mesmos dados para todos os usuários
useQuery({
queryKey: ['user'],
queryFn: () => fetchUser(userId),
});
// ✅ Correto - entrada de cache única por usuário
useQuery({
queryKey: ['user', userId],
queryFn: () => fetchUser(userId),
});
Conclusão
TanStack Query transforma a busca de dados no React de um processo manual e propenso a erros em um sistema declarativo e robusto. Ao tratar o estado do servidor como fundamentalmente diferente do estado do cliente, elimina boilerplate enquanto adiciona recursos poderosos como cache, sincronização e atualizações otimistas.
A mudança mental é simples: pare de pensar em como buscar dados e comece a pensar em quais dados você precisa. Declare seus requisitos através de query keys e funções, então deixe TanStack Query lidar com a complexidade de manter tudo sincronizado.
Comece pequeno—substitua um fetch com useEffect
por useQuery
. Uma vez que você veja os estados de loading instantâneos, retries automáticos e cache perfeito em ação, entenderá por que TanStack Query se tornou essencial para aplicações React modernas.
FAQs
São a mesma biblioteca. React Query foi renomeado para TanStack Query para refletir que agora suporta múltiplos frameworks além do React. O pacote específico para React ainda é mantido e usa a mesma API que você já conhece.
Sim, mas provavelmente você não precisa para estado do servidor. TanStack Query lida com cache e compartilhamento de dados entre componentes automaticamente. Use Redux ou Context para verdadeiro estado do cliente como preferências do usuário ou estado de UI, e TanStack Query para qualquer coisa que venha de uma API.
Por padrão, queries refazem busca no foco da janela, reconexão e montagem. Você controla a atualização com staleTime (quanto tempo os dados permanecem frescos) e gcTime (quanto tempo manter dados não utilizados). Queries desatualizadas refazem busca em background enquanto mostram dados em cache instantaneamente.
Invalidação marca queries como desatualizadas mas não refaz busca imediatamente—espera a query ser usada novamente. Refetch direto dispara uma requisição de rede imediata. Use invalidação após mutations para melhor performance, pois só refaz busca de queries que estão realmente sendo exibidas.
TanStack Query se destaca em atualizações quase em tempo real através de polling e intervalos de refetch. Para necessidades de tempo real verdadeiro, combine com WebSockets: use o socket para atualizações ao vivo e TanStack Query para carregamento inicial de dados e fallback quando o socket desconecta.
Sim. TanStack Query 5+ é totalmente compatível com React 19. As APIs usadas aqui (queries, mutations, invalidação) não mudaram, então os exemplos neste artigo funcionarão sem modificação.