TanStack Query vs Zustand: Quando usar cada um
A pergunta aparece toda semana no Discord e no Stack Overflow: “devo usar Redux, Zustand ou TanStack Query?” A resposta depende de entender que existem dois tipos completamente diferentes de estado.
Estado de servidor vs estado de cliente
Estado de servidor — dados que vivem no backend:
- Lista de usuários do banco de dados
- Pedidos de um cliente
- Configurações salvas
Características: assíncrono, potencialmente desatualizado, compartilhado com outros clientes.
Estado de cliente — dados que existem só no browser:
- Modal aberto/fechado
- Etapa atual de um wizard
- Tema selecionado pelo usuário
- Carrinho de compras (antes de salvar)
Características: síncrono, sempre atualizado, privado do usuário atual.
A regra de ouro
TanStack Query para estado de servidor. Zustand (ou useState) para estado de cliente.
Misturar os dois é o erro mais comum — e é o que deixa o código complexo sem necessidade.
TanStack Query na prática
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'
// Buscar dados
function ListaUsuarios() {
const { data, isLoading, error } = useQuery({
queryKey: ['usuarios'],
queryFn: () => fetch('/api/usuarios').then(r => r.json()),
staleTime: 1000 * 60 * 5, // dados frescos por 5 minutos
})
if (isLoading) return <Skeleton />
if (error) return <Erro mensagem={error.message} />
return <ul>{data.map(u => <li key={u.id}>{u.nome}</li>)}</ul>
}
// Mutação com invalidação automática
function FormNovoUsuario() {
const queryClient = useQueryClient()
const mutation = useMutation({
mutationFn: (novoUsuario) =>
fetch('/api/usuarios', {
method: 'POST',
body: JSON.stringify(novoUsuario),
}).then(r => r.json()),
onSuccess: () => {
// Invalida o cache — React Query rebusca automaticamente
queryClient.invalidateQueries({ queryKey: ['usuarios'] })
},
})
const handleSubmit = (e) => {
e.preventDefault()
mutation.mutate({ nome: e.target.nome.value })
}
return (
<form onSubmit={handleSubmit}>
<input name="nome" />
<button disabled={mutation.isPending}>
{mutation.isPending ? 'Salvando...' : 'Salvar'}
</button>
</form>
)
}O que o TanStack Query resolve por você:
- Cache automático
- Deduplicação de requisições paralelas
- Revalidação em foco de janela
- Retry com backoff exponencial
- Estado de loading/error/success
Zustand para estado de cliente
import { create } from 'zustand'
interface UIStore {
modalAberto: boolean
etapaAtual: number
abrirModal: () => void
fecharModal: () => void
avancarEtapa: () => void
voltarEtapa: () => void
}
const useUIStore = create<UIStore>((set) => ({
modalAberto: false,
etapaAtual: 1,
abrirModal: () => set({ modalAberto: true }),
fecharModal: () => set({ modalAberto: false, etapaAtual: 1 }),
avancarEtapa: () => set((state) => ({ etapaAtual: state.etapaAtual + 1 })),
voltarEtapa: () => set((state) => ({ etapaAtual: state.etapaAtual - 1 })),
}))
// Uso nos componentes
function Wizard() {
const { etapaAtual, avancarEtapa, voltarEtapa } = useUIStore()
return (
<div>
<p>Etapa {etapaAtual} de 3</p>
<button onClick={voltarEtapa} disabled={etapaAtual === 1}>Voltar</button>
<button onClick={avancarEtapa} disabled={etapaAtual === 3}>Avançar</button>
</div>
)
}O anti-padrão mais comum
Colocar dados do servidor no Zustand:
// Não faça isso
const useStore = create((set) => ({
usuarios: [],
carregarUsuarios: async () => {
const data = await fetch('/api/usuarios').then(r => r.json())
set({ usuarios: data })
},
}))Você vai precisar reinventar tudo que o TanStack Query já resolve: cache, loading state, error handling, invalidação, refetch…
Quando usar useState no lugar de Zustand?
useState é suficiente para estado local que não é compartilhado:
function Accordion({ titulo, conteudo }) {
const [aberto, setAberto] = useState(false) // local, sem compartilhamento
return (
<div>
<button onClick={() => setAberto(!aberto)}>{titulo}</button>
{aberto && <p>{conteudo}</p>}
</div>
)
}Escale para Zustand só quando o estado precisar ser compartilhado entre componentes distantes na árvore.
Resumo da decisão
| Situação | Solução |
|---|---|
| Dados da API | TanStack Query |
| Formulário local | useState / React Hook Form |
| Estado compartilhado da UI | Zustand |
| Estado global de autenticação | Zustand |
| Cache de dados do servidor | TanStack Query |
| Paginação/filtros de lista | TanStack Query |
Adote essa separação e o gerenciamento de estado no React se torna muito mais simples.