TanStack Query (anteriormente React Query) é o padrão ouro para gerenciamento de estado de servidor em aplicativos React, Vue, Solid e Angular em 2026. Ele elimina a complexidade de busca manual de dados, armazenamento em cache e sincronização, substituindo centenas de linhas de código useEffect por ganchos simples e poderosos.
📋 Table of Contents
Por que consultar o TanStack?
- Sem clichê— substitui os padrões Redux + fetch + useEffect
- Cache automático— dados armazenados em cache por chave, atualizados quando necessário
- Busca em segundo plano— dados obsoletos mostrados instantaneamente, dados novos em segundo plano
- Estados de carregamento/erro— integrado, sem gerenciamento de estado manual
- Atualizações otimistas— Atualizações da UI antes da confirmação do servidor
- Ferramentas de desenvolvimento– inspecionar o cache, buscar novamente, invalidar a extensão do navegador
Instalação e configuração
npm install @tanstack/react-query @tanstack/react-query-devtools
# Optional: axios for HTTP
npm install axios
// main.tsx
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
import { ReactQueryDevtools } from '@tanstack/react-query-devtools';
const queryClient = new QueryClient({
defaultOptions: {
queries: {
staleTime: 5 * 60 * 1000, // 5 minutes (data considered fresh)
gcTime: 10 * 60 * 1000, // 10 minutes (cache garbage collection)
retry: 2, // retry failed requests twice
refetchOnWindowFocus: true, // refetch when tab becomes active
},
},
});
function App() {
return (
<QueryClientProvider client={queryClient}>
<Router />
<ReactQueryDevtools initialIsOpen={false} />
</QueryClientProvider>
);
}
useQuery – Buscando dados
import { useQuery, useQueryClient } from '@tanstack/react-query';
import axios from 'axios';
interface User {
id: number;
name: string;
email: string;
}
// Query function
async function fetchUser(id: number): Promise<User> {
const { data } = await axios.get(`/api/users/${id}`);
return data;
}
// Component using query
function UserProfile({ userId }: { userId: number }) {
const {
data: user,
isLoading,
isError,
error,
isFetching, // true when refetching in background
isStale, // true when data is stale
refetch, // manually trigger refetch
} = useQuery({
queryKey: ['users', userId], // unique cache key
queryFn: () => fetchUser(userId),
enabled: userId > 0, // only fetch when userId is valid
staleTime: 60_000, // fresh for 1 minute
select: (data) => data, // transform data before returning
});
if (isLoading) return <Skeleton />;
if (isError) return <ErrorBoundary error={error} />;
return (
<div>
{isFetching && <small>Refreshing...</small>}
<h1>{user.name}</h1>
<p>{user.email}</p>
<button onClick={() => refetch()}>Refresh</button>
</div>
);
}
Consultas Paralelas
// Fetch multiple queries simultaneously
function Dashboard() {
const [userQuery, postsQuery, statsQuery] = useQueries({
queries: [
{ queryKey: ['user', 1], queryFn: () => fetchUser(1) },
{ queryKey: ['posts'], queryFn: fetchPosts },
{ queryKey: ['stats'], queryFn: fetchStats },
],
});
if (userQuery.isLoading || postsQuery.isLoading) return <Loading />;
return (
<div>
<UserCard user={userQuery.data} />
<PostList posts={postsQuery.data} />
<StatsPanel stats={statsQuery.data} />
</div>
);
}
useMutation — Criando, Atualizando, Excluindo
import { useMutation, useQueryClient } from '@tanstack/react-query';
async function createPost(post: { title: string; content: string }) {
const { data } = await axios.post('/api/posts', post);
return data;
}
function NewPostForm() {
const queryClient = useQueryClient();
const { mutate, isPending, isError, error } = useMutation({
mutationFn: createPost,
// Called when mutation succeeds
onSuccess: (newPost) => {
// Option 1: Invalidate cache (triggers refetch)
queryClient.invalidateQueries({ queryKey: ['posts'] });
// Option 2: Update cache directly (no refetch)
queryClient.setQueryData(['posts'], (old: Post[]) => [...old, newPost]);
// Show toast notification
toast.success('Post created!');
},
onError: (error) => {
toast.error(`Failed: ${error.message}`);
},
});
const handleSubmit = (e: React.FormEvent<HTMLFormElement>) => {
e.preventDefault();
const form = e.currentTarget;
mutate({
title: (form.title as HTMLInputElement).value,
content: (form.content as HTMLTextAreaElement).value,
});
};
return (
<form onSubmit={handleSubmit}>
<input name="title" required placeholder="Title" />
<textarea name="content" required placeholder="Content" />
<button type="submit" disabled={isPending}>
{isPending ? 'Creating...' : 'Create Post'}
</button>
{isError && <p className="error">{error.message}</p>}
</form>
);
}
Atualizações otimistas
const { mutate: likePost } = useMutation({
mutationFn: (postId: number) => axios.post(`/api/posts/${postId}/like`),
onMutate: async (postId) => {
// Cancel any outgoing refetches
await queryClient.cancelQueries({ queryKey: ['posts', postId] });
// Snapshot the previous value
const previousPost = queryClient.getQueryData(['posts', postId]);
// Optimistically update the UI
queryClient.setQueryData(['posts', postId], (old: Post) => ({
...old,
likes: old.likes + 1,
}));
// Return context for rollback
return { previousPost };
},
// Roll back on error
onError: (error, postId, context) => {
queryClient.setQueryData(['posts', postId], context?.previousPost);
toast.error('Failed to like post');
},
// Always refetch after mutation
onSettled: (data, error, postId) => {
queryClient.invalidateQueries({ queryKey: ['posts', postId] });
},
});
Consultas infinitas
import { useInfiniteQuery } from '@tanstack/react-query';
function InfinitePostList() {
const {
data,
fetchNextPage,
hasNextPage,
isFetchingNextPage,
} = useInfiniteQuery({
queryKey: ['posts', 'infinite'],
queryFn: ({ pageParam = 1 }) => fetchPosts(pageParam),
initialPageParam: 1,
getNextPageParam: (lastPage, pages) =>
lastPage.hasMore ? pages.length + 1 : undefined,
});
return (
<div>
{data?.pages.map((page, i) => (
<React.Fragment key={i}>
{page.posts.map(post => <PostCard key={post.id} post={post} />)}
</React.Fragment>
))}
<button onClick={() => fetchNextPage()} disabled={!hasNextPage || isFetchingNextPage}>
{isFetchingNextPage ? 'Loading...' : hasNextPage ? 'Load More' : 'All loaded'}
</button>
</div>
);
}
Ganchos de consulta personalizados
// hooks/useUsers.ts — reusable query hooks
export function useUsers(filters?: UserFilters) {
return useQuery({
queryKey: ['users', filters],
queryFn: () => fetchUsers(filters),
select: (data) => ({
users: data.users,
total: data.total,
}),
});
}
export function useUser(id: number) {
const queryClient = useQueryClient();
return useQuery({
queryKey: ['users', id],
queryFn: () => fetchUser(id),
// Pre-populate from list query
initialData: () => {
const usersQuery = queryClient.getQueryData<UsersResponse>(['users']);
return usersQuery?.users.find(u => u.id === id);
},
initialDataUpdatedAt: () =>
queryClient.getQueryState(['users'])?.dataUpdatedAt,
});
}
export function useUpdateUser() {
const queryClient = useQueryClient();
return useMutation({
mutationFn: (user: Partial<User> & { id: number }) =>
axios.patch(`/api/users/${user.id}`, user).then(r => r.data),
onSuccess: (updated) => {
queryClient.setQueryData(['users', updated.id], updated);
queryClient.invalidateQueries({ queryKey: ['users'] });
},
});
}
O TanStack Query em 2026 elimina 80% do padrão no código típico de busca de dados do React. Comece com useQuery para leituras e useMutation para gravações. Use invalidateQueries para atualizações de cache simples e setQueryData para atualizações otimistas. O investimento em aprendizagem paga dividendos em cada projeto.
🔗 Share this article
✍️ Leave a Comment