Exemplos de uso | TanStack Query (React Query) | TanStack | React
- Exemplos de uso | TanStack Query (React Query) | TanStack | React
TanStack Query no Next.js — CRUD de Catálogo de Produtos
Esta documentação apresenta o uso do TanStack Query em uma aplicação Next.js (App Router) utilizando um CRUD de catálogo de produtos como contexto. O objetivo é mostrar como implementar, por que cada abordagem foi escolhida e qual problema cada propriedade resolve dentro de uma aplicação real.
O TanStack Query é responsável por gerenciar estado assíncrono do servidor, lidando com cache, sincronização, loading states e atualização automática de dados.
Contexto do Projeto
O catálogo de produtos possui as seguintes operações:
- Listar produtos
- Buscar produto por id
- Criar produto
- Atualizar produto
- Remover produto
Problemas comuns resolvidos:
- Requisições duplicadas
- Cache manual complexo
- Listas desatualizadas após alterações
- Flicker visual ao paginar
- Sincronização entre páginas
Estrutura sugerida:
src/
├── app/
│ ├── products/
│ │ ├── page.tsx
│ │ ├── create/page.tsx
│ │ └── [id]/page.tsx
├── services/
│ └── product.service.ts
├── hooks/
│ └── products/
├── lib/
│ └── queryClient.ts
Configuração do Query Client
O QueryClient é responsável por armazenar:
- cache das queries
- estado de loading e erro
- controle de revalidação
- mutations em execução
lib/queryClient.ts
import { QueryClient } from '@tanstack/react-query'
export const queryClient = new QueryClient({
defaultOptions: {
queries: {
staleTime: 1000 * 60 * 2,
refetchOnWindowFocus: false
}
}
})
staleTime
Define por quanto tempo os dados são considerados frescos.
Durante esse período:
- o TanStack Query não refaz a requisição
- dados vêm do cache
No catálogo de produtos isso evita refetch constante, já que os dados não mudam com frequência.
refetchOnWindowFocus
Quando ativo, refaz a requisição ao voltar para a aba do navegador.
Foi desativado porque:
- listas de produtos normalmente não exigem atualização imediata
- reduz chamadas desnecessárias ao backend
Provider no Next.js
O provider permite que todo o app compartilhe o mesmo cache.
<QueryClientProvider client={queryClient}>
{children}
</QueryClientProvider>
Sem isso cada página teria seu próprio cache isolado.
Camada de Serviço (Service Layer)
A camada de serviço separa a lógica HTTP da interface.
Benefícios:
- reutilização
- testes mais simples
- componentes mais limpos
services/product.service.ts
export async function getProducts(page = 1) {
const res = await fetch(`/api/products?page=${page}`)
return res.json()
}
export async function getProduct(id: string) {
const res = await fetch(`/api/products/${id}`)
return res.json()
}
export async function createProduct(data) {
const res = await fetch(`/api/products`, {
method: 'POST',
body: JSON.stringify(data)
})
return res.json()
}
export async function updateProduct(id, data) {
const res = await fetch(`/api/products/${id}`, {
method: 'PUT',
body: JSON.stringify(data)
})
return res.json()
}
export async function deleteProduct(id) {
await fetch(`/api/products/${id}`, {
method: 'DELETE'
})
}
O TanStack Query executa essas funções, mas não depende de como a API foi implementada.
Query Keys Centralizadas
Query keys identificam unicamente dados dentro do cache.
export const productKeys = {
all: ['products'],
list: (page) => ['products', page],
detail: (id) => ['product', id]
}
Por que usar assim:
- cada página possui cache próprio
- evita sobrescrever dados
- facilita invalidar cache após mutations
- reduz erros de string manual
Listagem de Produtos (Read)
Hook de listagem
useQuery({
queryKey: productKeys.list(page),
queryFn: () => getProducts(page),
keepPreviousData: true
})
queryKey
Define a identidade da query.
Quando page muda:
- nova query é executada
- cache anterior permanece disponível
queryFn
Função responsável pela busca dos dados.
Separar permite reutilização e testes independentes.
keepPreviousData
Resolve o problema de flicker ao paginar.
Sem essa opção:
- lista desaparece
- loading aparece
- nova lista renderiza
Com ela:
- dados anteriores permanecem
- nova página carrega em background
- experiência mais fluida
Página de listagem
'use client'
import { useState } from 'react'
import { useProducts } from '@/hooks/products/useProducts'
export default function ProductsPage() {
const [page, setPage] = useState(1)
const { data, isLoading } = useProducts(page)
if (isLoading) return <p>Carregando...</p>
return (
<div>
{data.items.map(product => (
<div key={product.id}>
{product.name} - {product.price}
</div>
))}
<button onClick={() => setPage(p => p - 1)}>
Anterior
</button>
<button onClick={() => setPage(p => p + 1)}>
Próximo
</button>
</div>
)
}
Buscar Produto por ID
useQuery({
queryKey: productKeys.detail(id),
queryFn: () => getProduct(id),
enabled: !!id
})
enabled
Controla quando a query deve executar.
No Next.js o id pode iniciar como undefined.
Essa propriedade evita chamadas inválidas para a API.
Criar Produto (Create)
useMutation({
mutationFn: createProduct,
onSuccess: () => {
queryClient.invalidateQueries(productKeys.all)
}
})
mutationFn
Executa a alteração no servidor (POST).
Mutations não usam cache diretamente.
onSuccess + invalidateQueries
Após criar um produto:
- a lista ficou desatualizada
Invalidar a query força refetch automático e mantém a UI sincronizada.
Atualizar Produto (Update)
useMutation({
mutationFn: ({ id, data }) =>
updateProduct(id, data),
onSuccess: (_, variables) => {
queryClient.invalidateQueries(
productKeys.detail(variables.id)
)
queryClient.invalidateQueries(productKeys.all)
}
})
Duas invalidações são necessárias porque:
- a tela de detalhe mudou
- a lista também pode conter dados antigos
Remover Produto (Delete)
useMutation({
mutationFn: deleteProduct,
onSuccess: () => {
queryClient.invalidateQueries(productKeys.all)
}
})
A lista precisa ser atualizada pois o item deixou de existir.
Infinite Query para Catálogo Grande
useInfiniteQuery({
queryKey: ['products'],
queryFn: ({ pageParam = 1 }) =>
getProducts(pageParam),
getNextPageParam: lastPage =>
lastPage.nextPage ?? undefined
})
useInfiniteQuery
Usado quando:
- há scroll infinito
- não existe paginação tradicional
pageParam
Valor interno controlado pelo TanStack Query que indica qual página carregar.
getNextPageParam
Define como descobrir a próxima página. Sem essa função a biblioteca não saberia quando parar de buscar dados.
Transformação de Dados
useQuery({
queryKey: productKeys.all,
queryFn: getProducts,
select: (data) =>
data.items.map(p => ({
id: p.id,
label: `${p.name} - ${p.price}`
}))
})
select
Transforma os dados antes de chegarem ao componente.
Benefícios:
- evita processamento em cada render
- mantém UI simples
- transformação roda apenas quando dados mudam
Refetch e Sincronização Automática
useQuery({
queryKey: productKeys.all,
queryFn: getProducts,
refetchInterval: 10000
})
Usado quando dados mudam frequentemente, como preço ou estoque.
Evita criar polling manual.
Testing React Query no Next.js
const queryClient = new QueryClient({
defaultOptions: {
queries: { retry: false }
}
})
retry é desativado porque retries atrasam testes e dificultam validação de erros.
Boas Práticas Aplicadas ao CRUD
- Usar query para leitura e mutation para escrita
- Centralizar query keys
- Separar camada de API da UI
- Invalidar apenas queries necessárias
- Evitar duplicação entre estado local e cache
- Definir staleTime baseado na natureza do dado
- Utilizar enabled em queries dependentes