Communitygithub.com

use-zustand

Regras e boas práticas para gerenciamento de estado com Zustand em projetos React, focando em estado exclusivo do cliente e evitando sobreposição com dados do servidor (TanStack Query).

O que é use-zustand?

Este skill descreve um conjunto de regras e boas práticas para usar Zustand de forma eficaz em projetos React. Ele enfatiza que Zustand deve ser usado apenas para estado do lado do cliente (UI, formulários, tema, carrinho), enquanto dados do servidor devem ser gerenciados pelo TanStack Query, evitando duplicação e fontes de verdade divergentes. O documento cobre tópicos como: uso do padrão de slices para organizar stores grandes, seleção granular de estado com seletores e `useShallow`, middleware `immer` para estados aninhados, tipagem com a forma curried `create<State>()(...)`, derivação de valores computados em vez de armazená-los, persistência de estado entre sessões com `persist`, e rastreamento com `devtools`. Também recomenda a exportação da função criadora crua para testabilidade, e sugere uso pareado com React DevTools para depuração de re-renderizações desnecessárias. As regras são aplicáveis a novos stores e revisões de código existente.

Funciona com~Claude Code~Codex CLI~Cursor
npx skills add C1131-G/skills

Perguntar na sua IA favorita

Abre um novo chat com esta habilidade de agente já pré-carregada.

Documentação

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 a localStorage/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 version e, se a forma mudar, uma função migrate nas opções do persist para que o estado persistido antigo não quebre silenciosamente o app após uma mudança de esquema.
  • Use o middleware devtools para 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 set para cada atualização. Passe um terceiro argumento para set com 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 devtools mostra. Use o Profiler do React DevTools (ou why-did-you-render em 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 um useShallow (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>()(...); adicione persist/devtools apenas 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 ou useShallow ausente).

Habilidades Relacionadas