O que é Zustand?
Zustand é um store de estado pequeno e rápido para React. Toda a sua API cabe na cabeça: você chama create com uma função que retorna o estado e as ações, e recebe de volta um hook. Não existe provider, nem reducer, nem tipos de ação e nem dispatch. Você importa o hook e o utiliza.
O nome significa “estado” em alemão, e a biblioteca faz jus a essa simplicidade. Ela possui cerca de um kilobyte, utiliza inscrições baseadas em seletores para manter as re-renderizações no mínimo e funciona com JavaScript e TypeScript puros. Para muitos apps React, é a menor quantidade de cerimônia que você pode adicionar e ainda assim compartilhar estado globalmente.
Criando uma store
Uma store é um hook. O estado e as funções que o atualizam coexistem no mesmo lugar.
// store.js
import { create } from "zustand";
export const useStore = create((set, get) => ({
count: 0,
increment: () => set((state) => ({ count: state.count + 1 })),
decrement: () => set((state) => ({ count: state.count - 1 })),
reset: () => set({ count: 0 }),
double: () => get().count * 2,
}));
set mescla o objeto retornado ao estado, e get lê o estado atual de forma imperativa. Não há reducers para escrever nem actions para disparar — apenas funções que atualizam o estado.
Lendo o estado com seletores
Os componentes se inscrevem exatamente na fatia (slice) de que precisam.
// Counter.jsx
import { useStore } from "./store";
export function Counter() {
const count = useStore((state) => state.count);
const increment = useStore((state) => state.increment);
return <button onClick={increment}>{count}</button>;
}
Como o componente se inscreve apenas em state.count, atualizações em partes não relacionadas da store não causam a sua renderização novamente. Essa é a principal diferença de performance em relação à Context API do React, que renderiza novamente todos os consumidores sempre que o valor é alterado.
Para selecionar múltiplos valores, utilize uma comparação rasa (shallow comparison) para que o componente seja renderizado novamente apenas quando um deles realmente mudar:
// shallow.js
import { useShallow } from "zustand/react/shallow";
const { count, increment } = useStore(
useShallow((state) => ({ count: state.count, increment: state.increment })),
);
Atualizando o estado
set realiza o merge por padrão, portanto você especifica apenas o que mudou. Passe uma função quando o próximo valor depender do anterior.
// updates.js
set({ count: 0 }); // merge
set((state) => ({ count: state.count + 1 })); // derive from previous
set((state) => ({ items: [...state.items, item] })); // arrays
Para objetos aninhados, set substitui a chave de nível superior, então utilize o spread do objeto existente quando precisar manter outras propriedades. O middleware Immer está disponível se você preferir a sintaxe de mutação para atualizações profundas.
Ações assíncronas
A lógica assíncrona é apenas uma função async na store. Não há middleware para configurar.
// usersStore.js
import { create } from "zustand";
export const useUsers = create((set) => ({
users: [],
loading: false,
error: null,
fetchUsers: async () => {
set({ loading: true, error: null });
try {
const res = await fetch("/api/users");
if (!res.ok) throw new Error(`HTTP ${res.status}`);
set({ users: await res.json(), loading: false });
} catch (error) {
set({ error: error.message, loading: false });
}
},
}));
As flags de carregamento (loading) e erro ficam logo ao lado dos dados, e os componentes selecionam apenas as partes que precisam exibir.
Slices e middleware
Para stores grandes, divida-as em slices que são combinados em uma única store. Cada slice é uma função que recebe set e get.
// slices/cart.js
export const createCartSlice = (set) => ({
items: [],
addItem: (item) => set((state) => ({ items: [...state.items, item] })),
clear: () => set({ items: [] }),
});
// store.js
import { create } from "zustand";
import { createCartSlice } from "./slices/cart";
import { createUserSlice } from "./slices/user";
export const useStore = create((...args) => ({
...createCartSlice(...args),
...createUserSlice(...args),
}));
O middleware é composto como funções ao redor do criador da store. Os mais comuns são:
- persist salva e reidrata o estado no
localStorageou em outro armazenamento. - devtools conecta a store à extensão Redux DevTools.
- immer permite escrever atualizações mutáveis que permanecem imutáveis.
// persisted.js
import { persist } from "zustand/middleware";
export const useSettings = create(
persist(
(set) => ({
theme: "dark",
setTheme: (theme) => set({ theme }),
}),
{ name: "settings" },
),
);
Quando usar Zustand
O Zustand é ideal para estados globais do client: temas e preferências, carrinhos de compras, estados de UI compartilhados entre componentes distantes, rascunhos, filtros e estados de modais. Ele é especialmente útil quando o Context causaria re-renders excessivos ou quando o Redux parece ter mais estrutura do que o necessário.
Não o utilize para estados do server. Dados provenientes de uma API exigem cache, deduplicação, refetching em segundo plano e invalidação, e o TanStack Query lida com essas questões de forma muito mais eficiente. A configuração comum é utilizar Zustand para o estado do client e TanStack Query para o estado do server.
Melhores práticas
- Sempre leia o estado utilizando um selector.
- Use
useShallowao selecionar múltiplos valores. - Mantenha as actions na store para que os componentes não precisem conhecer a estrutura do estado.
- Divida stores grandes em slices.
- Use o persist middleware para configurações que devem persistir após o recarregamento da página.
- Mantenha dados do servidor fora da store.
- Armazene dados simples e serializáveis sempre que possível.
Erros comuns
- Chamar
useStore()sem um selector e causar re-renderizações a cada mudança. - Retornar um novo objeto de um selector sem utilizar uma comparação rasa (shallow comparison).
- Tratar o Zustand como um cache de estado do servidor.
- Mutar estados aninhados sem o Immer e perder atualizações.
- Criar uma store dentro de um componente, o que a reseta a cada renderização.
- Adicionar a “cerimônia” típica do Redux que o Zustand foi projetado para eliminar.
Próximos passos
O Zustand costuma ser o equilíbrio ideal entre Context e Redux. Compare-o com o Context para compartilhamentos simples e com o Redux Toolkit para estados rigorosos e com mais ferramentas, e mova os dados do servidor para o TanStack Query. Para Vue, esse mesmo papel é desempenhado pelo Pinia.