Gerenciamento de estado com Zustand
1. Introdução ao Zustand e seus Fundamentos
Zustand é um gerenciador de estado leve e minimalista para React, criado por Paul Henschel. Sua principal proposta é oferecer uma API simples e direta para gerenciar estado global sem o boilerplate excessivo de soluções como Redux ou a complexidade de Context API para aplicações de médio porte.
Comparado ao Redux, Zustand elimina a necessidade de criar actions, reducers e dispatchers separados. Enquanto a Context API pode causar re-renderizações desnecessárias em componentes que não consomem o estado alterado, Zustand oferece seletores precisos que evitam esse problema. A instalação é trivial:
npm install zustand
2. Criando e Utilizando Stores
A estrutura de uma store em Zustand é criada com a função create(), que recebe um callback contendo estado, ações e getters:
import { create } from 'zustand';
const useCounterStore = create((set, get) => ({
count: 0,
increment: () => set((state) => ({ count: state.count + 1 })),
decrement: () => set((state) => ({ count: state.count - 1 })),
reset: () => set({ count: 0 }),
getDoubleCount: () => get().count * 2,
}));
Para acessar o estado em componentes, utilizamos o hook gerado automaticamente:
function Counter() {
const count = useCounterStore((state) => state.count);
const increment = useCounterStore((state) => state.increment);
return (
<div>
<p>Contagem: {count}</p>
<button onClick={increment}>Incrementar</button>
</div>
);
}
A função set permite atualizações imutáveis de forma intuitiva, enquanto get fornece acesso ao estado atual dentro de ações.
3. Ações e Atualizações Complexas
Ações síncronas são definidas diretamente na store. Para operações assíncronas, como chamadas a APIs, as ações podem usar async/await:
const useUserStore = create((set, get) => ({
user: null,
loading: false,
error: null,
fetchUser: async (userId) => {
set({ loading: true, error: null });
try {
const response = await fetch(`https://api.example.com/users/${userId}`);
const data = await response.json();
set({ user: data, loading: false });
} catch (error) {
set({ error: error.message, loading: false });
}
},
updateUser: async (userData) => {
const previousUser = get().user;
set({ user: { ...previousUser, ...userData } });
try {
await fetch(`https://api.example.com/users/${userData.id}`, {
method: 'PUT',
body: JSON.stringify(userData),
});
} catch (error) {
set({ user: previousUser }); // rollback em caso de erro
}
},
}));
O uso de set com funções é recomendado para evitar race conditions quando múltiplas atualizações ocorrem simultaneamente.
4. Seletores e Performance
Para otimizar re-renderizações, utilizamos seletores que extraem apenas partes específicas do estado:
const userName = useUserStore((state) => state.user?.name);
const userEmail = useUserStore((state) => state.user?.email);
Seletores customizados podem ser criados para evitar recálculos desnecessários:
const useUserFullName = () => {
return useUserStore((state) => {
const user = state.user;
return user ? `${user.firstName} ${user.lastName}` : '';
});
};
Para comparação rasa de objetos, o utilitário shallow é essencial:
import { shallow } from 'zustand/shallow';
function UserProfile() {
const [name, email] = useUserStore(
(state) => [state.user?.name, state.user?.email],
shallow
);
return <div>{name} - {email}</div>;
}
5. Stores Múltiplas e Composição
Separar estado em stores independentes por domínio é uma prática recomendada:
// authStore.js
const useAuthStore = create((set) => ({
user: null,
token: null,
login: (credentials) => {
// lógica de autenticação
set({ user: credentials.user, token: credentials.token });
},
logout: () => set({ user: null, token: null }),
}));
// cartStore.js
const useCartStore = create((set, get) => ({
items: [],
addItem: (product) => {
const currentItems = get().items;
const existingItem = currentItems.find(item => item.id === product.id);
if (existingItem) {
set({
items: currentItems.map(item =>
item.id === product.id
? { ...item, quantity: item.quantity + 1 }
: item
),
});
} else {
set({ items: [...currentItems, { ...product, quantity: 1 }] });
}
},
getTotal: () => {
return get().items.reduce((total, item) => total + item.price * item.quantity, 0);
},
}));
A comunicação entre stores é feita importando e chamando ações diretamente:
const useCheckoutStore = create((set, get) => ({
processCheckout: async () => {
const authState = useAuthStore.getState();
const cartState = useCartStore.getState();
if (!authState.token) {
throw new Error('Usuário não autenticado');
}
const response = await fetch('/api/checkout', {
method: 'POST',
headers: { Authorization: `Bearer ${authState.token}` },
body: JSON.stringify({ items: cartState.items, total: cartState.getTotal() }),
});
if (response.ok) {
useCartStore.getState().clearCart();
}
},
}));
6. Middleware e Persistência
Zustand oferece middleware nativos como persist, devtools e immer:
import { persist, devtools } from 'zustand/middleware';
import { immer } from 'zustand/middleware/immer';
const useSettingsStore = create(
devtools(
persist(
immer((set) => ({
theme: 'light',
language: 'pt-BR',
notifications: true,
toggleTheme: () => set((state) => {
state.theme = state.theme === 'light' ? 'dark' : 'light';
}),
setLanguage: (lang) => set((state) => {
state.language = lang;
}),
})),
{
name: 'app-settings',
getStorage: () => localStorage,
}
),
{ name: 'SettingsStore' }
)
);
O middleware persist salva automaticamente o estado no localStorage. O devtools permite debug com Redux DevTools. O immer simplifica atualizações imutáveis com sintaxe mutável.
7. Integração com React e Next.js
Em componentes funcionais, o uso é direto com hooks. No Next.js, é importante evitar SSR para estado volátil:
// components/ContactForm.jsx
import { useState } from 'react';
import axios from 'axios';
import { create } from 'zustand';
const useFormStore = create((set) => ({
name: '',
email: '',
message: '',
setField: (field, value) => set({ [field]: value }),
reset: () => set({ name: '', email: '', message: '' }),
}));
export default function ContactForm() {
const { name, email, message, setField, reset } = useFormStore();
const [submitting, setSubmitting] = useState(false);
const handleSubmit = async (e) => {
e.preventDefault();
setSubmitting(true);
try {
await axios.post('/api/contact', { name, email, message });
reset();
alert('Mensagem enviada com sucesso!');
} catch (error) {
alert('Erro ao enviar mensagem');
} finally {
setSubmitting(false);
}
};
return (
<form onSubmit={handleSubmit}>
<input
value={name}
onChange={(e) => setField('name', e.target.value)}
placeholder="Nome"
/>
<input
value={email}
onChange={(e) => setField('email', e.target.value)}
placeholder="Email"
/>
<textarea
value={message}
onChange={(e) => setField('message', e.target.value)}
placeholder="Mensagem"
/>
<button type="submit" disabled={submitting}>
{submitting ? 'Enviando...' : 'Enviar'}
</button>
</form>
);
}
Em Next.js, stores que não precisam de SSR devem ser carregadas apenas no cliente:
// pages/contact.jsx
import dynamic from 'next/dynamic';
const ContactForm = dynamic(() => import('../components/ContactForm'), {
ssr: false,
});
export default function ContactPage() {
return (
<div>
<h1>Contato</h1>
<ContactForm />
</div>
);
}
Zustand se destaca por sua simplicidade, performance e flexibilidade. Com uma API enxuta e suporte a middleware poderosos, é uma excelente escolha para gerenciamento de estado em aplicações React modernas, desde projetos pequenos até sistemas complexos com múltiplos domínios de estado.
Referências
- Documentação Oficial do Zustand — Guia completo com exemplos de uso, API detalhada e melhores práticas
- Zustand no GitHub — Repositório oficial com código fonte, issues e exemplos da comunidade
- Zustand vs Redux: Comparação Completa — Artigo técnico comparando performance, complexidade e casos de uso
- Gerenciamento de Estado com Zustand em Next.js — Tutorial prático de integração com Next.js, incluindo SSR e persistência
- Zustand Middleware Guide — Guia avançado sobre middleware, persistência e devtools por TkDodo
- React State Management with Zustand — Tutorial completo cobrindo stores, ações assíncronas e composição de stores
- Zustand com Immer e TypeScript — Artigo sobre integração com TypeScript e middleware Immer para atualizações imutáveis simplificadas