Skip to main content

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:

  1. lista desaparece
  2. loading aparece
  3. 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