Use Cases (Atualizado: 02/06/2026)

Gerenciamento de estado Zustand com Claude Code

Projete stores Zustand com Claude Code: selectors, persist parcial, actions async, testes e prompts de revisão.

Gerenciamento de estado Zustand com Claude Code

Defina primeiro a fronteira do estado

Zustand é uma biblioteca leve de gerenciamento de estado para React. Gerenciar estado não é jogar todas as variáveis em um objeto global; é decidir quais valores precisam ser compartilhados, quais actions podem alterá-los e quais valores devem continuar locais no componente. Claude Code consegue gerar um store rapidamente, mas precisa de uma fronteira clara. Sem isso, texto temporário de formulário, dados de autenticação, respostas de API, toasts e carrinho acabam no mesmo global store.

Este guia usa Claude Code como assistente de desenho de estado. Os casos práticos são filtros de painel administrativo, carrinho, estado de UI de autenticação, modal/toast e atualização otimista. Os blocos são copiáveis: store TypeScript, selectors, persist com partialize, action assíncrona, testes Vitest e prompt de revisão.

A base técnica é a introdução oficial do Zustand, a referência de persist middleware e o guia de useShallow. Para separar estado de servidor, veja o guia de TanStack Query com Claude Code. Para estado mais granular, compare com Jotai atoms.

O que deve entrar no Zustand

A regra prática é simples: use Zustand quando vários componentes distantes precisam do mesmo valor, quando a lógica de atualização deve ser testada em um lugar, ou quando a URL não expressa bem o estado de UI. Não coloque todas as respostas de servidor no Zustand. Listas de produtos, usuários e buscas precisam de cache, refetch, stale time e recuperação de erro; isso costuma pertencer a uma biblioteca de server state.

CasoEm ZustandFora de ZustandInstrução para Claude Code
Filtros de adminkeyword, status, page, pageSizeresposta API completaSeparar valores de URL e UI
CarrinhoSKU, quantidade, preço exibidosessão de pagamento, estoque realPersistir só campos de baixo risco
Auth UIdiálogo de login, checkingtoken, email, endereçoNão salvar PII nem segredos
Modal/toastactiveModal, fila curta de toastslogs longos, auditoriaGuardar só o que a UI renderiza
Atualização otimistarequestId, snapshot anteriorverdade final do servidorDefinir rollback e concorrência

Masa encontrou um problema real em um painel: o objetivo era “usar Zustand”, não organizar estado. A primeira versão guardava input de busca, query da URL, linhas carregadas, seleção e toasts juntos. Depois da navegação, filtros antigos ainda afetavam a nova tela. A correção foi listar quais valores poderiam permanecer globais com segurança.

flowchart LR
  UI[React components] --> Selectors[Selectors and useShallow]
  Selectors --> Store[Zustand store]
  Store --> Actions[Actions]
  Actions --> API[API and server state]
  Store --> Persist[persist partialize]
  Persist --> Storage[localStorage safe fields]

Store TypeScript

Este store tem escopo limitado: filtros de admin, carrinho, auth UI, modal e toasts. Em produtos maiores você pode separar arquivos, mas pedir primeiro um slice completo para Claude Code facilita a revisão.

import { create, type StateCreator } from "zustand";

export type OrderStatus = "all" | "paid" | "refunded" | "failed";
export type AuthUiStatus = "anonymous" | "checking" | "signedIn";
export type ModalId = "invite-user" | "cart-drawer" | "delete-order" | null;

export interface AdminFilters {
  keyword: string;
  status: OrderStatus;
  page: number;
  pageSize: number;
}

export interface CartLine {
  id: string;
  name: string;
  price: number;
  quantity: number;
}

export interface AuthUi {
  status: AuthUiStatus;
  loginDialogOpen: boolean;
}

export interface Toast {
  id: string;
  kind: "success" | "error" | "info";
  message: string;
}

