Busque em todas as páginas da documentação
🤖 Read the SystemsArchitect.io Blog for over 100+ cloud architecture articles 🔥
npm install @tanstack/react-query// app/providers.tsx
"use client";
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
import { useState } from "react";
export function QueryProvider({ children }: { children: React.ReactNode }) {
const [queryClient] = useState(
() =>
new QueryClient({
defaultOptions: {
queries: {
staleTime: 60 * 1000, // 1 minute
refetchOnWindowFocus: false,
},
},
})
);
return (
<QueryClientProvider client={queryClient}>
{children}
</QueryClientProvider>
);
}// app/layout.tsx
import { QueryProvider } from "./providers";
export default function RootLayout({ children }: { children: React.ReactNode }) {
return (
<html lang="en">
<body>
<QueryProvider>{children}</QueryProvider>
</body>
</html>
);
}"use client";
import { useQuery } from "@tanstack/react-query";
function usePosts() {
return useQuery({
queryKey: ["posts"],
queryFn: async () => {
const res = await fetch("/api/posts");
if (!res.ok) throw new Error("Failed to fetch posts");
return res.json() as Promise<Post[]>;
},
});
}Quando usar isso: Você precisa buscar dados no cliente com cache automático, refetching em segundo plano, estados de carregamento/erro e invalidação de cache após mutações.
// app/components/PostManager.tsx
"use client";
import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query";
interface Post {
id: number;
title: string;
body: string;
}
function usePosts() {
return useQuery({
queryKey: ["posts"],
queryFn: async (): Promise<Post[]> => {
const res = await fetch("https://jsonplaceholder.typicode.com/posts?_limit=10");
if (!res.ok) throw new Error("Failed to fetch");
return res.json();
},
});
}
function useCreatePost() {
const queryClient = useQueryClient();
return useMutation({
mutationFn: async (newPost: { title: string; body: string }) => {
const res = await fetch("https://jsonplaceholder.typicode.com/posts", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(newPost),
});
return res.json() as Promise<Post>;
},
onMutate: async (newPost) => {
// Cancel in-flight queries
await queryClient.cancelQueries({ queryKey: ["posts"] });
// Snapshot previous value
const previousPosts = queryClient.getQueryData<Post[]>(["posts"]);
// Optimistically update
queryClient.setQueryData<Post[]>(["posts"], (old) => [
{ id: Date.now(), ...newPost } as Post,
...(old ?? []),
]);
return { previousPosts };
},
onError: (_err, _newPost, context) => {
// Rollback on error
queryClient.setQueryData(["posts"], context?.previousPosts);
},
onSettled: () => {
// Refetch to ensure server state
queryClient.invalidateQueries({ queryKey: ["posts"] });
},
});
}
export default function PostManager() {
const { data: posts, isLoading, error } = usePosts();
const createPost = useCreatePost();
if (isLoading) return <div className="p-6">Loading posts...</div>;
if (error) return <div className="p-6 text-red-600">Error: {error.message}</div>;
return (
<div className="max-w-2xl mx-auto p-6">
<form
onSubmit={(e) => {
e.preventDefault();
const form = e.target as HTMLFormElement;
const title = (form.elements.namedItem("title") as HTMLInputElement).value;
const body = (form.elements.namedItem("body") as HTMLTextAreaElement).value;
createPost.mutate({ title, body });
form.reset();
}}
className="mb-6 space-y-3"
>
<input
name="title"
placeholder="Post title"
required
className="w-full border rounded px-3 py-2"
/>
<textarea
name="body"
placeholder="Post body"
required
rows={3}
className="w-full border rounded px-3 py-2"
/>
<button
type="submit"
disabled={createPost.isPending}
className="bg-blue-600 text-white px-4 py-2 rounded disabled:opacity-50"
>
{createPost.isPending ? "Creating..." : "Create Post"}
</button>
</form>
<ul className="space-y-3">
{posts?.map((post) => (
<li key={post.id} className="border rounded p-3">
<h3 className="font-bold">{post.title}</h3>
<p className="text-gray-600 text-sm mt-1">{post.body}</p>
</li>
))}
</ul>
</div>
);
}O que isso demonstra:
useQuery para buscar dados com estados de carregamento e errouseMutation com atualizações otimistas e rollback em caso de erroinvalidateQueries após mutaçõesuseQueryClient para acessar o cache diretamentequeryKey; chaves idênticas compartilham dados em cachestaleTime controla por quanto tempo os dados são considerados frescos; dentro desta janela, nenhum refetch ocorregcTime (anteriormente cacheTime) controla por quanto tempo entradas de cache inativas são mantidas (padrão de 5 minutos)queryKey suporta arrays e objetos aninhados; as chaves são serializadas e comparadas profundamenteQueries dependentes (buscar B apenas após A ser concluído):
function useUserPosts(userId: number | undefined) {
return useQuery({
queryKey: ["posts", userId],
queryFn: () => fetch(`/api/users/${userId}/posts`).then((r) => r.json()),
enabled: !!userId, // Only fetch when userId is available
});
}
function UserPosts() {
const { data: user } = useQuery({
queryKey: ["user"],
queryFn: () => fetch("/api/me").then((r) => r.json()),
});
const { data: posts } = useUserPosts(user?.id);
// posts query waits until user is loaded
}Scroll infinito / paginação:
import { useInfiniteQuery } from "@tanstack/react-query";
function useInfinitePosts() {
return useInfiniteQuery({
queryKey: ["posts", "infinite"],
queryFn: async ({ pageParam }) => {
const res = await fetch(`/api/posts?cursor=${pageParam}&limit=10`);
return res.json() as Promise<{
posts: Post[];
nextCursor: string | null;
}>;
},
initialPageParam: "",
getNextPageParam: (lastPage) => lastPage.nextCursor,
});
}
function InfinitePostList() {
const { data, fetchNextPage, hasNextPage, isFetchingNextPage } =
useInfinitePosts();
const allPosts = data?.pages.flatMap((page) => page.posts) ?? [];
return (
<div>
{allPosts.map((post) => (
<div key={post.id}>{post.title}</div>
))}
{hasNextPage && (
<button onClick={() => fetchNextPage()} disabled={isFetchingNextPage}>
{isFetchingNextPage ? "Loading more..." : "Load More"}
</button>
)}
</div>
);
}Prefetching para navegação instantânea:
function PostList() {
const queryClient = useQueryClient();
const { data: posts } = usePosts();
return (
<ul>
{posts?.map((post) => (
<li
key={post.id}
onMouseEnter={() => {
queryClient.prefetchQuery({
queryKey: ["post", post.id],
queryFn: () =>
fetch(`/api/posts/${post.id}`).then((r) => r.json()),
});
}}
>
<Link href={`/posts/${post.id}`}>{post.title}</Link>
</li>
))}
</ul>
);
}SSR hydration com Next.js App Router:
// app/posts/page.tsx (Server Component)
import {
dehydrate,
HydrationBoundary,
QueryClient,
} from "@tanstack/react-query";
import PostList from "./PostList";
export default async function PostsPage() {
const queryClient = new QueryClient();
await queryClient.prefetchQuery({
queryKey: ["posts"],
queryFn: async () => {
const res = await fetch("https://jsonplaceholder.typicode.com/posts?_limit=10");
return res.json();
},
});
return (
<HydrationBoundary state={dehydrate(queryClient)}>
<PostList />
</HydrationBoundary>
);
}useQuery infere o tipo de dado do tipo de retorno de queryFn; anote a função para digitação explícitaqueryKey aceita readonly unknown[]; use as const para tipos de tupla estritosuseMutation: useMutation<TData, TError, TVariables, TContext>select transforma dados e refina o tipo retornado// Explicit typing with select
const { data } = useQuery({
queryKey: ["posts"],
queryFn: async (): Promise<Post[]> => {
const res = await fetch("/api/posts");
return res.json();
},
select: (posts) => posts.filter((p) => p.id > 5), // data is Post[]
});
// Query key factory pattern
const postKeys = {
all: ["posts"] as const,
lists: () => [...postKeys.all, "list"] as const,
list: (filters: PostFilters) => [...postKeys.lists(), filters] as const,
details: () => [...postKeys.all, "detail"] as const,
detail: (id: number) => [...postKeys.details(), id] as const,
};QueryClient em Server Component - Criar QueryClient fora de um componente ou em um Server Component causa estado compartilhado entre requisições. Correção: Crie QueryClient dentro de useState no provider, ou crie uma nova instância por requisição em funções de prefetch do servidor.
Confusão entre staleTime e gcTime - staleTime controla o comportamento de refetch; gcTime controla a expiração do cache. Definir staleTime: Infinity impede refetches, mas o cache ainda pode ser coletado pelo garbage collector. Correção: Entenda ambos: staleTime = "por quanto tempo os dados são considerados frescos?" gcTime = "por quanto tempo manter entradas de cache inativas?"
Query key deve ser serializável - Funções, instâncias de classe ou referências circulares em chaves de query causam problemas. Correção: Use apenas strings, números, objetos e arrays em chaves de query.
Mutações não invalidam automaticamente - Ao contrário de alguns frameworks, mutações não fazem refetch de queries relacionadas automaticamente. Correção: Use o callback onSettled ou onSuccess com queryClient.invalidateQueries().
Double fetching em Strict Mode - React Strict Mode em desenvolvimento monta componentes duas vezes, causando double fetches. Correção: Este é um comportamento esperado apenas em modo de desenvolvimento. Em produção, as queries buscam uma vez. TanStack Query desduplica requisições idênticas concorrentes.
Erro de tipo na atualização otimista - A forma dos dados otimistas deve corresponder exatamente aos dados da query. Correção: Use queryClient.getQueryData<Post[]>() com o tipo genérico correto e certifique-se de que os itens otimistas tenham todos os campos necessários.
| Biblioteca | Melhor Para | Contrapartida |
|---|---|---|
| TanStack Query | Busca de dados complexa no cliente | Dependência adicional, curva de aprendizado |
| SWR | Busca de dados simples com cache | Menos recursos (sem mutações, sem devtools) |
| Next.js Server Components | Busca de dados no lado do servidor | Sem cache no cliente, sem refetch em segundo plano |
| RTK Query | Aplicativos baseados em Redux | Vinculado ao Redux, configuração mais pesada |
| Apollo Client | APIs GraphQL | Exagero para REST, bundle maior |
queryKey é um array que identifica unicamente os dados em cache de uma query["posts", { status: "draft" }] funcionastaleTime = por quanto tempo os dados são considerados frescos (sem refetch durante esta janela)gcTime (anteriormente cacheTime) = por quanto tempo as entradas de cache inativas são mantidas antes da coleta de lixostaleTime: Infinity impede todos os refetches automáticosgcTime padrão é de 5 minutos; após isso, os dados da query desmontada são removidosconst queryClient = useQueryClient();
const mutation = useMutation({
mutationFn: createPost,
onSettled: () => {
queryClient.invalidateQueries({ queryKey: ["posts"] });
},
});Use invalidateQueries em onSettled ou onSuccess para refetch de dados stale.
onMutate: cancela queries em andamento, captura os dados atuais, atualiza o cache otimisticamenteonError: reverte para o snapshot se a mutação falharonSettled: invalida queries para refetch do servidor, independentemente de sucesso/falhaQueryClient no nível do módulo é compartilhado entre todas as requisições do servidorQueryClient dentro de useState no componente providerconst { data: user } = useQuery({
queryKey: ["user"],
queryFn: fetchUser,
});
const { data: posts } = useQuery({
queryKey: ["posts", user?.id],
queryFn: () => fetchUserPosts(user!.id),
enabled: !!user?.id,
});A opção enabled impede que a query seja executada até que a dependência esteja disponível.
QueryClientdehydrate(queryClient) para serializar o cache<HydrationBoundary state={dehydratedState}>useQuery com a mesma chave obtêm dados instantaneamenteconst { data, fetchNextPage, hasNextPage } = useInfiniteQuery({
queryKey: ["posts", "infinite"],
queryFn: ({ pageParam }) =>
fetch(`/api/posts?cursor=${pageParam}`).then(r => r.json()),
initialPageParam: "",
getNextPageParam: (lastPage) => lastPage.nextCursor,
});
const allPosts = data?.pages.flatMap(p => p.posts) ?? [];// useQuery infers data type from queryFn return type
const { data } = useQuery({
queryKey: ["posts"],
queryFn: async (): Promise<Post[]> => {
const res = await fetch("/api/posts");
return res.json();
},
});
// data is Post[] | undefined
// useMutation generics: <TData, TError, TVariables, TContext>
const mutation = useMutation<Post, Error, { title: string }>({
mutationFn: (vars) => createPost(vars),
});const postKeys = {
all: ["posts"] as const,
lists: () => [...postKeys.all, "list"] as const,
list: (filters: Filters) => [...postKeys.lists(), filters] as const,
detail: (id: number) => [...postKeys.all, "detail", id] as const,
};invalidateQueries({ queryKey: postKeys.all }) limpa todas as queries de postsas const fornece tipos de tupla estritos para melhor inferência do TypeScript<li
onMouseEnter={() => {
queryClient.prefetchQuery({
queryKey: ["post", post.id],
queryFn: () => fetchPost(post.id),
});
}}
>
<Link href={`/posts/${post.id}`}>{post.title}</Link>
</li>Os dados são cacheados antes que o usuário clique, tornando a próxima página instantânea.
Revisado por Chris St. John·Última atualização: 16 de jul. de 2026
🤖 Read the SystemsArchitect.io Blog for over 100+ cloud architecture articles 🔥