React: TanStack Query (React Query)
Última atualização: 2026-08-26
Na última aula, Tom usou hooks personalizados e o axios para encapsular as solicitações HTTP, mas surgiram novos problemas: tanto o painel quanto a barra lateral exibem o número de usuários, e cada componente envia sua própria solicitação (desperdiçando largura de banda e recursos do servidor); quando um usuário altera seu nome de usuário na Página A, os dados na Página B permanecem desatualizados; e, após enviar um formulário de edição, o usuário precisa acionar manualmente uma atualização dos dados. Ele percebeu: É necessária uma solução de “gerenciamento de estado no lado do servidor” para tratar os dados da API como um tipo especial de estado — um que seja armazenado em cache, tenha um prazo de validade e possa ser sincronizado automaticamente.
1. O que você vai aprender
- O useQuery gerencia a recuperação de dados (busca, armazenamento em cache e nova busca automática)
- O
useMutationgerencia as gravações de dados (inserções, exclusões, atualizações e consultas com dados desatualizados) - staleTime / cacheTime controlam a política de armazenamento em cache
- As atualizações otimistas permitem que a interface do usuário responda em tempo real
- Distinguindo entre estado do lado do servidor e estado do lado do cliente
2. Diagramas conceituais
flowchart TD
A[useQuery Call] --> B{Cache Hit?}
B -->|No matches found| C[Initiate API Request]
B -->|Hit but Expired| C
B -->|On Target and Fresh| D[Return cached data directly]
C --> E[Cached Data]
E --> F[Rendering Component]
F --> G{staleTime Has it expired??}
G -->|Not yet due| H[Data labeled as"Fresh"]
G -->|Expired| I[Data labeled as"Expired"]
I --> J{Bring the window back to the foreground?}
J -->|is | C
I --> K{refetchInterval?}
K -->|is | C
I --> L{There's something new useMutation<br/>invalidate?}
L -->|is | C
style A fill:#e1f5fe,stroke:#0288d1
style E fill:#fff3e0,stroke:#f57c00
style H fill:#e8f5e9,stroke:#388e3c
style I fill:#ffcdd2,stroke:#d32f2f
O mecanismo central do TanStack Query: ler primeiro do cache → marcar como atualizado ou expirado → acionar automaticamente uma nova solicitação quando expirado.
3. Um cenário da vida real
O painel do Tom precisa exibir o número total de usuários, o número total de pedidos e uma lista dos pedidos recentes. Esses dados provêm de três APIs diferentes e devem ser mantidos atualizados em tempo real — quando um usuário modifica dados em outra página e retorna ao painel, ele deve ver os resultados mais recentes. Além disso, após o envio do formulário “Adicionar produto”, a lista de produtos deve ser atualizada automaticamente, sem a necessidade de atualizar manualmente a página.
(1) Problema: Gerenciar o cache manualmente é muito difícil
Sem o TanStack Query, Tom teve que cuidar disso sozinho:
// Manually Manage the Cache — You have to write similar logic for each component.
function Dashboard() {
const [data, setData] = useState(null)
const [loading, setLoading] = useState(true)
useEffect(() => {
fetch('/api/stats').then(res => res.json())
.then(d => { setData(d); setLoading(false) })
.catch(e => { setLoading(false) })
}, [])
// Question 1:Switch to another page and then come back → Resubmit Request(Waste!)
// Question 2:If another component also needs the same data → Duplicate Request
// Question 3:The data does not refresh automatically,Users may be seeing data from a few minutes ago
}
O TanStack Query resolve todos os problemas acima com um único hook useQuery.
| Recurso | Busca manual + useEffect | Consulta no TanStack |
|---|---|---|
| Gerenciamento de cache | Sem cache; os dados são perdidos ao serem removidos | Armazenamento automático em cache; gerenciado pela queryKey |
| Solicitação duplicada | Vários componentes usando os mesmos dados → Solicitações duplicadas | Remover duplicatas automaticamente; solicitar apenas uma vez |
| Atualização automática | Nenhuma | Recuperação automática ao receber o foco da janela/ao se reconectar |
| Atualização do backend | Nenhuma | O parâmetro staleTime controla a atualização do backend |
| Status de carregamento/erro | Carregamento/erro gerenciado manualmente | Dados/carregamento/erro fornecidos automaticamente |
| Atualização otimista | Implementação manual | onMutate + onError Revertida |
npm install @tanstack/react-query
(2) Inicializar o provedor
import { QueryClient, QueryClientProvider } from '@tanstack/react-query'
// Create QueryClient(Usually at app Entrance)
const queryClient = new QueryClient({
defaultOptions: {
queries: {
staleTime: 30 * 1000, // Default 30 Data within seconds is considered"Fresh"
cacheTime: 5 * 60 * 1000, // Cache Retention 5 minutes
retry: 3, // Failure retry 3 times
refetchOnWindowFocus: true, // Refresh when the window returns to the foreground
}
}
})
function App() {
return (
<QueryClientProvider client={queryClient}>
<YourApp />
</QueryClientProvider>
)
}
4. useQuery: Recuperação de dados
▶ Exemplo 1: Recuperação de dados do painel
import { useQuery } from '@tanstack/react-query'
// Encapsulated API Function
async function fetchDashboardStats() {
const response = await fetch('/api/dashboard/stats')
if (!response.ok) throw new Error('Failed to retrieve statistics')
return response.json()
}
function Dashboard() {
// useQuery One line of code replaced useState + useEffect + Manual Caching
const {
data: stats, // Response Data
isLoading, // Loading for the first time(When there is no cache)
isFetching, // Is a request being made?(Including background retries)
error, // Error Object
refetch // Manually Trigger Retrieval
} = useQuery({
queryKey: ['dashboardStats'], // Unique Identifier,Used for caching matches
queryFn: fetchDashboardStats, // Data Retrieval Functions
staleTime: 30 * 1000, // 30 Do not resend the request within seconds
retry: 3, // Failure retry 3 times
})
if (isLoading) return <DashboardSkeleton />
if (error) return <ErrorPanel message={error.message} onRetry={refetch} />
return (
<div className="dashboard">
<StatCard title="Total Number of Users" value={stats.users} />
<StatCard title="Total Number of Orders" value={stats.orders} />
<StatCard title="Total Revenue" value={`$${stats.revenue}`} />
</div>
)
}
// The sidebar also displays the number of users — Using the same queryKey,No duplicate requests!
function Sidebar() {
const { data: stats } = useQuery({
queryKey: ['dashboardStats'],
queryFn: fetchDashboardStats,
staleTime: 30 * 1000,
})
return (
<aside>
<p>Users Online:{stats?.onlineUsers ?? '...'}</p>
</aside>
)
}
Mecanismo-chave: Instâncias idênticas de queryKey compartilham o mesmo cache. O Painel e a Barra Lateral utilizam o mesmo ['dashboardStats'], e o TanStack Query deduplica automaticamente as solicitações — cada componente aciona apenas uma solicitação de API, e ambos os componentes são atualizados simultaneamente assim que os dados são retornados.
▶ Exemplo 2: Consultas com parâmetros
function ProductDetail({ productId }) {
const { data, isLoading, error } = useQuery({
queryKey: ['product', productId], // queryKey Includes parameters
queryFn: async () => {
const res = await fetch(`/api/products/${productId}`)
if (!res.ok) throw new Error('The product does not exist.')
return res.json()
},
enabled: !!productId, // productId Do not send a request if it is empty
staleTime: 60 * 1000,
})
if (isLoading) return <p>Loading product details...</p>
if (error) return <p>Error:{error.message}</p>
return (
<div>
<h2>{data.name}</h2>
<p className="price">${data.price}</p>
<p>{data.description}</p>
</div>
)
}
O significado dos parâmetros incluídos em queryKey: O TanStack Query utiliza queryKey como um identificador único para o cache. ['product', 1] e ['product', 2] são dois caches distintos que não interferem um no outro. Quando productId muda de 1 para 2, o sistema prioriza a leitura de ['product', 2] do cache; se estiver armazenado em cache e não tiver expirado, ele é renderizado diretamente; caso contrário, ele envia uma solicitação.
▶ Exemplo: Campos-chave retornados por useQuery
| Campo | Significado | Caso de uso |
|---|---|---|
data |
Dados da última resposta bem-sucedida | Exibir interface do usuário |
isLoading |
Primeiro carregamento sem dados em cache | Exibir a tela básica no primeiro carregamento |
isFetching |
Quaisquer solicitações em andamento (incluindo novas tentativas em segundo plano) | Exibir indicador de atualização em segundo plano |
error |
Objeto de erro para solicitação com falha | Exibir mensagem de erro |
refetch |
Função para acionar manualmente uma nova solicitação | Botão “Atualizar” |
isStale |
Os dados estão desatualizados? | Exibir avisos de atualização condicionalmente |
5. useMutation: Gravação de dados
Use useQuery para leituras e useMutation para gravações. Essa é a regra de ouro do TanStack Query.
▶ Exemplo 3: Adicionar um produto e atualizar a lista
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'
// Product List Search
function useProducts() {
return useQuery({
queryKey: ['products'],
queryFn: async () => {
const res = await fetch('/api/products')
return res.json()
}
})
}
function ProductManager() {
const queryClient = useQueryClient()
// Add a product mutation
const addProductMutation = useMutation({
mutationFn: async (newProduct) => {
const res = await fetch('/api/products', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(newProduct)
})
if (!res.ok) throw new Error('Failed to add')
return res.json()
},
// Steps to Take After Success:Invalidate the product list cache,Trigger a re-request
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ['products'] })
// Optional:Display a success message at the same time
alert('Item added successfully!')
},
// Error Handling
onError: (error) => {
alert(`Failed to add:${error.message}`)
}
})
// Data
const { data: products, isLoading } = useProducts()
// Form Submission
function handleSubmit(event) {
event.preventDefault()
const formData = new FormData(event.target)
const newProduct = {
name: formData.get('name'),
price: Number(formData.get('price'))
}
addProductMutation.mutate(newProduct)
event.target.reset()
}
return (
<div>
<h2>Product Management</h2>
<form onSubmit={handleSubmit}>
<input name="name" placeholder="Product Name" required />
<input name="price" type="number" placeholder="Price" required />
<button type="submit" disabled={addProductMutation.isLoading}>
{addProductMutation.isLoading ? 'Submitting......' : 'Add Item'}
</button>
</form>
{addProductMutation.isError && (
<p className="error">Submission Failed:{addProductMutation.error.message}</p>
)}
<hr />
<h3>Product List</h3>
{isLoading && <p>Loading......</p>}
{products && (
<ul>
{products.map(p => (
<li key={p.id}>{p.name} — ${p.price}</li>
))}
</ul>
)}
</div>
)
}
Processo principal: useMutation.mutate() Aciona uma solicitação POST → O servidor processa a solicitação → onSuccess Executa invalidateQueries na chamada de retorno → O TanStack Query recupera automaticamente os dados de ['products'] → A interface de usuário da lista é atualizada automaticamente.
Por que não usar setData manualmente? Você pode chamar queryClient.setQueryData para atualizar manualmente o cache, mas uma abordagem melhor é fazer com que o TanStack Query solicite novamente os dados do servidor por meio de invalidateQueries — isso garante que os dados sempre venham do servidor e evita inconsistências entre o cache do front-end e os dados do lado do servidor.
6. Atualizações otimistas
Quando a latência da rede é alta, os usuários precisam aguardar a resposta do servidor após enviar uma solicitação antes de poderem ver as alterações na interface do usuário, o que resulta em uma experiência de usuário insatisfatória. A atualização otimista atualiza a interface do usuário imediatamente antes do envio da solicitação e reverte para o estado anterior caso a solicitação falhe.
▶ Exemplo 4: Alterando o status de conclusão da tarefa
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'
// Get the To-Do List
function useTodos() {
return useQuery({
queryKey: ['todos'],
queryFn: async () => {
const res = await fetch('/api/todos')
return res.json()
}
})
}
function TodoList() {
const queryClient = useQueryClient()
const { data: todos } = useTodos()
// Switch to "Completed" status — Using Optimistic Updates
const toggleMutation = useMutation({
// Actual API Request
mutationFn: async ({ id, done }) => {
const res = await fetch(`/api/todos/${id}`, {
method: 'PATCH',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ done })
})
if (!res.ok) throw new Error('Update Failed')
return res.json()
},
// ===== Optimistic Update Begins =====
onMutate: async ({ id, done }) => {
// 1. Cancel all ongoing todos Search(Avoid Overwriting Optimistic Updates)
await queryClient.cancelQueries({ queryKey: ['todos'] })
// 2. Save the current cache data,Used for rollback in case of failure
const previousTodos = queryClient.getQueryData(['todos'])
// 3. Refresh the cache immediately
queryClient.setQueryData(['todos'], (old) =>
old.map(todo =>
todo.id === id ? { ...todo, done } : todo
)
)
// 4. Restore old data for rollback
return { previousTodos }
},
// ===== End of Optimistic Update =====
// Rollback on Failure
onError: (err, variables, context) => {
if (context?.previousTodos) {
queryClient.setQueryData(['todos'], context.previousTodos)
}
},
// Whether we succeed or fail,Finally, resend the request to ensure synchronization with the server.
onSettled: () => {
queryClient.invalidateQueries({ queryKey: ['todos'] })
}
})
return (
<ul>
{todos?.map(todo => (
<li key={todo.id} style={{ opacity: toggleMutation.isLoading ? 0.7 : 1 }}>
<input
type="checkbox"
checked={todo.done}
onChange={() => toggleMutation.mutate({ id: todo.id, done: !todo.done })}
/>
<span style={{ textDecoration: todo.done ? 'line-through' : 'none' }}>
{todo.title}
</span>
</li>
))}
</ul>
)
}
O guia de três passos para o otimismo:
onMutate: Atualize o cache imediatamente antes de enviar a solicitação para que os usuários possam ver as alterações na hora; salve os dados antigos caso seja necessário reverter as alterações.onError: Restaurar o cache (revertê-lo) usando os dados históricos salvos quando uma solicitação falharonSettled: Independentemente do sucesso ou do fracasso, solicite novamente os dados ao servidor para garantir consistência absoluta.
7. Estado do lado do servidor x Estado do lado do cliente
Para compreender o conceito por trás do TanStack Query, é necessário distinguir entre dois estados:
| Dimensão | Estado do servidor | Estado do cliente |
|---|---|---|
| Fonte | API de back-end / Banco de dados | Front-end (local, ações do usuário) |
| Propriedade | O servidor é o proprietário dos dados | O front-end é o proprietário dos dados |
| Persistência | Armazenado em um banco de dados | Armazenado na memória ou no localStorage |
| Requisitos de sincronização | Deve permanecer sincronizado com o servidor | Não precisa permanecer sincronizado com o servidor |
| Método de atualização | Gravar via API + Recarregar | Usar diretamente setState |
| Ferramentas de gerenciamento | TanStack Query | State / Redux Toolkit |
| Exemplo | Lista de usuários, dados de produtos, informações de pedidos | Botão de alternância em janela pop-up, valores inseridos em formulários, cor do tema |
Princípios Fundamentais:
- Dados retornados pela API (lista de usuários, informações sobre produtos, status dos pedidos) → Gerenciados usando o TanStack Query
- Estado local do front-end (abertura/fechamento de modais, valores dos campos de entrada, configurações de tema) → Gerenciar usando Zustand / Context / useState
▶ Exemplo: Consulta no TanStack com o DevTools
O TanStack Query oferece um componente dedicado do DevTools que permite visualizar o status do cache, o tempo de expiração e a hora da última atualização de todas as consultas durante o desenvolvimento.
npm install @tanstack/react-query-devtools
import { ReactQueryDevtools } from '@tanstack/react-query-devtools'
function App() {
return (
<QueryClientProvider client={queryClient}>
<YourApp />
{/* Display only in the development environment DevTools */}
<ReactQueryDevtools initialIsOpen={false} />
</QueryClientProvider>
)
}
Recursos do DevTools:
- Visualizar todas as queryKeys e seus dados em cache correspondentes
- Visualizar os status “caducado”, “ativo” e “inativo” de cada entrada do cache
- Acionar manualmente as operações de recarga, invalidação e remoção
- Monitorar a latência da rede e o tamanho da resposta das solicitações
▶ Exemplo: Métodos globais do QueryClient
Além de useQuery e useMutation, o objeto queryClient também oferece vários métodos globais para manipular o cache de fora do componente:
import { queryClient } from './queryClient'
// 1. Invalidate the cache(Trigger a re-request)
queryClient.invalidateQueries({ queryKey: ['products'] })
// 2. Clear All Cache(When a user logs out)
queryClient.clear()
// 3. Set the default option(Global Changes staleTime)
queryClient.setDefaultOptions({
queries: { staleTime: 60 * 1000 }
})
// 4. Retrieve Cached Data(Do not trigger a request)
const cachedData = queryClient.getQueryData(['products'])
// 5. Prefetch Data(User hover Preload when the link is reached)
queryClient.prefetchQuery({
queryKey: ['product', '42'],
queryFn: () => fetchProduct('42')
})
// After the user clicks the link,,The data is already in the cache.,Instant Rendering
// 6. Cancel the current query
queryClient.cancelQueries({ queryKey: ['product', '42'] })
A pré-busca de dados (prefetchQuery) é uma forma eficaz de melhorar a experiência do usuário. Quando o usuário passa o cursor sobre um link, uma solicitação é iniciada antecipadamente para que os dados já estejam carregados no momento em que ele clicar, proporcionando um efeito de “carregamento instantâneo”.
▶ Exemplo: Quando personalizar a configuração do QueryClient
O defaultOptions definido durante a inicialização estabelece o valor padrão global, mas cada chamada ao useQuery pode substituí-lo individualmente:
// Global Default:30 Freshness Period (in Seconds)
const queryClient = new QueryClient({
defaultOptions: {
queries: { staleTime: 30 * 1000, retry: 3 }
}
})
// A Query Override:The data remains virtually unchanged,Set the shelf life to 10 minutes
function useProductCategories() {
return useQuery({
queryKey: ['categories'],
queryFn: fetchCategories,
staleTime: 10 * 60 * 1000, // 10 Do not retry within minutes
})
}
// Another query override:High real-time requirements,Disable Caching
function useRealtimeNotifications() {
return useQuery({
queryKey: ['notifications'],
queryFn: fetchNotifications,
staleTime: 0, // The data expires immediately
refetchInterval: 30 * 1000, // Auto-poll every 30s
})
}
Princípios de configuração: Defina valores conservadores (intervalo mais curto staleTime) na configuração global e substitua-os individualmente para cada consulta, com base nas características dos dados. Para dados que mudam com frequência (notificações, estatísticas em tempo real), defina um staleTime curto ou até mesmo habilite a sondagem; para dados que mudam com pouca frequência (listas de categorias, informações de configuração), defina um staleTime longo para reduzir o número de solicitações.
❓ Perguntas Frequentes
P: Quais são as principais vantagens do TanStack Query em comparação com o uso de
useEffect+fetch? R: Três vantagens principais: (1) Cache compartilhado — quando vários componentes utilizam o mesmoqueryKey, as duplicatas são automaticamente filtradas para evitar solicitações redundantes; (2) Recarga automática — os dados são atualizados automaticamente quando a janela volta ao primeiro plano, a rede se reconecta ou durante a sondagem periódica; (3) Gerenciamento do ciclo de vida — tratamento automático dos estados de carregamento, erro e dados; novas tentativas automáticas em caso de falha; e não há necessidade de implementar manualmente um AbortController para cancelar solicitações. Um únicouseQuerysubstitui 20 linhas deuseEffect+fetch+useState.
P: Qual é a diferença entre
staleTimeecacheTime? O que acontece comstaleTime = 0? R:staleTimecontrola a “atualidade” dos dados — dentro desse intervalo de tempo, os dados são considerados atualizados e não acionarão uma nova busca automática. cacheTime controla o tempo de retenção do cache — por quanto tempo o cache permanece após o componente ser desmontado antes de ser removido pela coleta de lixo. staleTime = 0 significa que os dados são marcados como expirados assim que são retornados, acionando uma nova busca em segundo plano toda vez que forem usados (embora os dados em cache sejam retornados primeiro e depois atualizados). Recomenda-se definir staleTime = 30s para evitar instabilidade nas solicitações.
P: No método
onSuccessdouseMutation, como devo escolher entreinvalidateQueriesesetQueryData? R: A opção preferida éinvalidateQueries— invalidar o cache para forçar o TanStack Query a buscar novamente os dados do servidor, garantindo a consistência dos dados.setQueryDataé adequado para cenários com pouquíssimas alterações e um formato de retorno conhecido (como a geração de um ID exclusivo no lado do cliente). Se você precisar exibir os resultados das ações do usuário imediatamente (sem esperar pela resposta do servidor), use atualizações otimistas (onMutate + rollback) em vez de setQueryData.
P: Quando vários componentes utilizam a mesma queryKey, como o TanStack Query determina quando acionar uma nova solicitação? R: As regras são: (1) Se houver acerto no cache e os dados não estiverem vencidos (dentro do staleTime), os dados em cache são retornados diretamente, sem o envio de uma solicitação; (2) Se houver acerto no cache, mas o cache estiver expirado, o resultado em cache é retornado imediatamente e uma nova solicitação é iniciada em segundo plano; (3) Se não houver cache, uma solicitação é iniciada. Independentemente de quantos componentes estejam assinados no mesmo
queryKey, apenas uma solicitação será enviada.
P: O TanStack Query e o Zustand podem ser usados juntos? R: Sim, e essa abordagem é recomendada. O TanStack Query gerencia o estado do lado do servidor (dados da API), enquanto o Zustand gerencia o estado do lado do cliente (estado da interface do usuário). Uma arquitetura comum: o TanStack Query busca e armazena os dados em cache → injeta os dados no armazenamento do Zustand para processamento posterior → os componentes leem o estado final do Zustand. Os dois são complementares, não substitutos um do outro.
📖 Resumo
- O useQuery gerencia a recuperação de dados: armazenamento em cache automático, deduplicação de solicitações, nova recuperação em segundo plano e novas tentativas em caso de falha
- O
useMutationgerencia as gravações de dados: funciona em conjunto com oinvalidateQueriespara acionar automaticamente as atualizações de dados staleTimecontrola a atualização dos dados, ecacheTimecontrola o tempo de retenção do cache- Atualizações otimizadas: use
onMutatepara atualizar a interface do usuário primeiro,onErrorpara reverter as alterações eonSettledpara a sincronização final, melhorando assim a experiência do usuário - Use o TanStack Query para o estado do lado do servidor (dados da API) e o Zustand/Context para o estado do lado do cliente (estado da interface do usuário)
📝 Exercícios
- Crie um componente de lista de usuários usando
useQuery: utilizehttps://jsonplaceholder.typicode.com/userscomo API, implemente o compartilhamento de cache (ambos os componentes usam o mesmoqueryKeypara garantir que a solicitação seja enviada apenas uma vez) e adicione um botão de atualização manual. - Use
useMutationpara implementar o recurso “Adicionar produto”: quando o formulário é enviado, ele aciona uma solicitação POST; se for bem-sucedido, a lista de produtos é atualizada automaticamente; se não for bem-sucedido, é exibida uma mensagem de erro. - Implemente uma atualização otimista para “alterar o status de conclusão de uma tarefa”: ao clicar na caixa de seleção, o status é alterado imediatamente; se a solicitação à API falhar, a alteração é revertida. Depois de escrever o código, teste-o desconectando-se da rede e clicando na caixa de seleção para observar o comportamento de reversão.