Gerenciar estado global com Zustand é uma forma simples de compartilhar dados entre componentes React sem Provider, sem boilerplate e sem a cerimônia de actions e reducers. Você cria uma store com uma função, usa um hook para ler e atualizar os dados e pronto: qualquer componente pode acessar o que precisa, e só renderiza de novo quando a parte que ele usa muda.
Neste guia você vai ver como criar uma store, usar seletores para evitar renderizações desnecessárias, lidar com ações assíncronas, persistir dados, tipar com TypeScript e quando faz sentido escolher Zustand em vez de Context API ou Redux Toolkit.
Por que usar Zustand
O React já oferece useState e Context. O problema aparece quando vários componentes distantes precisam do mesmo dado. Passar props por muitos níveis cansa, e o Context re-renderiza todos os consumidores quando o valor muda. O Zustand resolve isso com:
- API mínima: uma função
createe um hook; - sem Provider envolvendo a aplicação;
- assinaturas seletivas, que reduzem re-renderizações;
- middlewares prontos para persistência e integração com DevTools;
- funciona fora de componentes, útil em serviços e testes.
Criando a primeira store
npm install zustand
import { create } from 'zustand'
export const useContador = create((set) => ({
valor: 0,
incrementar: () => set((s) => ({ valor: s.valor + 1 })),
zerar: () => set({ valor: 0 }),
}))
E no componente:
function Contador() {
const valor = useContador((s) => s.valor)
const incrementar = useContador((s) => s.incrementar)
return <button onClick={incrementar}>{valor}</button>
}
A função set faz um merge raso com o estado atual, então você só precisa devolver as chaves que mudaram.
Seletores e desempenho
O ponto que mais faz diferença é o seletor. Se o componente chama useContador() sem argumento, ele recebe o estado inteiro e re-renderiza a cada mudança. Com um seletor, só re-renderiza quando o valor selecionado muda.
Selecionando vários campos
Retornar um objeto novo em cada chamada faz a comparação falhar. Para isso existe o useShallow:
import { useShallow } from 'zustand/react/shallow'
const { nome, email } = useUsuario(
useShallow((s) => ({ nome: s.nome, email: s.email }))
)
Lendo fora do React
Toda store expõe getState, setState e subscribe. Isso permite, por exemplo, ler um token dentro de um interceptador HTTP sem hook.
Ações assíncronas
Não há nada especial: a ação pode ser async e chamar set quando quiser.
export const useProdutos = create((set) => ({
itens: [],
carregando: false,
erro: null,
buscar: async () => {
set({ carregando: true, erro: null })
try {
const r = await fetch('/api/produtos')
set({ itens: await r.json() })
} catch (e) {
set({ erro: String(e) })
} finally {
set({ carregando: false })
}
},
}))
Para cache de dados do servidor, com revalidação e paginação, bibliotecas como TanStack Query costumam ser mais adequadas. Um arranjo comum é Zustand para estado da interface e uma biblioteca de dados para o que vem da API. Se a API for sua, tRPC para APIs type-safe combina bem com esse modelo.
Persistência e DevTools
import { create } from 'zustand'
import { persist, devtools } from 'zustand/middleware'
export const useTema = create(
devtools(
persist(
(set) => ({
escuro: false,
alternar: () => set((s) => ({ escuro: !s.escuro })),
}),
{ name: 'tema' }
)
)
)
O persist salva no localStorage por padrão e restaura ao recarregar. Não guarde ali tokens de acesso ou dados sensíveis; o motivo é o mesmo discutido no guia sobre JWT.
Tipando com TypeScript
type Carrinho = {
itens: { id: string; qtd: number }[]
adicionar: (id: string) => void
}
export const useCarrinho = create<Carrinho>()((set) => ({
itens: [],
adicionar: (id) =>
set((s) => {
const existe = s.itens.find((i) => i.id === id)
return {
itens: existe
? s.itens.map((i) => (i.id === id ? { ...i, qtd: i.qtd + 1 } : i))
: [...s.itens, { id, qtd: 1 }],
}
}),
}))
Repare nos parênteses duplos em create<Carrinho>()(...). Essa forma é a recomendada para que a inferência funcione com middlewares.
Organizando stores grandes
Em vez de uma store gigante, prefira stores pequenas por domínio (usuário, carrinho, interface) ou o padrão de “slices”, em que cada fatia é uma função que recebe set e get e é combinada em uma store só.
Zustand, Context API e Redux Toolkit
| Critério | Context API | Zustand | Redux Toolkit |
|---|---|---|---|
| Instalação | Nativo do React | Pacote pequeno | Pacote maior |
| Boilerplate | Baixo | Muito baixo | Médio |
| Re-renderizações | Todos os consumidores | Só quem usa o dado alterado | Só quem usa o dado alterado |
| Provider | Obrigatório | Não precisa | Obrigatório |
| DevTools | Não | Via middleware | Integrado |
| Indicado para | Tema, idioma, dados que mudam pouco | Apps pequenos a grandes | Apps grandes com muitas regras e times |
Boas práticas
- Sempre use seletores ao ler da store.
- Mantenha as ações dentro da store, não espalhadas pelos componentes.
- Não duplique no estado o que pode ser calculado a partir de outros dados.
- Separe estado de servidor e estado de interface.
- Nos testes, reinicie a store entre casos com
setStatepara o estado inicial.
Testando stores do Zustand
Como a store é um módulo comum, dá para testá-la sem renderizar componentes. Chame as ações via getState e verifique o resultado:
import { useContador } from './contador'
beforeEach(() => useContador.setState({ valor: 0 }))
test('incrementa', () => {
useContador.getState().incrementar()
expect(useContador.getState().valor).toBe(1)
})
Para testar componentes que usam a store, a mesma técnica de reiniciar o estado antes de cada caso garante que um teste não interfira no outro. Isso deixa a suíte previsível e fácil de manter.
Se o projeto crescer muito, revise periodicamente quais stores ainda fazem sentido. Estados que eram globais às vezes podem voltar a ser locais, o que simplifica componentes e reduz dependências. Manter a store enxuta é parte do trabalho, não uma tarefa feita uma vez só.
Perguntas frequentes
Zustand substitui o Redux?
Em muitos projetos, sim. Redux Toolkit continua útil quando o time precisa de convenções rígidas e ferramentas de depuração mais completas.
Zustand funciona com Next.js?
Funciona. Com renderização no servidor, é recomendado criar a store por requisição e expor via contexto, para não compartilhar estado entre usuários.
Preciso de Provider para usar Zustand?
Não no uso comum. A store é um módulo e o hook pode ser importado em qualquer componente.
Como evitar re-renderizações com Zustand?
Selecione apenas o que o componente usa e, ao retornar objetos ou arrays, use useShallow.
Zustand serve para React Native?
Sim. Para persistência, troque o armazenamento padrão por um adaptador como o AsyncStorage.
O que levar dessa leitura
Estado global com Zustand oferece o equilíbrio entre simplicidade e desempenho: pouca configuração, sem Provider e re-renderizações controladas por seletores. Comece com stores pequenas, use middlewares quando precisar e reserve bibliotecas de dados para o que vem do servidor.