export interface CommerceUiState {
  filters: AdminFilters;
  cart: CartLine[];
  auth: AuthUi;
  activeModal: ModalId;
  toasts: Toast[];
  setFilters: (patch: Partial<AdminFilters>) => void;
  resetFilters: () => void;
  addToCart: (line: Omit<CartLine, "quantity">) => void;
  removeFromCart: (id: string) => void;
  updateQuantity: (id: string, quantity: number) => void;
  setAuthStatus: (status: AuthUiStatus) => void;
  setLoginDialogOpen: (open: boolean) => void;
  openModal: (modal: Exclude<ModalId, null>) => void;
  closeModal: () => void;
  pushToast: (toast: Omit<Toast, "id">) => void;
  dismissToast: (id: string) => void;
  checkoutTotal: () => number;
}

export const initialFilters: AdminFilters = {
  keyword: "",
  status: "all",
  page: 1,
  pageSize: 25,
};

const createToastId = () =>
  globalThis.crypto?.randomUUID?.() ?? Math.random().toString(36).slice(2);

export const createCommerceUiSlice: StateCreator<CommerceUiState> = (set, get) => ({
  filters: initialFilters,
  cart: [],
  auth: { status: "anonymous", loginDialogOpen: false },
  activeModal: null,
  toasts: [],

  setFilters: (patch) =>
    set((state) => ({
      filters: {
        ...state.filters,
        ...patch,
        page: patch.page ?? 1,
      },
    })),

  resetFilters: () => set({ filters: initialFilters }),

  addToCart: (line) =>
    set((state) => {
      const current = state.cart.find((item) => item.id === line.id);
      if (!current) {
        return { cart: [...state.cart, { ...line, quantity: 1 }] };
      }

      return {
        cart: state.cart.map((item) =>
          item.id === line.id ? { ...item, quantity: item.quantity + 1 } : item,
        ),
      };
    }),

  removeFromCart: (id) =>
    set((state) => ({
      cart: state.cart.filter((item) => item.id !== id),
    })),

  updateQuantity: (id, quantity) =>
    set((state) => ({
      cart: state.cart
        .map((item) =>
          item.id === id ? { ...item, quantity: Math.max(0, quantity) } : item,
        )
        .filter((item) => item.quantity > 0),
    })),

  setAuthStatus: (status) =>
    set((state) => ({
      auth: { ...state.auth, status },
    })),

  setLoginDialogOpen: (open) =>
    set((state) => ({
      auth: { ...state.auth, loginDialogOpen: open },
    })),

  openModal: (modal) => set({ activeModal: modal }),
  closeModal: () => set({ activeModal: null }),

  pushToast: (toast) =>
    set((state) => ({
      toasts: [...state.toasts, { ...toast, id: createToastId() }],
    })),

  dismissToast: (id) =>
    set((state) => ({
      toasts: state.toasts.filter((toast) => toast.id !== id),
    })),

  checkoutTotal: () =>
    get().cart.reduce((total, item) => total + item.price * item.quantity, 0),
});

export const useCommerceUiStore = create<CommerceUiState>()(createCommerceUiSlice);

O ponto importante é guardar estado visual de autenticação, não segredos. Tokens, emails, endereços, nomes legais e sessão de pagamento ficam fora. Ao pedir persist, diga explicitamente que PII é informação pessoal identificável e não deve ser salva.

Selectors e renderização

Um componente que chama useCommerceUiStore() sem selector assina o store inteiro. Um toast novo pode renderizar de novo o badge do carrinho. Um selector escolhe só a parte necessária; useShallow ajuda quando retornamos um objeto com valores iguais.

import { useShallow } from "zustand/react/shallow";
import {
  useCommerceUiStore,
  type CommerceUiState,
} from "./commerce-ui-store";

export const selectCartCount = (state: CommerceUiState) =>
  state.cart.reduce((sum, item) => sum + item.quantity, 0);

export const selectCartTotal = (state: CommerceUiState) => state.checkoutTotal();

export function CartBadge() {
  const count = useCommerceUiStore(selectCartCount);

  return <button type="button">Cart ({count})</button>;
}

