Introdução ao Zustand e Sua Arquitetura
Zustand é uma biblioteca de gerenciamento de estado para JavaScript e React que se destaca pela sua simplicidade, performance e curva de aprendizado suave. Diferentemente de Redux ou MobX, que exigem boilerplate significativo, Zustand oferece uma API minimalista sem perder em funcionalidade. Sua estratégia é baseada em hooks customizados e imutabilidade, permitindo criar stores de forma declarativa com poucas linhas de código.
A razão pela qual Zustand ganhou popularidade reside em sua filosofia: ser pequeno (cerca de 2KB), sem dependências externas e extremamente flexível. Você não precisa de actions, reducers ou dispatch explícitos — apenas crie uma função que retorna um objeto com estado e métodos. Isso significa que você pode começar simples e crescer em complexidade conforme necessário, introduzindo Slices, Middleware e Persist apenas quando forem relevantes para seu projeto.
Criando Stores e Entendendo Slices
O Básico: Sua Primeira Store
Uma store Zustand é criada através da função create, que recebe um callback. Esse callback espera uma função que retorna um objeto com o estado inicial e os métodos que o modificam. Vamos começar com um exemplo prático de uma store de carrinho de compras:
import create from 'zustand';
const useCartStore = create((set) => ({
items: [],
total: 0,
addItem: (item) => set((state) => ({
items: [...state.items, item],
total: state.total + item.price,
})),
removeItem: (id) => set((state) => ({
items: state.items.filter(item => item.id !== id),
total: state.total - state.items.find(item => item.id === id)?.price || 0,
})),
clearCart: () => set({ items: [], total: 0 }),
}));
O parâmetro set é uma função que recebe um novo estado (ou um callback que retorna o novo estado) e o mescla com o estado atual. Note que você é responsável por manter a imutabilidade — Zustand não força isso, mas é a prática correta.
Entendendo Slices: Organização em Larga Escala
Quando sua aplicação cresce, manter tudo em uma única store pode ficar desordenado. Aqui entram os Slices — padrão de organização que divide a store em pedaços lógicos. A ideia é criar funções que retornam partes do estado e seus métodos, depois combiná-las em uma única store.
Vamos refatorar nosso exemplo usando slices. Primeiro, criamos um slice para o carrinho:
import create from 'zustand';
// Slice: Gerenciamento do Carrinho
const createCartSlice = (set) => ({
items: [],
total: 0,
addItem: (item) => set((state) => ({
items: [...state.items, item],
total: state.total + item.price,
})),
removeItem: (id) => set((state) => ({
items: state.items.filter(item => item.id !== id),
total: state.total - (state.items.find(item => item.id === id)?.price || 0),
})),
clearCart: () => set({ items: [], total: 0 }),
});
// Slice: Gerenciamento de Usuário
const createUserSlice = (set) => ({
user: null,
isAuthenticated: false,
setUser: (user) => set({ user, isAuthenticated: !!user }),
logout: () => set({ user: null, isAuthenticated: false }),
});
// Combinando Slices em uma Store Única
const useAppStore = create((set) => ({
...createCartSlice(set),
...createUserSlice(set),
}));
Essa abordagem oferece várias vantagens: cada slice é isolado logicamente, testável separadamente, e fácil de manter. Quando seu projeto escalou para 50+ estados, essa organização evita que tudo se torne um caos. Você pode até mesmo colocar cada slice em um arquivo separado e importá-los conforme necessário.
Middleware: Interceptando e Transformando Ações
O Que é Middleware no Contexto de Zustand
Middleware no Zustand funciona de forma similar a outras bibliotecas: é uma camada que intercepta chamadas de set e permite transformar, logar ou validar mudanças de estado. Zustand oferece uma API simplificada para isso através de funções que envolvem a store. O middleware recebe acesso ao estado anterior, à função set, à ação que está sendo realizada e metadados.
Implementando Seu Primeiro Middleware
Vamos criar um middleware que registra todas as mudanças de estado — uma prática comum em desenvolvimento:
import create from 'zustand';
// Middleware de Logging
const loggerMiddleware = (config) => (set, get, api) =>
config(
(args) => {
console.log('Estado anterior:', get());
console.log('Ação sendo executada:', args);
set(args);
console.log('Novo estado:', get());
},
get,
api
);
// Store com Middleware
const useStore = create(
loggerMiddleware((set) => ({
count: 0,
increment: () => set((state) => ({ count: state.count + 1 })),
decrement: () => set((state) => ({ count: state.count - 1 })),
}))
);
Cada vez que você chama increment() ou decrement(), o middleware vai imprimir o estado anterior, a ação e o novo estado. Isso é extremamente útil para debug em desenvolvimento.
Middleware Mais Complexo: Validação de Estado
Um caso de uso real é validar se uma mudança de estado é válida antes de permitir:
import create from 'zustand';
// Middleware de Validação
const validatorMiddleware = (config) => (set, get, api) =>
config(
(args) => {
// Se for uma função, executa a validação
const newState = typeof args === 'function' ? args(get()) : args;
// Valida se count nunca fica negativo
if (newState.count < 0) {
console.warn('Count não pode ser negativo. Operação rejeitada.');
return;
}
set(args);
},
get,
api
);
const useCountStore = create(
validatorMiddleware((set) => ({
count: 0,
increment: () => set((state) => ({ count: state.count + 1 })),
decrement: () => set((state) => ({ count: state.count - 1 })),
}))
);
// Tentativa de ir para -1 será bloqueada
useCountStore.getState().decrement(); // count = 0
useCountStore.getState().decrement(); // rejeitado, count continua 0
Combinando Múltiplos Middlewares
Em projetos reais, você frequentemente precisa de vários middlewares. Zustand permite compor middlewares:
import create from 'zustand';
const loggerMiddleware = (config) => (set, get, api) =>
config(
(args) => {
console.log('Antes:', get());
set(args);
console.log('Depois:', get());
},
get,
api
);
const validatorMiddleware = (config) => (set, get, api) =>
config(
(args) => {
const newState = typeof args === 'function' ? args(get()) : args;
if (newState.count < 0) {
console.warn('Validação falhou');
return;
}
set(args);
},
get,
api
);
// Aplicar middlewares em sequência
const useStore = create(
validatorMiddleware(
loggerMiddleware((set) => ({
count: 0,
increment: () => set((state) => ({ count: state.count + 1 })),
decrement: () => set((state) => ({ count: state.count - 1 })),
}))
)
);
Note que a ordem importa — o middleware mais interno é executado primeiro. Neste exemplo, logger executa depois de validator, garantindo que apenas mudanças válidas sejam logadas.
Persistência de Estado com Persist
Por Que Persistir Estado
Muitas aplicações precisam manter estado entre sessões do usuário. Sem persistência, toda vez que o usuário recarrega a página, ele volta ao estado inicial. Zustand fornece um middleware built-in chamado persist que sincroniza o estado com localStorage (ou qualquer storage que você escolher).
Configuração Básica do Persist
import create from 'zustand';
import { persist } from 'zustand/middleware';
const useCartStore = create(
persist(
(set) => ({
items: [],
total: 0,
addItem: (item) => set((state) => ({
items: [...state.items, item],
total: state.total + item.price,
})),
clearCart: () => set({ items: [], total: 0 }),
}),
{
name: 'cart-store', // Nome da chave no localStorage
}
)
);
Com essa simples configuração, sempre que o estado muda, ele é automaticamente salvo em localStorage com a chave cart-store. Quando a página é recarregada, o estado é restaurado automaticamente. Você não precisa fazer nada além disso — é totalmente transparente.
Customizando o Storage e Comportamento
Por padrão, persist usa localStorage, mas você pode usar qualquer storage que siga a interface Storage:
import create from 'zustand';
import { persist } from 'zustand/middleware';
// Usando sessionStorage em vez de localStorage
const useTemporaryStore = create(
persist(
(set) => ({
sessionData: 'inicial',
setSessionData: (data) => set({ sessionData: data }),
}),
{
name: 'temp-store',
storage: sessionStorage, // Muda para sessionStorage
}
)
);
// Usando um Storage Customizado (por exemplo, AsyncStorage do React Native)
const customStorage = {
getItem: async (name) => {
// Implementação para buscar de um storage customizado
return JSON.parse(await AsyncStorage.getItem(name));
},
setItem: async (name, value) => {
// Implementação para salvar em um storage customizado
await AsyncStorage.setItem(name, JSON.stringify(value));
},
removeItem: async (name) => {
await AsyncStorage.removeItem(name);
},
};
const useNativeStore = create(
persist(
(set) => ({
data: [],
addData: (item) => set((state) => ({ data: [...state.data, item] })),
}),
{
name: 'native-store',
storage: customStorage,
}
)
);
Selecionando Quais Partes do Estado Persistir
Nem sempre você quer persistir todo o estado. Imagine que tem dados sensíveis ou temporários — você pode escolher explicitamente o que salvar:
import create from 'zustand';
import { persist } from 'zustand/middleware';
const useUserStore = create(
persist(
(set) => ({
user: { name: 'João', email: 'joao@email.com' },
password: '',
rememberMe: true,
setUser: (user) => set({ user }),
setPassword: (pwd) => set({ password: pwd }),
setRememberMe: (value) => set({ rememberMe: value }),
}),
{
name: 'user-store',
partialize: (state) => ({
user: state.user,
rememberMe: state.rememberMe,
// password NÃO será persistido — informação sensível
}),
}
)
);
Com partialize, você fornece uma função que retorna apenas as partes do estado que deseja persistir. Isso é crucial para lidar com informações sensíveis como senhas, tokens ou dados temporários.
Combinando Persist com Slices
Agora vamos integrar persist com a estrutura de slices que vimos antes:
import create from 'zustand';
import { persist } from 'zustand/middleware';
const createCartSlice = (set) => ({
items: [],
total: 0,
addItem: (item) => set((state) => ({
items: [...state.items, item],
total: state.total + item.price,
})),
clearCart: () => set({ items: [], total: 0 }),
});
const createUserSlice = (set) => ({
user: null,
isAuthenticated: false,
setUser: (user) => set({ user, isAuthenticated: !!user }),
logout: () => set({ user: null, isAuthenticated: false }),
});
// Store com Persist, combinando slices
const useAppStore = create(
persist(
(set) => ({
...createCartSlice(set),
...createUserSlice(set),
}),
{
name: 'app-store',
// Persiste apenas dados relevantes
partialize: (state) => ({
items: state.items,
total: state.total,
user: state.user,
isAuthenticated: state.isAuthenticated,
}),
}
)
);
Essa abordagem combina a organização limpa dos slices com a persistência automática, criando uma solução robusta e escalável.
Ciclo de Vida e Eventos do Persist
Zustand persist oferece hooks de ciclo de vida que permite executar código quando a store é inicializada ou sincronizada:
import create from 'zustand';
import { persist } from 'zustand/middleware';
const useStore = create(
persist(
(set) => ({
data: [],
addData: (item) => set((state) => ({ data: [...state.data, item] })),
}),
{
name: 'my-store',
onRehydrateStorage: () => (state, error) => {
if (error) {
console.error('Erro ao recuperar estado:', error);
} else {
console.log('Estado recuperado com sucesso:', state);
}
},
}
)
);
O callback onRehydrateStorage é executado quando o estado é restaurado do storage. Isso é útil para validar dados, migrar versões antigas ou sincronizar com um servidor.
Conclusão
Zustand oferece uma abordagem diferente e refrescante para gerenciamento de estado: simplicidade sem sacrificar funcionalidade. Os três pilares que exploramos — Slices para organização escalável, Middleware para interceptar e controlar mudanças, e Persist para manter estado entre sessões — formam uma base sólida para construir aplicações React robustas. O grande diferencial é que você não é forçado a usar nada disso desde o início; você começa simples e introduz esses padrões conforme a necessidade surge. Isso torna Zustand ideal tanto para projetos pequenos quanto para aplicações empresariais complexas.