Was macht Zustand einsetzen?
Nutze diese Skill-Direktive für die Arbeit mit Zustand. Kombiniere sie mit use-tanstack-query, um Client-Zustand und Server-Zustand in ihren jeweiligen Bereichen zu halten.
Wende außerdem enforce-code-quality und enforce-typescript-strict auf die betroffenen Dateien an.
Wende diese Regeln jedes Mal an, wenn du in einem React-Projekt Zustand-Stores schreibst oder überprüfst.
1. Zustand ist nur für clientseitigen Zustand – keine Überschneidung mit TanStack Query
Zustand verwaltet UI/Client-Zustand: Sidebar offen/geschlossen, Multi-Step-Formulardaten, Theme, Warenkorb-Inhalte, die der Benutzer gerade bearbeitet. Server-Daten (alles, was von einer API abgerufen wird) gehören in TanStack Query und werden nicht in einen Zustand-Store dupliziert – siehe use-tanstack-query. Die Vermischung der beiden Verantwortlichkeiten (z.B. das Kopieren abgerufener Daten in Zustand, „um den Zugriff zu vereinfachen“) erzeugt zwei Wahrheitsquellen, die auseinanderdriften können.
2. Kein Provider erforderlich, aber nur clientseitig
Ein Zustand-Store ist ein modulweites Singleton – kein Context-Wrapper erforderlich und von überall durch Import des Hooks zugänglich. Das bedeutet aber auch, dass er auf Browser-APIs und pro-Client-Zustand angewiesen ist. Daher:
- Initialisiere/verwende Zustand-Stores nur in Client-Komponenten (
'use client'in Next.js). - Erstelle oder lies einen Zustand-Store niemals in einer Server-Komponente – es gibt keine „pro-Request“-Isolation wie bei einem neuen
QueryClient; dies riskiert, dass Zustand zwischen Benutzern/Requests durchsickert.
3. Organisiere große Stores mit dem Slices-Muster, nicht als einen flachen Block
Ein einzelner Store mit zusammengewürfelten, nicht zusammenhängenden Belangen (Benutzer, Warenkorb, UI-Theme, Benachrichtigungen – alles in einem Objekt) wird schnell unübersichtlich und schwer nachvollziehbar. Teile nach Domänen in Slices auf und setze sie dann in einem einzigen create()-Aufruf zusammen:
// 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),
}));
Greife nur dann zu separaten Stores (nicht Slices), wenn zwei Domänen wirklich unabhängig sind und niemals interagieren müssen (z.B. ein Auth-Store vs. ein reiner Analyse-Store) – mehrere Stores verursachen Koordinationsaufwand, also verwende standardmäßig zuerst Slices innerhalb eines Stores.
4. Verwende benannte Aktionsmethoden, keine rohen set()-Aufrufe aus Komponenten
Mache Aktionen als benannte Funktionen im Store verfügbar; rufe das set des Stores nicht direkt aus einer Komponente auf.
// Vermeiden: funktioniert, ist aber nicht semantisch – Komponenten müssen die Zustandsform kennen
const set = useAppStore((state) => state.set);
set({ theme: "dark" });
// Bevorzugen: benannte, selbsterklärende Aktionen
const setTheme = useAppStore((state) => state.setTheme);
setTheme("dark");
Verwende get() innerhalb der Aktionen eines Slices, um anderen Zustand zu lesen oder Aktionen anderer Slices aufzurufen (z.B. eine logout-Aktion, die auch den Warenkorb leert) – achte jedoch auf zirkuläre Abhängigkeiten zwischen Slices, die sich gegenseitig aufrufen.
5. Wähle immer den spezifischen Zustandsteil aus, den du brauchst – niemals den gesamten Store
// Vermeiden: wird bei JEDER Store-Änderung neu gerendert, auch bei nicht verwandten Feldern
const state = useAppStore();
// Bevorzugen: wird nur neu gerendert, wenn sich `user` ändert
const user = useAppStore((state) => state.user);
6. Verwende useShallow bei der Auswahl mehrerer Werte oder eines Objekts
Die Auswahl eines neu erstellten Objekts oder Arrays aus dem Store (z.B. { user, theme }) erzeugt bei jedem Rendern eine neue Referenz, selbst wenn sich die zugrunde liegenden Werte nicht geändert haben, was unnötige Neurenderings verursacht. Ummantle es mit useShallow, damit Zustand die ausgewählten Werte flach vergleicht, anstatt per Referenz:
import { useShallow } from "zustand/react/shallow";
const { user, theme } = useAppStore(
useShallow((state) => ({ user: state.user, theme: state.theme }))
);
7. Verwende das immer-Middleware für verschachtelte Zustandsaktualisierungen
Manuelles Spreaden mehrerer Ebenen verschachtelten Zustands, um ein Feld zu aktualisieren, ist fehleranfällig und ausführlich. Das immer-Middleware ermöglicht eine direkte „Mutationssyntax“, die unter der Haube unveränderlich angewendet wird:
import { immer } from "zustand/middleware/immer";
const useStore = create<State>()(
immer((set) => ({
nested: { deeply: { value: 0 } },
increment: () =>
set((state) => {
state.nested.deeply.value += 1; // sieht veränderlich aus, ist aber unveränderlich
}),
}))
);
Verzichte auf immer bei sehr häufigen Aktualisierungen (z.B. 60fps-Animationen), bei denen der Overhead eine Rolle spielt; bei typischem CRUD-artigen Zustand ist es ein Lesbarkeitsgewinn, der den geringen Aufwand wert ist.
8. Typisiere Stores mit der curried create<State>()(...)-Form
Einfaches create((set) => ({...})) kann von TypeScript bei mehr als einem trivialen Store nicht zuverlässig abgeleitet werden, da der Zustandsgeneriker invariant ist. Verwende immer die curried Form:
interface CounterState {
count: number;
increment: () => void;
}
const useCounterStore = create<CounterState>()((set) => ({
count: 0,
increment: () => set((state) => ({ count: state.count + 1 })),
}));
9. Speichere keine abgeleiteten/berechneten Werte – berechne sie in einem Selektor oder der Komponente
Wenn ein Wert aus bereits im Store vorhandenem Zustand berechnet werden kann (eine Summe aus einer Liste von Artikeln, ein Boolean aus einem Statusfeld), speichere ihn nicht als eigenes Feld – er wird aus dem Takt geraten, sobald ein Aktualisierungspfad vergisst, ihn neu zu berechnen. Berechne ihn im Selektor oder als einfachen abgeleiteten Wert in der Komponente:
// Vermeiden: ein separates `itemCount`-Feld, das manuell synchron gehalten werden muss
addItem: (item) => set((state) => ({ items: [...state.items, item], itemCount: state.itemCount + 1 })),
// Bevorzugen: zur Lesezeit ableiten
const itemCount = useCartStore((state) => state.items.length);
10. Verwende persist für Zustand, der sitzungsübergreifend erhalten bleiben soll, devtools nur in der Entwicklung
- Verwende das
persist-Middleware (unterstützt durchlocalStorage/sessionStorageoder einen benutzerdefinierten Storage-Adapter) für Zustand, der wirklich einen Seitenneuladen oder eine neue Sitzung überstehen muss (Warenkorb-Inhalte, Benutzereinstellungen) – nicht für Zustand, der bei jedem Besuch zurückgesetzt werden soll. - Füge eine
versionund, falls sich die Form jemals ändert, einemigrate-Funktion in die Optionen vonpersistein, damit alter persistierter Zustand die App nach einer Schemaänderung nicht stillschweigend zerstört. - Verwende das
devtools-Middleware zur Integration mit den Redux DevTools, um Zustandsänderungen während der Entwicklung zu überprüfen, stelle jedoch sicher, dass es in Produktions-Builds nicht aktiviert ist.
export const useSettingsStore = create<SettingsState>()(
devtools(
persist(
(set) => ({ theme: "light", setTheme: (theme) => set({ theme }) }),
{ name: "settings-storage", version: 1 }
)
)
);
11. Verwende devtools zur Nachverfolgung des Zustandsflusses und kombiniere es mit den React DevTools, um unnötige Neurenderings zu erkennen
devtools ist nicht nur ein nettes Logger-Tool – nutze es aktiv, um zu debuggen, warum sich der Zustand geändert hat, nicht nur dass er sich geändert hat:
-
Benenne jede Aktion, sodass die DevTools-Zeitleiste eine aussagekräftige Spur anstelle eines anonymen
set-Eintrags für jede Aktualisierung zeigt. Übergib ein drittes Argument ansetmit einem Aktionsnamen/-typ:addItem: (item) => set( (state) => ({ items: [...state.items, item] }), false, "cart/addItem" // wird als dieses Label in der Redux-DevTools-Aktionsliste angezeigt ), -
Benenne jeden Store über
devtools(..., { name: "cart-store" }), wenn ein Projekt mehrere Stores hat, damit das DevTools-Panel keine unklare Mischung von Aktualisierungen aus allen Stores gleichzeitig anzeigt. -
Nutze die DevTools-Zeitleiste, um die tatsächliche Reihenfolge des Zustandsflusses zu bestätigen, wenn ein Fehler wie ein Race-Condition- oder Aktualisierungsreihenfolge-Problem aussieht – gehe die Aktionsliste Schritt für Schritt durch, um genau zu sehen, was sich wann geändert hat und mit welchem vorherigen/nächsten Zustand, anstatt auf
console.log-Ausgaben in Aktionscode zu raten. -
Die Diagnose unnötiger Neurenderings ist ein separates Werkzeug, das
devtoolsselbst nicht anzeigt. Verwende den React DevTools Profiler (oderwhy-did-you-renderin der Entwicklung, wenn du eine aufrufortsgenaue Detail benötigst), um zu sehen, welche Komponenten bei einer bestimmten Zustandsänderung neu gerendert wurden und warum. Wenn eine Komponente bei einer Store-Aktualisierung neu rendert, die nicht die von ihr selektierten Felder betrifft, ist das ein Hinweis darauf, dass dir ein Selektor (Regel 5) oder einuseShallow-Wrapper (Regel 6) fehlt – behebe den Selektor, nicht den Store.
12. Mache Stores testbar: Exportiere den rohen Ersteller, nicht nur den gebundenen Hook
Exportiere sowohl den Hook (für Komponenten) als auch die zugrunde liegende StateCreator/Store-Erstellungsfunktion (für Tests), damit Tests eine frische, isolierte Store-Instanz pro Test erstellen können, anstatt das modulweite Singleton über die gesamte Testsuite zu teilen (was Zustand zwischen Tests durchsickern lässt).
Anwenden dieser Regeln
- Neuer Store: Beginne von Anfang an mit einem einzelnen Store unter Verwendung des Slices-Musters, wenn mehr als eine Domäne betroffen ist; typisiere ihn mit der curried
create<State>()(...)-Form; fügepersist/devtoolsnur hinzu, wenn der Zustand sie tatsächlich benötigt. - Überprüfen von vorhandenem Code: Markiere die Auswahl des gesamten Stores ohne Selektorfunktion, rohe
set()-Aufrufe aus Komponenten anstelle von benannten Aktionen, abgeleiteten Zustand, der als eigenes Feld gespeichert ist, serverseitig abgerufene Daten, die in Zustand statt in TanStack Query dupliziert wurden, jeden Store, der aus einer Server-Komponente heraus angesprochen wird, unbenannte Aktionen/Stores, die die DevTools-Zeitleiste schwer lesbar machen, und jede Komponente, die aufgrund von nicht verwandten Store-Änderungen neu rendert (fehlender Selektor oderuseShallow).