export function AdminFilterSummary() {
  const { filters, setFilters, resetFilters } = useCommerceUiStore(
    useShallow((state) => ({
      filters: state.filters,
      setFilters: state.setFilters,
      resetFilters: state.resetFilters,
    })),
  );

  return (
    <form>
      <input
        value={filters.keyword}
        placeholder="Search orders"
        onChange={(event) => setFilters({ keyword: event.currentTarget.value })}
      />
      <select
        value={filters.status}
        onChange={(event) =>
          setFilters({ status: event.currentTarget.value as CommerceUiState["filters"]["status"] })
        }
      >
        <option value="all">All</option>
        <option value="paid">Paid</option>
        <option value="refunded">Refunded</option>
        <option value="failed">Failed</option>
      </select>
      <output>Page {filters.page}</output>
      <button type="button" onClick={resetFilters}>
        Reset
      </button>
    </form>
  );
}

Peça a Claude Code para revisar todos os componentes que importam o store. Fora componentes de debug, nenhum deve assinar o store inteiro. Essa regra evita lentidão quando tabelas, filtros, sidebar e notificações mudam ao mesmo tempo.

Persist parcial

persist mantém estado após recarregar, mas pode vazar dados. Carrinho e filtros são bons candidatos; modal, toast, requestId, token e PII não são. partialize limita os campos salvos.

import { create } from "zustand";
import { createJSONStorage, persist } from "zustand/middleware";
import {
  createCommerceUiSlice,
  type CommerceUiState,
} from "./commerce-ui-store";

const noopStorage = {
  getItem: () => null,
  setItem: () => undefined,
  removeItem: () => undefined,
};

type PersistedCommerceUiState = Pick<CommerceUiState, "filters" | "cart">;

export const usePersistedCommerceUiStore = create<CommerceUiState>()(
  persist(createCommerceUiSlice, {
    name: "commerce-ui-v1",
    version: 1,
    storage: createJSONStorage(() =>
      typeof window === "undefined" ? noopStorage : window.localStorage,
    ),
    partialize: (state): PersistedCommerceUiState => ({
      filters: state.filters,
      cart: state.cart,
    }),
  }),
);

Em SSR, o servidor não lê localStorage. Se o HTML inicial mostra carrinho 0 e o navegador restaura 3 itens, pode haver mismatch de hydration. Mostre esses valores depois do mount ou separe do markup crítico.

Action async e atualização otimista

Atualização otimista altera a UI antes da confirmação do servidor. Serve para follow, like ou mudanças de quantidade de baixo risco. O perigo é uma resposta antiga voltar depois de uma nova. Use requestId e snapshot anterior para rollback.

import { create } from "zustand";

interface Profile {
  id: string;
  name: string;
  isFollowing: boolean;
}

interface ProfileState {
  profiles: Record<string, Profile>;
  followRequestIds: Record<string, string>;
  setProfile: (profile: Profile) => void;
  followOptimistically: (profileId: string) => Promise<void>;
}

const removeKey = <T,>(record: Record<string, T>, key: string) => {
  const { [key]: _removed, ...rest } = record;
  return rest;
};

async function updateFollowOnServer(profileId: string, follow: boolean) {
  const response = await fetch(`/api/profiles/${profileId}/follow`, {
    method: follow ? "PUT" : "DELETE",
  });

  if (!response.ok) {
    throw new Error(`Follow update failed: ${response.status}`);
  }
}

export const useProfileStore = create<ProfileState>((set, get) => ({
  profiles: {},
  followRequestIds: {},

  setProfile: (profile) =>
    set((state) => ({
      profiles: { ...state.profiles, [profile.id]: profile },
    })),

  followOptimistically: async (profileId) => {
    const before = get().profiles[profileId];
    if (!before) {
      throw new Error("Profile not found");
    }

    const requestId = `${profileId}-${Date.now()}`;

    set((state) => ({
      profiles: {
        ...state.profiles,
        [profileId]: { ...before, isFollowing: true },
      },
      followRequestIds: {
        ...state.followRequestIds,
        [profileId]: requestId,
      },
    }));

    try {
      await updateFollowOnServer(profileId, true);
    } catch (error) {
      set((state) => {
        if (state.followRequestIds[profileId] !== requestId) return state;

        return {
          profiles: { ...state.profiles, [profileId]: before },
          followRequestIds: removeKey(state.followRequestIds, profileId),
        };
      });
      throw error;
    }

    set((state) => {
      if (state.followRequestIds[profileId] !== requestId) return state;

      return {
        followRequestIds: removeKey(state.followRequestIds, profileId),
      };
    });
  },
}));

