Tips & Tricks (Actualizado: 2/6/2026)

Gestión de estado en React con Claude Code: guía práctica

Ordena estado React con Claude Code: Context, Zustand, Jotai, TanStack Query, tests y prompts seguros.

Gestión de estado en React con Claude Code: guía práctica

La gestión de estado en React se complica cuando todo se trata como si fuera lo mismo. El valor de un input, un modal abierto, un carrito, una preferencia de tema y una lista de productos obtenida por API son estado, pero no tienen el mismo dueño ni la misma vida útil.

Claude Code ayuda mucho a limpiar este tipo de código, pero necesita una instrucción precisa. “Mejora el state management” suele terminar en una store global demasiado grande, caches duplicadas o una migración de librería sin pruebas. Un flujo más seguro es pedir primero un inventario de estado, separar client state y server state, y después migrar una parte pequeña.

Esta guía cubre criterios de decisión, ejemplos copiables de React/TypeScript, casos de uso de todo, carrito, settings y productos, pruebas, prompts de refactorización, enlaces oficiales y un CTA de monetización para convertir este aprendizaje en un proceso de equipo.

Clasifica el estado antes de elegir librería

La primera pregunta no es “¿Zustand o Jotai?”, sino “¿quién es dueño de este dato?”. Puede ser estado local de UI, estado de cliente compartido, preferencia persistida o estado del 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"]

El estado local de UI debe vivir cerca del componente. Usa useState para valores simples y useReducer cuando una pantalla tiene varias acciones. El estado de cliente compartido nace en el navegador y se lee desde partes lejanas del árbol, como un carrito. Las preferencias persistidas sobreviven a una recarga. El server state viene de una API y puede cambiar fuera de tu aplicación.

Cuándo basta React

La documentación oficial de React sobre Managing State recomienda estructurar bien el estado, elevarlo cuando sea necesario y escalar con reducer y context. Ese orden evita meter una librería antes de tiempo.

NecesidadPrimera opciónConsidera librería cuando
Input, tab, modaluseStateCasi nunca
Pantalla con muchas accionesuseReducerEl reducer se reutiliza en varias rutas
Evitar prop drillingContextLas actualizaciones frecuentes re-renderizan demasiado
Carrito o borrador de editorZustandComponentes lejanos lo modifican
Tema y densidad visualJotaiHay muchas preferencias pequeñas
Productos, pedidos, perfil APITanStack QueryNecesitas cache, retry, refetch e invalidación

Una regla práctica: si el valor se usa en menos de tres lugares, no viene de una API y no necesita persistir tras recargar, empieza con React.

Caso 1: Todo con useReducer y Context

Un todo list tiene acciones claras: añadir, alternar y borrar. Todavía no necesita sincronización con servidor, así que reducer y context son suficientes.

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;
}

Para Claude Code, esta estructura es fácil de revisar. Puedes pedir “añade una acción edited”, “rechaza títulos vacíos” o “escribe tests del reducer” sin tocar toda la aplicación.

Caso 2: Carrito con Zustand

Un carrito lo leen la página de producto, el header, el drawer y checkout. Zustand funciona bien cuando varias zonas distantes necesitan el mismo estado de cliente y acciones simples. El middleware persist sirve para guardar solo la parte segura del estado.

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,
      }),
    },
  ),
);

El carrito del navegador no debe decidir el precio final. El servidor debe validar precio, stock, impuestos, descuento y pago. En el prompt, dile a Claude Code qué se puede persistir y qué debe confirmarse por API.

Caso 3: Settings con Jotai

Tema, densidad, sidebar y modo preview son valores pequeños. Jotai permite modelarlos como atoms y crear valores derivados sin construir una store gigante.

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);
});

El peligro es atomizar todo. Jotai es cómodo para preferencias y valores derivados, pero no debe reemplazar el cache de una API ni esconder reglas complejas de negocio.

Caso 4: Productos y pedidos con TanStack Query

Productos, pedidos y perfil son server state. Pueden estar obsoletos, fallar, reintentarse o cambiar desde otro dispositivo. TanStack Query v5 está diseñado para fetching, caching, synchronization e updates de server state.

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"] });
    },
  });
}

La frontera práctica es esta: Zustand puede guardar la intención temporal del carrito; TanStack Query debe leer el carrito confirmado, productos, pedidos y checkout desde el servidor.

Prompts seguros para Claude Code

La documentación de How Claude Code works describe un ciclo de contexto, acción y verificación. Úsalo literalmente. Primero pide análisis, no cambios.

Inspecciona la gestión de estado de esta app React. No edites todavía.

Objetivos:
- separar client state y server state
- identificar qué puede quedarse en useState/useReducer/Context
- justificar cualquier uso de Zustand, Jotai o TanStack Query

Devuelve:
- tabla de inventario de estado
- estado duplicado o derivado
- datos de API guardados como client state
- orden de migración seguro con tests

Luego migra una parte:

Aplica solo el refactor del todo list a useReducer + Context.

Restricciones:
- no cambiar rutas ni copy visible
- no añadir dependencias
- añadir o actualizar tests del reducer
- reportar archivos modificados y comandos de verificación

Para carrito y API, añade reglas de dominio: no persistir tokens, confirmar precio en servidor, usar TanStack Query para datos remotos y mantener estable la API pública de componentes.

Tests y errores comunes

Empieza probando lógica pura. Un reducer se puede testear con Vitest sin montar toda la UI.

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, sigue su testing guide: crea un QueryClient nuevo por test para que el cache no se comparta.

Los errores más comunes son meter todo en una store global, guardar server state en Zustand, duplicar valores derivados, persistir datos sensibles y aceptar el diff de Claude Code sin test, build y revisión manual en recarga, error de API y red lenta.

Enlaces, CTA y resultado probado

Referencias oficiales: React Managing State, Zustand persist, Jotai, TanStack Query overview y Claude Code best practices.

Para mejorar los prompts, sigue con la guía de mejores prompts y los tips de productividad de Claude Code. Si necesitas plantillas, revisa productos. Para aplicar esto en un repositorio real de equipo, usa training y consultoría.

Al probar este flujo, el mayor avance fue mover primero el server state a TanStack Query. Zustand quedó más pequeño, Jotai se limitó a preferencias y los diffs de Claude Code fueron más fáciles de revisar porque antes existía un inventario de estado y un plan de migración.

#Claude Code #React #gestión de estado #Zustand #frontend
Gratis

PDF gratis: cheatsheet de Claude Code

Introduce tu email y descarga una hoja con comandos, hábitos de revisión y flujos seguros.

Cuidamos tus datos y no enviamos spam.

Masa

Sobre el autor

Masa

Ingeniero enfocado en workflows prácticos con Claude Code.