Gerenciamento de estado React com Claude Code: guia prático
Organize estado React com Claude Code: Context, Zustand, Jotai, TanStack Query, testes e prompts seguros.
Gerenciamento de estado em React fica confuso quando todos os dados mutáveis são tratados como a mesma coisa. Um input, um modal aberto, um carrinho, uma preferência de tema e uma lista de produtos vinda da API são state, mas têm donos e ciclos de vida diferentes.
Claude Code pode ajudar a refatorar, desde que a tarefa seja específica. “Melhore o state management” é amplo demais. Isso pode virar uma store global gigante, cache duplicado ou migração de biblioteca sem testes. O fluxo mais seguro é inventariar o estado, separar client state de server state e migrar uma fatia por vez.
Este guia mostra quando React basta, quando Zustand, Jotai e TanStack Query ajudam, e traz exemplos copiáveis para todo, carrinho, settings e produtos. Também inclui testes, prompts seguros, links oficiais e CTA de monetização.
Classifique antes de escolher a biblioteca
Primeiro pergunte quem é dono do dado. Ele pertence à UI local, ao navegador, a uma preferência persistida ou ao servidor?
flowchart TD
A["React state management"] --> B["Local UI state"]
A --> C["Shared client state"]
A --> D["Persisted preferences"]
A --> E["Server state"]
B --> B1["input, modal, tab"]
C --> C1["cart, wizard, editor"]
D --> D1["theme, density, locale"]
E --> E1["products, orders, profile from API"]
Estado local de UI fica perto do componente. useState resolve valores simples, e useReducer ajuda quando há várias ações. Estado de cliente compartilhado nasce no navegador e é usado por componentes distantes, como carrinho. Preferências persistidas sobrevivem ao reload. Server state vem de API e pode mudar fora do app.
Quando React é suficiente
A documentação de Managing State do React recomenda estruturar bem o estado, elevar quando necessário e escalar com reducer e context. Use essa ordem antes de pedir uma nova biblioteca ao Claude Code.
| Necessidade | Primeira opção | Considere biblioteca quando |
|---|---|---|
| Input, tab, modal | useState | Quase nunca |
| Tela com várias ações | useReducer | O reducer aparece em várias rotas |
| Prop drilling profundo | Context | Atualizações frequentes re-renderizam demais |
| Carrinho ou draft | Zustand | Componentes distantes atualizam o mesmo valor |
| Tema e densidade | Jotai | Muitas preferências pequenas se combinam |
| Produtos, pedidos, perfil API | TanStack Query | Cache, retry, refetch e invalidation são necessários |
Regra simples: se o valor aparece em menos de três lugares, não vem de API e não precisa sobreviver ao reload, comece com React.
Caso 1: Todo com useReducer e Context
Uma lista de tarefas tem ações claras: adicionar, alternar, remover. Sem sincronização com servidor, reducer e context são um começo seguro.
import {
createContext,
useContext,
useMemo,
useReducer,
type Dispatch,
type ReactNode,
} from "react";
export type Todo = {
id: string;
title: string;
done: boolean;
};
type TodoAction =
| { type: "added"; title: string }
| { type: "toggled"; id: string }
| { type: "deleted"; id: string };
export function todoReducer(todos: Todo[], action: TodoAction): Todo[] {
switch (action.type) {
case "added":
return [
...todos,
{ id: crypto.randomUUID(), title: action.title.trim(), done: false },
];
case "toggled":
return todos.map((todo) =>
todo.id === action.id ? { ...todo, done: !todo.done } : todo,
);
case "deleted":
return todos.filter((todo) => todo.id !== action.id);
default:
return todos;
}
}
const TodoStateContext = createContext<Todo[] | null>(null);
const TodoDispatchContext = createContext<Dispatch<TodoAction> | null>(null);
export function TodoProvider({ children }: { children: ReactNode }) {
const [todos, dispatch] = useReducer(todoReducer, []);
const stateValue = useMemo(() => todos, [todos]);
return (
<TodoStateContext.Provider value={stateValue}>
<TodoDispatchContext.Provider value={dispatch}>
{children}
</TodoDispatchContext.Provider>
</TodoStateContext.Provider>
);
}
export function useTodos() {
const value = useContext(TodoStateContext);
if (value === null) throw new Error("useTodos must be used in TodoProvider");
return value;
}
export function useTodoDispatch() {
const value = useContext(TodoDispatchContext);
if (value === null) {
throw new Error("useTodoDispatch must be used in TodoProvider");
}
return value;
}
Essa estrutura ajuda o Claude Code a fazer mudanças pequenas: adicionar ação edited, bloquear título vazio ou criar testes do reducer.
Caso 2: Carrinho com Zustand
Carrinho aparece em página de produto, header, drawer e checkout. Zustand cria uma store leve como hook. O middleware persist deve guardar só o que é seguro.
import { create } from "zustand";
import { createJSONStorage, persist } from "zustand/middleware";
export type CartItem = {
productId: string;
name: string;
price: number;
quantity: number;
};
type CartState = {
items: CartItem[];
currency: "JPY" | "USD";
addItem: (item: Omit<CartItem, "quantity">) => void;
removeItem: (productId: string) => void;
setQuantity: (productId: string, quantity: number) => void;
clear: () => void;
total: () => number;
};
export const useCartStore = create<CartState>()(
persist(
(set, get) => ({
items: [],
currency: "USD",
addItem: (item) =>
set((state) => {
const current = state.items.find(
(entry) => entry.productId === item.productId,
);
if (current) {
return {
items: state.items.map((entry) =>
entry.productId === item.productId
? { ...entry, quantity: entry.quantity + 1 }
: entry,
),
};
}
return { items: [...state.items, { ...item, quantity: 1 }] };
}),
removeItem: (productId) =>
set((state) => ({
items: state.items.filter((item) => item.productId !== productId),
})),
setQuantity: (productId, quantity) =>
set((state) => ({
items: state.items.map((item) =>
item.productId === productId
? { ...item, quantity: Math.max(1, quantity) }
: item,
),
})),
clear: () => set({ items: [] }),
total: () =>
get().items.reduce((sum, item) => sum + item.price * item.quantity, 0),
}),
{
name: "cart-v1",
storage: createJSONStorage(() => localStorage),
partialize: (state) => ({
items: state.items,
currency: state.currency,
}),
},
),
);
O carrinho local não valida preço final. Servidor precisa confirmar preço, estoque, desconto, impostos e pagamento. No prompt, diga ao Claude Code quais campos podem persistir e quais dependem da API.
Caso 3: Settings com Jotai
Tema, densidade, sidebar e preview são valores pequenos. Jotai funciona bem quando você quer atoms pequenos e valores derivados.
import { atom } from "jotai";
import { atomWithStorage } from "jotai/utils";
export const themeAtom = atomWithStorage<"light" | "dark">(
"settings.theme",
"light",
);
export const densityAtom = atomWithStorage<"comfortable" | "compact">(
"settings.density",
"comfortable",
);
export const sidebarOpenAtom = atom(true);
export const settingsLabelAtom = atom((get) => {
const theme = get(themeAtom) === "dark" ? "dark theme" : "light theme";
const density =
get(densityAtom) === "compact" ? "compact layout" : "comfortable layout";
return `${theme}, ${density}`;
});
export const resetSettingsAtom = atom(null, (_get, set) => {
set(themeAtom, "light");
set(densityAtom, "comfortable");
set(sidebarOpenAtom, true);
});
O cuidado é não atomizar tudo. Preferências e derivados combinam com Jotai; cache de API e regras de mutation combinam com TanStack Query.
Caso 4: Produtos e pedidos com TanStack Query
Produtos, pedidos e perfil são server state. Podem ficar stale, falhar ou mudar em outro dispositivo. TanStack Query v5 cuida de fetching, caching, syncing e updating.
import {
QueryClient,
QueryClientProvider,
useMutation,
useQuery,
useQueryClient,
} from "@tanstack/react-query";
import type { ReactNode } from "react";
type Product = {
id: string;
name: string;
price: number;
stock: number;
};
const queryClient = new QueryClient();
export function ProductsProvider({ children }: { children: ReactNode }) {
return (
<QueryClientProvider client={queryClient}>{children}</QueryClientProvider>
);
}
async function fetchProducts(category: string): Promise<Product[]> {
const response = await fetch(`/api/products?category=${category}`);
if (!response.ok) throw new Error("Failed to fetch products");
return response.json() as Promise<Product[]>;
}
async function addCartLine(productId: string) {
const response = await fetch("/api/cart/lines", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ productId }),
});
if (!response.ok) throw new Error("Failed to add cart line");
return response.json();
}
export function useProducts(category: string) {
return useQuery({
queryKey: ["products", category],
queryFn: () => fetchProducts(category),
staleTime: 60_000,
});
}
export function useAddCartLine() {
const queryClient = useQueryClient();
return useMutation({
mutationFn: addCartLine,
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ["cart"] });
},
});
}
A fronteira prática: Zustand guarda intenção temporária no navegador; TanStack Query busca produto, pedido, carrinho confirmado e checkout no servidor.
Prompts seguros e testes
How Claude Code works descreve o ciclo de contexto, ação e verificação. Use isso no prompt: primeiro peça inspeção, não edição.
Inspecione o gerenciamento de estado desta app React. Ainda não edite arquivos.
Objetivos:
- separar client state e server state
- identificar o que pode ficar em useState/useReducer/Context
- justificar Zustand, Jotai ou TanStack Query quando fizer sentido
Retorne:
- inventário de estados
- state duplicado ou derivado
- dados de API salvos como client state
- ordem segura de migração com testes
Teste lógica pura primeiro.
import { describe, expect, it, vi } from "vitest";
import { todoReducer, type Todo } from "./TodoProvider";
describe("todoReducer", () => {
it("adds a todo with a generated id", () => {
vi.spyOn(crypto, "randomUUID").mockReturnValue("todo-1");
const result = todoReducer([], { type: "added", title: "Write tests" });
expect(result).toEqual([
{ id: "todo-1", title: "Write tests", done: false },
]);
});
it("toggles a todo", () => {
const initial: Todo[] = [{ id: "todo-1", title: "Ship", done: false }];
const result = todoReducer(initial, { type: "toggled", id: "todo-1" });
expect(result[0].done).toBe(true);
});
});
Para TanStack Query, siga o testing guide: crie um QueryClient novo por teste.
Armadilhas, links e CTA
Armadilhas comuns: colocar tudo em store global, guardar server state no Zustand, duplicar valores derivados, persistir dados sensíveis e aceitar diff do Claude Code sem test, build e revisão manual com reload e erro de API.
Referências oficiais: React Managing State, Zustand persist, Jotai, TanStack Query overview, Claude Code best practices.
Para melhorar prompts, leia o guia de prompts e as dicas de produtividade Claude Code. Para templates, veja produtos. Para aplicar em um repositório real de equipe, use training e consultoria.
Na prática, o maior ganho veio ao mover server state primeiro para TanStack Query. Zustand ficou menor, Jotai ficou focado em preferências e os diffs do Claude Code ficaram mais fáceis de revisar porque havia inventário e plano antes da implementação.
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.
Sobre o autor
Masa
Engenheiro focado em workflows práticos com Claude Code.
Artigos relacionados
Escada de segurança de permissões no Claude Code
Amplie de read-only para edições limitadas, comandos de prova e deploy checks sem perder controle.
Claude Code Small PR Proof Pack: pequenas mudanças fáceis de revisar
Um pacote de prova para PRs do Claude Code: diff, checks, URL pública, CTA e rollback.
Gate de revisão antes do commit com Claude Code
Revisão antes do commit com Claude Code: diff, build, URL pública, Gumroad, consultoria, testes e arquivos fora do escopo.