Was ist Zustand?
Zustand ist ein kleiner, schneller State Store für React. Die gesamte API ist extrem überschaubar: Du rufst create mit einer Funktion auf, die den State und die Actions zurückgibt, und erhältst im Gegenzug einen Hook. Es gibt keinen Provider, keinen Reducer, keine Action-Types und kein Dispatch. Du importierst den Hook und verwendest ihn einfach.
Der Name ist das deutsche Wort für „state“, und die Library wird dieser Einfachheit gerecht. Sie ist nur etwa ein Kilobyte groß, nutzt selector-basierte Subscriptions, um Re-Renders zu minimieren, und funktioniert sowohl mit plain JavaScript als auch mit TypeScript. Für viele React-Apps ist es der Weg mit dem geringsten Overhead, um State global zu teilen.
Einen Store erstellen
Ein Store ist ein Hook. Der State und die Funktionen, die ihn aktualisieren, existieren gemeinsam.
// store.js
import { create } from "zustand";
export const useStore = create((set, get) => ({
count: 0,
increment: () => set((state) => ({ count: state.count + 1 })),
decrement: () => set((state) => ({ count: state.count - 1 })),
reset: () => set({ count: 0 }),
double: () => get().count * 2,
}));
set führt das zurückgegebene Objekt in den State zusammen, und get liest den aktuellen State imperativ aus. Es muss kein Reducer geschrieben und keine Action dispatched werden – es gibt lediglich Funktionen, die den State aktualisieren.
State mit Selectors auslesen
Komponenten abonnieren genau den Slice, den sie benötigen.
// Counter.jsx
import { useStore } from "./store";
export function Counter() {
const count = useStore((state) => state.count);
const increment = useStore((state) => state.increment);
return <button onClick={increment}>{count}</button>;
}
Da die Komponente nur state.count abonniert, führen Aktualisierungen an nicht verwandten Teilen des Stores nicht zu einem Re-Render. Das ist der entscheidende Performance-Unterschied zur Context API von React, bei der jeder Consumer neu gerendert wird, sobald sich der Wert ändert.
Um mehrere Werte auszuwählen, verwende einen shallow comparison, damit die Komponente nur dann neu gerendert wird, wenn sich tatsächlich einer dieser Werte ändert:
// shallow.js
import { useShallow } from "zustand/react/shallow";
const { count, increment } = useStore(
useShallow((state) => ({ count: state.count, increment: state.increment })),
);
State aktualisieren
set führt standardmäßig einen Merge durch, sodass Sie nur die Änderungen angeben müssen. Übergeben Sie eine Funktion, wenn der nächste Wert vom vorherigen abhängt.
// updates.js
set({ count: 0 }); // merge
set((state) => ({ count: state.count + 1 })); // derive from previous
set((state) => ({ items: [...state.items, item] })); // arrays
Bei verschachtelten Objekten ersetzt set den Key auf der obersten Ebene. Verwenden Sie daher den Spread-Operator für das bestehende Objekt, wenn Sie andere Eigenschaften beibehalten möchten. Falls Sie eine mutierende Syntax für tiefe Updates bevorzugen, steht die Immer middleware zur Verfügung.
Async Actions
Asynchrone Logik ist in diesem Fall einfach eine async-Funktion im Store. Es muss keine middleware konfiguriert werden.
// usersStore.js
import { create } from "zustand";
export const useUsers = create((set) => ({
users: [],
loading: false,
error: null,
fetchUsers: async () => {
set({ loading: true, error: null });
try {
const res = await fetch("/api/users");
if (!res.ok) throw new Error(`HTTP ${res.status}`);
set({ users: await res.json(), loading: false });
} catch (error) {
set({ error: error.message, loading: false });
}
},
}));
Die Loading- und Error-Flags befinden sich direkt neben den Daten, und die Komponenten wählen nur die Teile aus, die sie tatsächlich anzeigen.
Slices und middleware
Teilen Sie einen großen Store in Slices auf, die anschließend zu einem einzigen Store kombiniert werden. Jeder Slice ist eine Funktion, die set und get erhält.
// slices/cart.js
export const createCartSlice = (set) => ({
items: [],
addItem: (item) => set((state) => ({ items: [...state.items, item] })),
clear: () => set({ items: [] }),
});
// store.js
import { create } from "zustand";
import { createCartSlice } from "./slices/cart";
import { createUserSlice } from "./slices/user";
export const useStore = create((...args) => ({
...createCartSlice(...args),
...createUserSlice(...args),
}));
Middleware wird wie Funktionen um den Store-Creator herum komponiert. Die gebräuchlichsten sind:
- persist speichert den State in
localStorageoder einem anderen Speicher und stellt ihn wieder her (Rehydration). - devtools verbindet den Store mit der Redux DevTools Extension.
- immer ermöglicht es Ihnen, mutierende Updates zu schreiben, die dennoch immutable bleiben.
// persisted.js
import { persist } from "zustand/middleware";
export const useSettings = create(
persist(
(set) => ({
theme: "dark",
setTheme: (theme) => set({ theme }),
}),
{ name: "settings" },
),
);
Wann man Zustand einsetzen sollte
Zustand eignet sich hervorragend für globalen Client-State: Themes und Einstellungen, ein Warenkorb, UI-Zustände, die über weit entfernte Komponenten hinweg geteilt werden, Entwürfe, Filter und Modal-Zustände. Es ist besonders nützlich, wenn Context zu viele Re-Renders verursachen würde oder wenn Redux sich nach mehr Struktur anfühlt, als man eigentlich benötigt.
Verwenden Sie es nicht für Server-State. Daten, die von einer API kommen, benötigen Caching, Deduplizierung, Background-Refetching und Invalidation – diese Anforderungen werden von TanStack Query wesentlich besser gelöst. Das gängige Setup besteht aus Zustand für den Client-State und TanStack Query für den Server-State.
Best Practices
- Lesen Sie den State immer mit einem Selector aus.
- Verwenden Sie
useShallow, wenn Sie mehrere Werte auswählen. - Definieren Sie Actions im Store, damit Komponenten die Struktur des State nicht kennen müssen.
- Teilen Sie große Stores in Slices auf.
- Verwenden Sie die persist middleware für Einstellungen, die Neuladevorgänge überstehen sollen.
- Halten Sie Server-Daten aus dem Store heraus.
- Speichern Sie nach Möglichkeit einfache, serialisierbare Daten.
Häufige Fehler
- Aufruf von
useStore()ohne Selector, was zu einem Re-Render bei jeder Änderung führt. - Rückgabe eines neuen Objekts aus einem Selector ohne shallow comparison.
- Verwendung von Zustand als Cache für Server-State.
- Mutation von verschachteltem State ohne Immer, wodurch Updates verloren gehen.
- Erstellung eines Stores innerhalb einer Komponente, wodurch dieser bei jedem Render zurückgesetzt wird.
- Einführung von Redux-typischem Boilerplate, das Zustand eigentlich eliminieren soll.
Wie geht es weiter?
Zustand ist oft der ideale Mittelweg zwischen Context und Redux. Vergleiche es mit Context für einfaches Teilen von Daten und Redux Toolkit für einen strikten, tool-gestützten State, und lagere Server-Daten nach TanStack Query aus. Für Vue übernimmt Pinia die gleiche Rolle.