O que use-zustand faz?
Use este skill diretamente para gerenciamento de estado com Zustand. Combine-o com use-tanstack-query para manter estado do cliente e estado do servidor em seus devidos lugares.
Aplique também enforce-code-quality e enforce-typescript-strict nos arquivos envolvidos.
Aplique estas regras sempre que escrever ou revisar stores Zustand em um projeto React.
1. Zustand é exclusivo para estado do cliente — não sobreponha com TanStack Query
Zustand gerencia estado UI/do cliente: sidebar aberta/fechada, dados de formulários multi-etapas, tema, conteúdo do carrinho que o usuário está editando ativamente. Dados do servidor (qualquer coisa buscada de uma API) pertencem ao TanStack Query, não devem ser duplicados em um store Zustand — veja use-tanstack-query. Misturar as duas responsabilidades (ex.: copiar dados buscados para o Zustand "para facilitar o acesso") cria duas fontes de verdade que podem dessincronizar.
2. Nenhum Provider necessário, mas somente no cliente
Um store Zustand é um singleton no nível de módulo — nenhum wrapper de contexto é necessário, e é acessível de qualquer lugar importando o hook. Mas isso também significa que depende de APIs do navegador e estado por cliente, então:
- Inicialize/use stores Zustand apenas em Componentes Cliente (
'use client'no Next.js). - Nunca crie ou leia um store Zustand em um Componente Servidor — não há isolamento "por requisição" como há com um novo
QueryClient, então fazer isso corre o risco de vazar estado entre usuários/requisições.
3. Organize stores grandes com o padrão de slices, não um bloco único
Um único store com preocupações não relacionadas agrupadas (usuário, carrinho, tema da UI, notificações, tudo em um objeto) fica difícil de navegar e entender. Separe por domínio em slices, depois componha-os em uma única chamada create():
// userSlice.ts
export interface UserSlice {
user: User | null;
setUser: (user: User) => void;
logout: () => void;
}
export const createUserSlice: StateCreator<UserSlice> = (set) => ({
user: null,
setUser: (user) => set({ user }),
logout: () => set({ user: null }),
});
// cartSlice.ts
export interface CartSlice {
items: CartItem[];
addItem: (item: CartItem) => void;
}
export const createCartSlice: StateCreator<CartSlice> = (set) => ({
items: [],
addItem: (item) => set((state) => ({ items: [...state.items, item] })),
});
// store.ts
type AppState = UserSlice & CartSlice;
export const useAppStore = create<AppState>()((...a) => ({
...createUserSlice(...a),
...createCartSlice(...a),
}));
Recorra a stores genuinamente separados (não slices) apenas quando dois domínios são verdadeiramente independentes e nunca precisam interagir (ex.: um store de autenticação vs. um store apenas de análise) — múltiplos stores adicionam custo de coordenação, então prefira slices dentro de um único store primeiro.
4. Use métodos de ação nomeados, não chamadas set() brutas dos componentes
Exponha ações como funções nomeadas no store; não chame set diretamente de um componente.
// Evite: funciona, mas não é semântico — componentes precisam conhecer a forma do estado
const set = useAppStore((state) => state.set);
set({ theme: "dark" });
// Prefira: ações nomeadas e autodocumentadas
const setTheme = useAppStore((state) => state.setTheme);
setTheme("dark");
Use get() dentro das ações de um slice para ler outro estado ou chamar ações de outros slices (ex.: uma ação logout que também limpa o carrinho) — mas cuidado com dependências circulares entre slices chamando uns aos outros.
5. Sempre selecione a fatia específica de estado que você precisa — nunca o store inteiro
// Evite: re-renderiza em QUALQUER mudança no store, mesmo campos não relacionados
const state = useAppStore();
// Prefira: re-renderiza apenas quando `user` muda
const user = useAppStore((state) => state.user);
6. Use useShallow ao selecionar múltiplos valores ou um objeto
Selecionar um objeto ou array recém-construído do store (ex.: { user, theme }) cria uma nova referência a cada renderização mesmo que os valores subjacentes não tenham mudado, causando re-renderizações desnecessárias. Embrulhe com useShallow para que o Zustand compare os valores selecionados de forma superficial ao invés de por referência:
import { useShallow } from "zustand/react/shallow";
const { user, theme } = useAppStore(
useShallow((state) => ({ user: state.user, theme: state.theme }))
);
7. Use o middleware immer para atualizações de estado aninhado
Espalhar manualmente múltiplos níveis de estado aninhado para atualizar um campo é propenso a erros e verboso. O middleware immer permite escrever sintaxe de "mutação" direta que na verdade é aplicada de forma imutável internamente:
import { immer } from "zustand/middleware/immer";
const useStore = create<State>()(
immer((set) => ({
nested: { deeply: { value: 0 } },
increment: () =>
set((state) => {
state.nested.deeply.value += 1; // parece mutável, mas é imutável
}),
}))
);
Pule immer para atualizações de altíssima frequência (ex.: estado de animação a 60fps) onde sua sobrecarga importa; para estado típico no estilo CRUD é um ganho de legibilidade que vale o pequeno custo.
8. Tipifique stores com a forma curried create<State>()(...)
O create simples create((set) => ({...})) não pode ser inferido confiavelmente pelo TypeScript para algo além de um store trivial, devido a como o genérico de estado é invariante. Sempre use a forma curried:
interface CounterState {
count: number;
increment: () => void;
}
const useCounterStore = create<CounterState>()((set) => ({
count: 0,
increment: () => set((state) => ({ count: state.count + 1 })),
}));
9. Não armazene valores derivados/computados — calcule-os em um seletor ou no componente
Se um valor pode ser calculado a partir de outro estado já no store (um total a partir de uma lista de itens, um booleano a partir de um campo de status), não o armazene também como seu próprio campo — ele dessincronizará no momento em que um caminho de atualização esquecer de recalculá-lo. Calcule-o no seletor, ou como um valor derivado simples no componente:
// Evite: um campo `itemCount` separado que deve ser mantido em sincronia manualmente
addItem: (item) => set((state) => ({ items: [...state.items, item], itemCount: state.itemCount + 1 })),
// Prefira: derive-o no momento da leitura
const itemCount = useCartStore((state) => state.items.length);
10. Use persist para estado que deve sobreviver entre sessões, devtools apenas em desenvolvimento
- Use o middleware
persist(com suporte alocalStorage/sessionStorage, ou um adaptador de armazenamento personalizado) para estado que genuinamente precisa sobreviver a um recarregamento de página ou nova sessão (conteúdo do carrinho, preferências do usuário) — não para estado que deve ser reiniciado a cada visita. - Inclua uma
versione, se a forma mudar, uma funçãomigratenas opções dopersistpara que o estado persistido antigo não quebre silenciosamente o app após uma mudança de esquema. - Use o middleware
devtoolspara integrar com Redux DevTools para inspecionar mudanças de estado durante o desenvolvimento, mas certifique-se de que não está habilitado em builds de produção.
export const useSettingsStore = create<SettingsState>()(
devtools(
persist(
(set) => ({ theme: "light", setTheme: (theme) => set({ theme }) }),
{ name: "settings-storage", version: 1 }
)
)
);
11. Use devtools para rastrear o fluxo de estado e combine com React DevTools para capturar re-renderizações desnecessárias
devtools não é apenas um logger opcional — use-o ativamente para depurar por que o estado mudou, não apenas que mudou:
-
Nomeie cada ação para que a linha do tempo do devtools mostre um rastro significativo ao invés de uma entrada anônima
setpara cada atualização. Passe um terceiro argumento parasetcom um nome/tipo de ação:addItem: (item) => set( (state) => ({ items: [...state.items, item] }), false, "cart/addItem" // aparece como este rótulo na lista de ações do Redux DevTools ), -
Nomeie cada store via
devtools(..., { name: "cart-store" })quando um projeto tem múltiplos stores, para que o painel devtools não mostre uma mistura ambígua de atualizações de todos os stores ao mesmo tempo. -
Use a linha do tempo do devtools para confirmar a ordem real do fluxo de estado quando um bug parece um problema de condição de corrida ou ordenação de atualizações — percorra a lista de ações para ver exatamente o que mudou, quando, e com qual estado anterior/seguinte, ao invés de adivinhar a partir de
console.logs espalhados pelo código das ações. -
Diagnosticar re-renderizações desnecessárias é uma ferramenta separada, não algo que o
devtoolsmostra. Use o Profiler do React DevTools (ouwhy-did-you-renderem desenvolvimento se precisar de detalhes no nível do local da chamada) para ver quais componentes re-renderizaram em uma determinada mudança de estado e por quê. Se um componente re-renderiza em uma atualização de store que não tocou nos campos que ele seleciona, isso é um sinal de que você está perdendo um seletor (regra 5) ou umuseShallow(regra 6) — vá corrigir o seletor, não o store.
12. Torne stores testáveis: exporte o criador bruto, não apenas o hook vinculado
Exporte tanto o hook (para componentes) quanto o StateCreator/função de criação de store subjacente (para testes), para que os testes possam criar uma instância de store nova e isolada por teste ao invés de compartilhar o singleton no nível de módulo por toda a suíte de testes (o que vaza estado entre testes).
Aplicando estas regras
- Novo store: comece com um único store usando o padrão de slices desde o primeiro dia se mais de um domínio estiver envolvido; tipifique com a forma curried
create<State>()(...); adicionepersist/devtoolsapenas se o estado realmente precisar deles. - Revisando código existente: sinalize seleção do store inteiro sem função seletora, chamadas
set()brutas de componentes ao invés de ações nomeadas, estado derivado armazenado como campo próprio, dados buscados do servidor duplicados no Zustand ao invés de TanStack Query, qualquer store acessado de um Componente Servidor, ações/stores sem nome que dificultam a leitura da linha do tempo do devtools, e qualquer componente re-renderizando em mudanças de store não relacionadas (seletor ouuseShallowausente).