React State Management mit Claude Code: Context bis TanStack Query
React State Management mit Claude Code: klare Kriterien, Beispiele mit Context, Zustand, Jotai und TanStack Query.
React State Management wird schwierig, wenn jede veränderliche Information gleich behandelt wird. Ein Eingabefeld, ein geöffnetes Modal, ein Warenkorb, eine Theme-Einstellung und eine Produktliste aus einer API sind alle state, aber sie haben unterschiedliche Besitzer, Lebensdauer und Fehlerfälle.
Claude Code kann diese Struktur aufräumen, wenn die Aufgabe eng genug formuliert ist. “Verbessere das State Management” ist zu grob. Das Ergebnis kann ein zu großer globaler Store, doppelte API-Caches oder eine Migration ohne Tests sein. Besser ist: erst State inventarisieren, client state und server state trennen, dann kleine Bereiche refaktorisieren.
Dieser Artikel zeigt, wann React selbst reicht, wann Zustand, Jotai oder TanStack Query helfen, und wie man todo, cart, settings und Produkte sicher umsetzt. Dazu kommen Tests, Prompts, typische Fehler, offizielle Links und ein CTA für Teams.
Erst klassifizieren, dann Bibliothek wählen
Die wichtigste Frage lautet: Wem gehört dieser Zustand? Gehört er zum aktuellen UI, zum Browser, zu einer persistierten Präferenz oder zum Server?
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"]
Local UI state bleibt nah an der Komponente. useState reicht für einfache Werte, useReducer für mehrere Aktionen in einer Ansicht. Shared client state entsteht im Browser und wird von entfernten Komponenten gelesen oder geändert, zum Beispiel ein Warenkorb. Persisted preferences sind Theme, Dichte oder Sprache. Server state kommt aus einer API und kann außerhalb der App verändert werden.
Wann React ausreicht
Die offiziellen React-Dokumente zu Managing State empfehlen, State sauber zu strukturieren, bei Bedarf hochzuziehen und später reducer plus context zu verwenden. Genau diese Reihenfolge sollte Claude Code einhalten.
| Bedarf | Erste Wahl | Bibliothek prüfen, wenn |
|---|---|---|
| Input, Tab, Modal | useState | Fast nie |
| Ansicht mit vielen Aktionen | useReducer | Der reducer über mehrere Routen genutzt wird |
| Tiefes Prop Passing | Context | Häufige Updates zu breite Re-renders auslösen |
| Warenkorb oder Editor-Entwurf | Zustand | Entfernte Komponenten ihn ändern |
| Theme und Density | Jotai | Viele kleine Präferenzen kombiniert werden |
| Produkte, Bestellungen, Profil API | TanStack Query | Cache, retry, refetch, invalidation nötig sind |
Eine einfache Regel: Wenn ein Wert in weniger als drei Stellen genutzt wird, nicht aus einer API kommt und nach Reload nicht bleiben muss, starten Sie mit React.
Use Case 1: Todo mit useReducer und Context
Eine Todo-Liste hat klare Aktionen: hinzufügen, umschalten, löschen. Ohne Server-Synchronisierung braucht sie noch keinen externen Store.
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;
}
Diese Form ist gut reviewbar. Claude Code kann eine edited action, Validierung gegen leere Titel oder Tests für den reducer ergänzen, ohne die gesamte App anzufassen.
Use Case 2: Warenkorb mit Zustand
Ein Warenkorb wird auf Produktseiten, im Header, im Drawer und im Checkout genutzt. Zustand ist dafür praktisch, weil es einen kleinen Hook-basierten Store erstellt. Mit persist bleibt nur der sichere Teil nach einem Reload erhalten.
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,
}),
},
),
);
Der Browser-Warenkorb ist nicht die Wahrheit für Checkout. Preis, Bestand, Steuer, Rabatt und Zahlung müssen serverseitig bestätigt werden. Im Prompt an Claude Code sollten persistierte Felder und serverseitige Felder getrennt stehen.
Use Case 3: Settings mit Jotai
Theme, Layout-Dichte, Sidebar und Preview sind kleine Werte. Jotai eignet sich, wenn solche Präferenzen als kleine atoms und abgeleitete Werte modelliert werden.
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);
});
Jotai sollte nicht zum Ersatz für API-Caching werden. Für Präferenzen und derived values ist es stark; für server state ist TanStack Query die sauberere Grenze.
Use Case 4: Produkte und Bestellungen mit TanStack Query
Produkte, Bestellungen und Profile sind server state. Sie können stale werden, fehlschlagen oder von einem anderen Gerät verändert werden. TanStack Query v5 behandelt fetching, caching, synchronizing und updating solcher Daten.
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"] });
},
});
}
Die Grenze: Zustand kann die temporäre Einkaufsabsicht halten, TanStack Query liest bestätigte Warenkörbe, Produkte, Bestellungen und Checkout-Status vom Server.
Sichere Prompts und Tests
How Claude Code works beschreibt den Zyklus aus Kontext, Aktion und Verifikation. Nutzen Sie das im Prompt: zuerst analysieren, nicht editieren.
Untersuche das State Management dieser React-App. Bearbeite noch keine Dateien.
Ziele:
- client state und server state trennen
- markieren, was in useState/useReducer/Context bleiben kann
- begründen, wo Zustand, Jotai oder TanStack Query helfen
Ausgabe:
- State-Inventar als Tabelle
- duplizierter oder abgeleiteter State
- API-Daten, die als client state gespeichert werden
- sichere Migrationsreihenfolge mit Tests
Testen Sie reine Logik zuerst.
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);
});
});
Für TanStack Query empfiehlt der offizielle testing guide, pro Test einen neuen QueryClient zu erstellen.
Fehler, Links und CTA
Häufige Fehler: alles in einen globalen Store legen, server state in Zustand speichern, derived values doppelt halten, sensible Daten persistieren, oder Claude Code ohne test, build und manuelle Prüfung vertrauen.
Offizielle Referenzen: React Managing State, Zustand persist, Jotai, TanStack Query overview, Claude Code best practices.
Für bessere Prompts lesen Sie den Prompt Guide und die Claude Code Productivity Tips. Wiederverwendbare Vorlagen finden Sie auf der Products-Seite. Für Team-Rollout und Repository-spezifische Regeln ist Training und Beratung sinnvoll.
Im Praxistest brachte es am meisten, server state zuerst nach TanStack Query zu verschieben. Zustand wurde kleiner, Jotai blieb bei Präferenzen, und Claude-Code-Diffs waren leichter zu prüfen, weil vorher ein State-Inventar existierte.
Kostenloses PDF: Claude-Code-Cheatsheet
E-Mail eintragen und eine Seite mit Befehlen, Review-Gewohnheiten und sicheren Workflows herunterladen.
Wir schützen Ihre Daten und senden keinen Spam.
Über den Autor
Masa
Engineer für praktische Claude-Code-Workflows und Team-Einführung.
Ähnliche Artikel
Claude Code Permission Safety Ladder: Zugriff kontrolliert erweitern
Von read-only zu begrenzten Änderungen, Prüfbefehlen und Deploy-Checks mit klarer Kontrolle.
Claude Code Small PR Proof Pack: kleine Änderungen reviewbar machen
Ein Proof Pack für Claude-Code-PRs: Diff, Checks, öffentliche URL, CTA-Pfad und Rollback.
Claude-Code-Review-Gate vor dem Commit
Vor dem Commit mit Claude Code prüfen: Diff, Build, öffentliche URL, Gumroad-Links, Beratung-CTA, fehlende Tests und fremde Dateien.