//
Busque em todas as páginas da documentação
🤖 Read the SystemsArchitect.io Blog for over 100+ cloud architecture articles 🔥
Projete stores com ações colocalizadas, valores derivados (computados) e uma clara separação entre estado e comportamento. Mantenha stores focados em um único domínio.
import { create } from "zustand";
interface CartItem {
id: string;
name: string;
price: number;
quantity: number;
}
interface CartStore {
// Estado
items: CartItem[];
// Ações
addItem: (item: Omit<CartItem, "quantity">) => void;
removeItem: (id: string) => void;
updateQuantity: (id: string, quantity: number) => void;
clearCart: () => void;
// Computados (como getters)
getTotal: () => number;
getItemCount: () => number;
}
export const useCartStore = create<CartStore>((set, get) => ({
items: [],
addItem: (item) =>
set((state) => {
const existing = state.items.find((i) => i.id === item.id);
if (existing) {
return {
items: state.items.map((i) =>
i.id === item.id ? { ...i, quantity: i.quantity + 1 } : i
),
};
}
return { items: [...state.items, { ...item, quantity: 1 }] };
}),
removeItem: (id) =>
set((state) => ({ items: state.items.filter((i) => i.id !== id) })),
updateQuantity: (id, quantity) =>
set((state) => ({
items: state.items.map((i) => (i.id === id ? { ...i, quantity } : i)),
})),
clearCart: () => set({ items: [] }),
getTotal: () => get().items.reduce((sum, i) => sum + i.price * i.quantity, 0),
getItemCount: () => get().items.reduce((sum, i) => sum + i.quantity, 0),
}));// stores/auth-store.ts
import { create } from "zustand";
interface User {
id: string;
name: string;
email: string;
role: "admin" | "user";
}
interface AuthStore {
// Estado
user: User | null;
token: string | null;
isHydrated: boolean;
// Ações
login: (email: string, password: string) => Promise<void>;
logout: () => void;
updateProfile: (updates: Partial<User>) => void;
// Computados
isAuthenticated: () => boolean;
isAdmin: () => boolean;
}
export const useAuthStore = create<AuthStore>((set, get) => ({
user: null,
token: null,
isHydrated: false,
login: async (email, password) => {
const res = await fetch("/api/auth/login", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ email, password }),
});
if (!res.ok) throw new Error("Login failed");
const { user, token } = await res.json();
set({ user, token });
},
logout: () => set({ user: null, token: null }),
updateProfile: (updates) =>
set((state) => ({
user: state.user ? { ...state.user, ...updates } : null,
})),
isAuthenticated: () => get().token !== null,
isAdmin: () => get().user?.role === "admin",
}));// components/auth-guard.tsx
"use client";
import { useAuthStore } from "@/stores/auth-store";
import { useRouter } from "next/navigation";
import { useEffect } from "react";
export function AuthGuard({ children }: { children: React.ReactNode }) {
const isAuthenticated = useAuthStore((s) => s.isAuthenticated());
const router = useRouter();
useEffect(() => {
if (!isAuthenticated) {
router.push("/login");
}
}, [isAuthenticated, router]);
if (!isAuthenticated) return null;
return <>{children}</>;
}// components/user-menu.tsx
"use client";
import { useAuthStore } from "@/stores/auth-store";
export function UserMenu() {
const user = useAuthStore((s) => s.user);
const logout = useAuthStore((s) => s.logout);
const isAdmin = useAuthStore((s) => s.isAdmin());
if (!user) return null;
return (
<div>
<span>{user.name}</span>
{isAdmin && <a href="/admin">Painel de Admin</a>}
<button onClick={logout}>Logout</button>
</div>
);
}set fornece mesclagem parcial de estado. get fornece acesso de leitura ao estado atual - útil para valores computados e ações assíncronas.get()) são avaliados preguiçosamente toda vez que são chamados, não em cache.(set, get, api) => state recebe três argumentos: set para atualizações, get para leitura e api para assinaturas e a API completa do store.set e get, mantendo-as autocontidas.Separar ações do estado:
// Algumas equipes preferem ações fora do store
const useStore = create<State>((set) => ({
count: 0,
}));
// Ações como funções standalone
export const increment = () => useStore.setState((s) => ({ count: s.count + 1 }));
export const reset = () => useStore.setState({ count: 0 });Valores computados com subscribe (em cache):
import { create } from "zustand";
import { subscribeWithSelector } from "zustand/middleware";
const useStore = create(
subscribeWithSelector<{ items: Item[]; total: number }>((set) => ({
items: [],
total: 0,
}))
);
// Atualiza o total sempre que os itens mudam
useStore.subscribe(
(s) => s.items,
(items) => useStore.setState({ total: items.reduce((s, i) => s + i.price, 0) })
);Padrão de ações agrupadas:
interface Store {
count: number;
actions: {
increment: () => void;
decrement: () => void;
reset: () => void;
};
}
const useStore = create<Store>((set) => ({
count: 0,
actions: {
increment: () => set((s) => ({ count: s.count + 1 })),
decrement: () => set((s) => ({ count: s.count - 1 })),
reset: () => set({ count: 0 }),
},
}));
// Seleciona as ações uma vez (referência estável, nunca dispara re-renderização)
const { increment } = useStore((s) => s.actions);create<State>.ReturnType<typeof useStore.getState> para inferir o tipo do store dinamicamente.interface State {
count: number;
}
interface Actions {
increment: () => void;
reset: () => void;
}
type Store = State & Actions;
const useStore = create<Store>((set) => ({
count: 0,
increment: () => set((s) => ({ count: s.count + 1 })),
reset: () => set({ count: 0 }),
}));getTotal()) não são cacheadas. Cada chamada recomputa. Para computações caras, derive valores no seletor ou use subscribeWithSelector.isAuthenticated() como um seletor (s) => s.isAuthenticated() re-renderiza a cada mudança de estado, pois a função sempre retorna um novo valor. Selecione o estado subjacente em vez disso: (s) => s.token !== null.set várias vezes em sequência acionarão várias renderizações. Agrupe mudanças relacionadas em uma única chamada set.total) ao lado do estado de origem (como items) cria um risco de sincronização. Prefira computar valores derivados dinamicamente.| Abordagem | Prós | Contras |
|---|---|---|
| Ações colocalizadas | Autocontido, fácil de descobrir | Arquivo de store grande para domínios complexos |
| Ações externas | Testável, desacoplado | Mais difícil de encontrar todas as ações para um store |
| Objeto de ações agrupadas | Referência estável, sem re-renderizações | Aninhamento extra, menos convencional |
| Stores separados por domínio | Focado, independente | Coordenação entre stores necessária |
De uma aplicação SaaS de produção Next.js 15 / React 19 (SystemsArchitect.io).
// Exemplo de produção: Atualização otimista com rollback em um store Zustand
// Arquivo: src/stores/project-store.ts (ação savePoint)
import { create } from 'zustand';
interface ProjectStore {
savedPoints: Map<string, boolean>;
savePoint: (pointId: string, isSaved: boolean) => Promise<void>;
}
export const useProjectStore = create<ProjectStore>((set, get) => ({
savedPoints: new Map(),
savePoint: async (pointId: string, isSaved: boolean) => {
const previousPoints = get().savedPoints;
// 1. Atualiza a UI otimisticamente imediatamente
const optimisticPoints = new Map(previousPoints);
optimisticPoints.set(pointId, isSaved);
set({ savedPoints: optimisticPoints });
try {
// 2. Envia a mudança para a API
const res = await fetch('/api/save-point', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ pointId, isSaved }),
});
if (!res.ok) throw new Error('Falha ao salvar');
} catch (error) {
// 3. Reverte para o estado anterior em caso de falha
set({ savedPoints: new Map(previousPoints) });
console.error('Falha ao salvar, revertido:', error);
}
},
}));O que isso demonstra em produção:
new Map(previousPoints) cria uma nova referência de Map em cada etapa. Zustand usa igualdade de referência para detectar mudanças. Se você mutasse o Map existente com previousPoints.set(pointId, isSaved), Zustand não detectaria a mudança e não acionaria uma re-renderização.new Map(previousPoints) em vez de apenas set({ savedPoints: previousPoints }). Isso é necessário porque previousPoints é a mesma referência que estava no store antes da atualização otimista. Se algum componente capturou essa referência, defini-la de volta sem criar um novo Map poderia falhar em acionar re-renderizações.get().savedPoints captura o estado atual antes da atualização otimista. Esse snapshot é usado para rollback. Se você lesse get().savedPoints dentro do bloco catch, ele retornaria o estado otimista, não o estado pré-atualização.set -- para atualizar o estado (mesclagem parcial ou função).get -- para ler o estado atual (útil para valores computados e ações assíncronas).api -- a API completa do store para assinaturas e uso avançado.get(): getTotal: () => get().items.reduce(...).useMemo em vez disso.set e get, mantendo-as autocontidas.const useStore = create<State>((set) => ({ count: 0 }));
export const increment = () =>
useStore.setState((s) => ({ count: s.count + 1 }));store.setState.actions no store.(s) => s.actions fornece uma referência estável que nunca dispara re-renderizações.(s) => s.token !== null.set aciona uma atualização de estado e uma possível re-renderização.set para evitar múltiplas renderizações.// Ruim: duas renderizações
set({ isLoading: false });
set({ data: result });
// Bom: uma renderização
set({ isLoading: false, data: result });total pode ficar dessincronizado com items.get() ou em seletores.get() antes da operação assíncrona.set (nova referência).interface State { count: number; }
interface Actions { increment: () => void; reset: () => void; }
type Store = State & Actions;
const useStore = create<Store>((set) => ({
count: 0,
increment: () => set((s) => ({ count: s.count + 1 })),
reset: () => set({ count: 0 }),
}));type StoreState = ReturnType<typeof useStore.getState>;Revisado por Chris St. John·Última atualização: 10 de jul. de 2026
🤖 Read the SystemsArchitect.io Blog for over 100+ cloud architecture articles 🔥