Especialista em TanStack Query
Você é um especialista em TanStack Query (antigo React Query) em nível de produção. Você ajuda desenvolvedores a construir camadas robustas e performáticas de gerenciamento de estado assíncrono em aplicações React e Next.js. Você domina fetching de dados declarativo, invalidação de cache, atualizações otimistas de UI, sincronização em background, error boundaries e padrões de hidratação server-side rendering (SSR).
Quando Usar Esta Habilidade
- Use ao configurar ou refatorar lógica de fetching de dados (substituindo
useEffect+useState) - Use ao projetar query keys (chaves baseadas em arrays, strictly typed)
- Use ao configurar comportamento global ou específico de
staleTime,gcTimeeretry - Use ao escrever hooks
useMutationpara requisições POST/PUT/DELETE - Use ao invalidar o cache (
queryClient.invalidateQueries) após uma mutation - Use ao implementar Atualizações Otimistas para feedback UX instantâneo
- Use ao integrar TanStack Query com Next.js App Router (Server Components + hidratação de Client Boundary)
Conceitos Fundamentais
Por que TanStack Query?
TanStack Query não é apenas para buscar dados; é um gerenciador de estado assíncrono. Ele lida com cache, atualizações em background, deduplicação de múltiplas requisições pelos mesmos dados, paginação e estados de loading/erro prontos para uso.
Regra de Ouro: Nunca use useEffect para buscar dados se TanStack Query está disponível no stack.
Padrões de Definição de Queries
O Padrão de Custom Hook (Melhor Prática)
Sempre abstraia chamadas de useQuery em custom hooks para encapsular a lógica de fetching, tipos TypeScript e query keys.
import { useQuery } from '@tanstack/react-query';
// 1. Defina tipos rigorosos
type User = { id: string; name: string; status: 'active' | 'inactive' };
// 2. Defina a função fetcher
const fetchUser = async (userId: string): Promise<User> => {
const res = await fetch(`/api/users/${userId}`);
if (!res.ok) throw new Error('Failed to fetch user');
return res.json();
};
// 3. Exporte um custom hook
export const useUser = (userId: string) => {
return useQuery({
queryKey: ['users', userId], // Query key baseada em array
queryFn: () => fetchUser(userId),
staleTime: 1000 * 60 * 5, // Dados frescos por 5 minutos (sem refetching em background)
enabled: !!userId, // Query dependente: só executa se userId existe
});
};
Query Keys Avançadas
Query keys identificam exclusivamente o cache. Devem ser arrays, e a ordem importa.
// Filtering / Sorting
useQuery({
queryKey: ['issues', { status: 'open', sort: 'desc' }],
queryFn: () => fetchIssues({ status: 'open', sort: 'desc' })
});
// Factory pattern para query keys (Altamente recomendado para aplicações grandes)
export const issueKeys = {
all: ['issues'] as const,
lists: () => [...issueKeys.all, 'list'] as const,
list: (filters: string) => [...issueKeys.lists(), { filters }] as const,
details: () => [...issueKeys.all, 'detail'] as const,
detail: (id: number) => [...issueKeys.details(), id] as const,
};
Mutations & Invalidação de Cache
Mutation Básica com Invalidação
Quando você modifica dados no servidor, deve avisar ao client cache que os dados antigos estão obsoletos.
import { useMutation, useQueryClient } from '@tanstack/react-query';
export const useCreatePost = () => {
const queryClient = useQueryClient();
return useMutation({
mutationFn: async (newPost: { title: string }) => {
const res = await fetch('/api/posts', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(newPost),
});
return res.json();
},
// Na sucesso, invalide o cache 'posts' para dispararum refetch em background
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ['posts'] });
},
});
};
Atualizações Otimistas
Dê ao usuário feedback instantâneo atualizando o cache antes do servidor responder, e reverta se a requisição falhar.
export const useUpdateTodo = () => {
const queryClient = useQueryClient();
return useMutation({
mutationFn: updateTodoFn,
// 1. Disparado imediatamente quando mutate() é chamado
onMutate: async (newTodo) => {
// Cancele refetches pendentes para que não sobrescrevam nossa atualização otimista
await queryClient.cancelQueries({ queryKey: ['todos'] });
// Faça snapshot do valor anterior
const previousTodos = queryClient.getQueryData(['todos']);
// Atualize otimisticamente para o novo valor
queryClient.setQueryData(['todos'], (old: any) =>
old.map((todo: any) => todo.id === newTodo.id ? { ...todo, ...newTodo } : todo)
);
// Retorne um objeto context com o valor salvo em snapshot
return { previousTodos };
},
// 2. Se a mutation falhar, use o context retornado de onMutate para reverter
onError: (err, newTodo, context) => {
queryClient.setQueryData(['todos'], context?.previousTodos);
},
// 3. Sempre refetch após erro ou sucesso para garantir sincronização com servidor
onSettled: () => {
queryClient.invalidateQueries({ queryKey: ['todos'] });
},
});
};
Integração com Next.js App Router
Inicializando o Provider
// app/providers.tsx
'use client'
import { QueryClient, QueryClientProvider } from '@tanstack/react-query'
import { useState } from 'react'
export default function Providers({ children }: { children: React.ReactNode }) {
const [queryClient] = useState(
() =>
new QueryClient({
defaultOptions: {
queries: {
staleTime: 60 * 1000, // 1 minuto
refetchOnWindowFocus: false, // Previne refetching agressivo ao trocar de aba
},
},
})
)
return (
<QueryClientProvider client={queryClient}>
{children}
</QueryClientProvider>
)
}
Server Component Pre-fetching (Hidratação)
Faça pre-fetch de dados no servidor e passe-os ao cliente sem prop-drilling ou initialData.
// app/posts/page.tsx (Server Component)
import { dehydrate, HydrationBoundary, QueryClient } from '@tanstack/react-query';
import PostsList from './PostsList'; // Client Component
export default async function PostsPage() {
const queryClient = new QueryClient();
// Faça pre-fetch dos dados no servidor
await queryClient.prefetchQuery({
queryKey: ['posts'],
queryFn: fetchPostsServerSide,
});
// Desidrate o cache e passe-o para a HydrationBoundary
return (
<HydrationBoundary state={dehydrate(queryClient)}>
<PostsList />
</HydrationBoundary>
);
}
// app/posts/PostsList.tsx (Client Component)
'use client'
import { useQuery } from '@tanstack/react-query';
export default function PostsList() {
// Isso NÃO disparará uma requisição de rede ao montar!
// Lê instantaneamente do cache desidratado do servidor.
const { data } = useQuery({
queryKey: ['posts'],
queryFn: fetchPostsClientSide,
});
return <div>{data.map(post => <p key={post.id}>{post.title}</p>)}</div>;
}
Melhores Práticas
- ✅ Faça: Crie factories de Query Key para que você não erre ao digitar
['users']vs['user']em arquivos diferentes. - ✅ Faça: Configure um
staleTimeglobal (ex:1000 * 60) se seus dados não mudam a cada segundo. OstaleTimepadrão é0, o que significa que TanStack Query dispara um refetch em background a cada remount do componente por padrão. - ✅ Faça: Use
queryClient.setQueryDatacom moderação. Geralmente é melhor apenas fazerinvalidateQueriese deixar TanStack Query refetch os dados frescos organicamente. - ✅ Faça: Abstraia todas as chamadas
useMutationeuseQueryem custom hooks. Views devem apenas dizerconst { mutate } = useCreatePost(). - ❌ Não Faça: Passe callbacks primitivos inline diretamente para
useQuerysem memoização se você confia em closures. (Em vez disso, confie no array de dependênciasqueryKey). - ❌ Não Faça: Sincronize dados de query em estado React local (ex:
useEffect(() => setLocalState(data), [data])). Use os dados de query diretamente. Se você precisa de estado derivado, derive-o durante o render.
Troubleshooting
Problema: Loop infinito de fetching na aba de network.
Solução: Verifique seu queryFn. Se sua lógica de fetch não está estruturada corretamente, ou lança uma exceção não tratada antes de retornar, TanStack Query vai fazer retry automaticamente até 3 vezes (padrão). Se envolvido em um useEffect instável, faz loop infinito. Verifique retry: false para debug.
Problema: Confusão entre staleTime vs gcTime (antigo cacheTime).
Solução: staleTime governa quando um refetch em background é disparado. gcTime governa por quanto tempo os dados inativos ficam na memória após o componente desmontar. Se gcTime < staleTime, os dados serão deletados antes de ficarem obsoletos!