Dê regras de produto a Claude Code: cliques repetidos são permitidos, o que volta no erro, haverá toast e qual requisição vence.

Testes com Vitest

Actions de Zustand podem ser testadas sem renderizar React. Reinicie o store antes de cada teste e valide os contratos.

import { beforeEach, describe, expect, it } from "vitest";
import { useCommerceUiStore } from "./commerce-ui-store";

const initialState = useCommerceUiStore.getInitialState();

beforeEach(() => {
  useCommerceUiStore.setState(initialState, true);
});

describe("commerce ui store", () => {
  it("increments cart quantity and calculates total", () => {
    const store = useCommerceUiStore.getState();

    store.addToCart({ id: "sku-1", name: "Workshop", price: 1200 });
    store.addToCart({ id: "sku-1", name: "Workshop", price: 1200 });

    expect(useCommerceUiStore.getState().cart[0]?.quantity).toBe(2);
    expect(useCommerceUiStore.getState().checkoutTotal()).toBe(2400);
  });

  it("resets the page when a filter changes", () => {
    useCommerceUiStore.getState().setFilters({ page: 4 });
    useCommerceUiStore.getState().setFilters({ keyword: "refund" });

    expect(useCommerceUiStore.getState().filters).toMatchObject({
      keyword: "refund",
      page: 1,
    });
  });

  it("keeps auth UI and toasts explicit", () => {
    useCommerceUiStore.getState().setAuthStatus("signedIn");
    useCommerceUiStore.getState().pushToast({
      kind: "success",
      message: "Saved",
    });

    expect(useCommerceUiStore.getState().auth.status).toBe("signedIn");
    expect(useCommerceUiStore.getState().toasts).toHaveLength(1);
  });
});

Prompt de revisão

Revise apenas o desenho de estado Zustand neste diff.

Verifique:
1. Valores que deveriam ser local state não foram movidos ao global store.
2. Componentes usam selectors, não o store inteiro.
3. persist partialize não salva PII, tokens, modal, toasts ou requestId.
4. SSR/hydration mismatch foi considerado.
5. Async actions lidam com respostas antigas, cliques repetidos e rollback.
6. devtools e immer têm motivo concreto.
7. Vitest cobre actions principais e um caminho de falha.

Retorne:
- Blocking issues
- Suggested patches
- Missing tests
- Questions before merge

Armadilhas e suporte

As armadilhas principais são globalizar tudo, esquecer selectors, persistir PII, ignorar SSR/hydration, escrever async actions sem controle de corrida e adicionar devtools ou immer por hábito. O store deve reduzir coordenação, não esconder decisões.

Claude Code Lab oferece treinamento e consultoria para revisar apps React existentes, separar Zustand de TanStack Query, definir regras de persist seguras e criar prompts de revisão reutilizáveis. Nota de verificação: em 2 de junho de 2026, os exemplos foram conferidos com a documentação oficial do Zustand para create, persist, useShallow e testing. Na prática, equipes que primeiro escrevem uma tabela de campos seguros para persistir gastam menos tempo removendo PII do localStorage e corrigindo hydration.

#Claude Code #Zustand #React #state management #TypeScript
Grátis

PDF grátis: cheatsheet do Claude Code

Informe seu e-mail e baixe uma página com comandos, hábitos de revisão e workflows seguros.

Cuidamos dos seus dados e não enviamos spam.

Masa

Sobre o autor

Masa

Engenheiro focado em workflows práticos com Claude Code.