TanStack Query é a solução padrão para gerenciamento de estado do servidor em aplicações React.
useQuery: buscar e cachear dados
useQuery assina um estado do servidor com queryKey como chave de cache.
import { useQuery, QueryClient, QueryClientProvider } from '@tanstack/react-query';
// Setup: wrap your app
const queryClient = new QueryClient({
defaultOptions: {
queries: {
staleTime: 1000 * 60 * 5, // 5 minutes
gcTime: 1000 * 60 * 30, // 30 minutes (formerly cacheTime)
retry: 3,
refetchOnWindowFocus: true,
},
},
});
function App() {
return (
<QueryClientProvider client={queryClient}>
<MyApp />
</QueryClientProvider>
);
}
// Basic query
interface User {
id: number;
name: string;
email: string;
}
function useUser(userId: number) {
return useQuery<User>({
queryKey: ['user', userId], // cache key — must be unique
queryFn: async () => {
const res = await fetch(`/api/users/${userId}`);
if (!res.ok) throw new Error('Failed to fetch user');
return res.json();
},
enabled: userId > 0, // only run if userId is valid
staleTime: 1000 * 60 * 10, // override global default
});
}
function UserProfile({ userId }: { userId: number }) {
const { data, isLoading, isError, error } = useUser(userId);
if (isLoading) return <div>Loading...</div>;
if (isError) return <div>Error: {error.message}</div>;
return <div>{data.name} — {data.email}</div>;
}useMutation: criar, atualizar, excluir
useMutation trata operações CRUD com callbacks onSuccess.
import { useMutation, useQueryClient } from '@tanstack/react-query';
interface CreatePostInput {
title: string;
body: string;
userId: number;
}
function useCreatePost() {
const queryClient = useQueryClient();
return useMutation({
mutationFn: async (input: CreatePostInput) => {
const res = await fetch('/api/posts', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(input),
});
if (!res.ok) throw new Error('Failed to create post');
return res.json();
},
onSuccess: (newPost) => {
// Invalidate and refetch posts list
queryClient.invalidateQueries({ queryKey: ['posts'] });
// Or add directly to cache (no refetch needed)
queryClient.setQueryData(['post', newPost.id], newPost);
},
onError: (error) => {
console.error('Failed to create post:', error);
},
});
}
function CreatePostForm() {
const mutation = useCreatePost();
const handleSubmit = (e: React.FormEvent) => {
e.preventDefault();
mutation.mutate({ title: 'New Post', body: 'Content', userId: 1 });
};
return (
<form onSubmit={handleSubmit}>
<button type="submit" disabled={mutation.isPending}>
{mutation.isPending ? 'Creating...' : 'Create Post'}
</button>
{mutation.isError && <p>Error: {mutation.error.message}</p>}
{mutation.isSuccess && <p>Post created!</p>}
</form>
);
}Atualizações otimistas para UI instantânea
Atualizações otimistas aplicam mudanças antes da resposta do servidor.
import { useMutation, useQueryClient } from '@tanstack/react-query';
interface Todo {
id: number;
title: string;
completed: boolean;
}
function useToggleTodo() {
const queryClient = useQueryClient();
return useMutation({
mutationFn: (todo: Todo) =>
fetch(`/api/todos/${todo.id}`, {
method: 'PATCH',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ completed: !todo.completed }),
}).then(r => r.json()),
// Optimistic update — runs BEFORE the mutation
onMutate: async (updatedTodo) => {
// Cancel outgoing refetches
await queryClient.cancelQueries({ queryKey: ['todos'] });
// Snapshot current data for rollback
const previousTodos = queryClient.getQueryData<Todo[]>(['todos']);
// Optimistically update the cache
queryClient.setQueryData<Todo[]>(['todos'], (old) =>
old?.map(todo =>
todo.id === updatedTodo.id
? { ...todo, completed: !todo.completed }
: todo
)
);
return { previousTodos }; // context for onError
},
// If mutation fails, roll back to snapshot
onError: (_err, _variables, context) => {
if (context?.previousTodos) {
queryClient.setQueryData(['todos'], context.previousTodos);
}
},
// Always refetch after error or success
onSettled: () => {
queryClient.invalidateQueries({ queryKey: ['todos'] });
},
});
}Consultas infinitas para paginação
useInfiniteQuery trata paginação baseada em cursor.
import { useInfiniteQuery } from '@tanstack/react-query';
interface Page {
items: Post[];
nextCursor?: string;
}
function useInfinitePosts() {
return useInfiniteQuery<Page>({
queryKey: ['posts', 'infinite'],
queryFn: async ({ pageParam }) => {
const url = pageParam
? `/api/posts?cursor=${pageParam}`
: '/api/posts';
return fetch(url).then(r => r.json());
},
initialPageParam: undefined as string | undefined,
getNextPageParam: (lastPage) => lastPage.nextCursor,
});
}
function InfinitePostList() {
const {
data,
fetchNextPage,
hasNextPage,
isFetchingNextPage,
} = useInfinitePosts();
return (
<div>
{data?.pages.flatMap(page => page.items).map(post => (
<div key={post.id}>{post.title}</div>
))}
{hasNextPage && (
<button onClick={() => fetchNextPage()} disabled={isFetchingNextPage}>
{isFetchingNextPage ? 'Loading...' : 'Load more'}
</button>
)}
</div>
);
}Padrões de chave de consulta e pré-busca
Factories de chave consistentes evitam erros de cache.
// Query key patterns — factory functions for consistency
const queryKeys = {
all: ['posts'] as const,
lists: () => [...queryKeys.all, 'list'] as const,
list: (filters: string) => [...queryKeys.lists(), { filters }] as const,
details: () => [...queryKeys.all, 'detail'] as const,
detail: (id: number) => [...queryKeys.details(), id] as const,
};
// Usage:
useQuery({ queryKey: queryKeys.detail(1), queryFn: ... });
// Invalidate all posts:
queryClient.invalidateQueries({ queryKey: queryKeys.all });
// Invalidate only lists:
queryClient.invalidateQueries({ queryKey: queryKeys.lists() });
// Prefetching for better UX
async function prefetchUser(userId: number) {
await queryClient.prefetchQuery({
queryKey: ['user', userId],
queryFn: () => fetchUser(userId),
staleTime: 10 * 60 * 1000, // only prefetch if > 10 min stale
});
}
// On hover: prefetch before click
<link onMouseEnter={() => prefetchUser(userId)} href={`/users/${userId}`}>
View Profile
</link>TanStack Query vs outras soluções
| Feature | TanStack Query | Redux Toolkit | SWR | Apollo |
|---|---|---|---|---|
| Caching | Built-in (staleTime, gcTime) | Manual | Built-in | Built-in (Apollo-only) |
| Optimistic updates | First-class | Manual | Basic | First-class |
| Infinite query | useInfiniteQuery | Manual | useSWRInfinite | fetchMore |
| Mutations | useMutation | RTK Query | No (use fetch) | useMutation |
| DevTools | Excellent | Excellent | Basic | Good |
| REST + GraphQL | Both | Both | REST focused | GraphQL only |
Boas práticas
- Usar factories de chave de consulta para consistência.
- Definir staleTime de acordo com os requisitos de frescura dos dados.
- Sempre fornecer estados de carregamento e erro.
- Usar atualizações otimistas para ações rápidas.
- Pré-buscar dados em hover ou transições de rota.
FAQ
Diferença entre staleTime e gcTime?
staleTime controla quando os dados ficam obsoletos, gcTime quanto tempo ficam na memória.
Como TanStack Query lida com condições de corrida?
Deduplica automaticamente requisições idênticas.
TanStack Query com Redux ou Zustand?
Complementares: TanStack Query para estado do servidor, Zustand para estado do cliente.
Tratar erros de autenticação globalmente?
Usar o callback onError global do QueryClient.
Mudanças no TanStack Query v5?
cacheTime renomeado para gcTime, parâmetro queryFn como objeto, suporte RSC.