persist und devtools kombinieren, ohne den Storage aufzublaehen oder DevTools zu verwirren
Ein Zustand-Store bleibt ohne Middleware fluechtig und schwer zu debuggen. persist und devtools loesen genau diese beiden Probleme, sobald man weiss, wie sie kombiniert und mit partialize gezielt eingeschraenkt werden.
Inhaltsverzeichnis
- 1. Was Zustand-Middlewares ueberhaupt sind
- 2. persist im Grundaufbau
- 3. partialize fuer selektive Persistenz
- 4. devtools und die Redux DevTools Extension
- 5. Mehrere Middlewares kombinieren
- 6. Versionierung und Migration gespeicherter States
- 7. Eigene Storage-Adapter einsetzen
- 8. Hydration-Timing bei Server-Side-Rendering
- 9. Haeufige Fehler bei Zustand-Middleware
- 10. Zusammenfassung
- 11. FAQ
1. Was Zustand-Middlewares ueberhaupt sind
Eine Zustand-Middleware ist eine Funktion, die den eigentlichen Store-Creator umschliesst und zusaetzliches Verhalten injiziert, ohne die fachliche Store-Logik selbst zu veraendern. persist ergaenzt Speicherung und Wiederherstellung des States, devtools ergaenzt eine Anbindung an die Redux-DevTools-Browsererweiterung, beide arbeiten dabei transparent im Hintergrund der eigentlichen State-Updates, ohne dass die aufrufenden Komponenten davon ueberhaupt etwas mitbekommen.
Im Vergleich zu Redux, wo Middleware ueber einen eigenen Store-Enhancer-Mechanismus mit vergleichsweise viel Boilerplate eingebunden wird, ist eine Zustand-Middleware schlicht eine Funktion hoeherer Ordnung, die um create() gelegt wird. Das macht Middlewares minimal invasiv, leicht kombinierbar und einfach wieder entfernbar, ohne den Rest des Stores anfassen zu muessen. Fuer ein bestehendes Projekt bedeutet das, dass Persistenz oder DevTools-Anbindung meist ohne grosse Refaktorierung nachtraeglich ergaenzt werden koennen, weil sich am eigentlichen Store-Interface nichts aendert.
2. persist im Grundaufbau
persist(config, { name, storage }) speichert den Store-State automatisch in localStorage, oder alternativ in sessionStorage beziehungsweise einem beliebigen eigenen Storage-Adapter. Die Option name bestimmt den Schluessel, unter dem der serialisierte State im Storage abgelegt wird, sie sollte projektweit eindeutig sein, um Kollisionen mit anderen Stores zu vermeiden.
Beim Start der Anwendung liest persist automatisch aus dem Storage und rehydriert den Store, bevor die erste Komponente ueberhaupt rendert. Ein eigener useEffect, der beim Mount manuell aus localStorage liest und den Store befuellt, ist damit nicht mehr noetig, dieser komplette Ablauf ist bereits in der Middleware gekapselt.
import { create } from 'zustand';
import { persist } from 'zustand/middleware';
const useSettingsStore = create(
persist(
(set) => ({
theme: 'light',
setTheme: (theme) => set({ theme }),
}),
{ name: 'settings-storage' }
)
);
3. partialize fuer selektive Persistenz
partialize(state) erlaubt es, nur einen Teil des States zu persistieren, etwa dauerhafte Benutzereinstellungen statt fluechtiger UI-Flags wie isModalOpen oder activeTab. Ohne partialize landet standardmaessig der komplette Store im Storage, inklusive Werten, die beim naechsten Laden ohnehin auf ihren Ausgangswert zurueckgesetzt werden sollten.
Praktisch wird partialize meist als Funktion definiert, die ein neues Objekt mit nur den gewuenschten Schluesseln zurueckgibt. In TypeScript laesst sich der Rueckgabetyp ueber Partial absichern, sodass ein versehentlich vergessenes oder falsch benanntes Feld bereits beim Kompilieren auffaellt statt erst zur Laufzeit im Storage zu fehlen.
const useAppStore = create(
persist(
(set) => ({
theme: 'light',
language: 'de',
isModalOpen: false,
activeTab: 'overview',
setTheme: (theme) => set({ theme }),
}),
{
name: 'app-storage',
// Nur dauerhafte Einstellungen persistieren,
// fluechtige UI-Flags bleiben aussen vor
partialize: (state) => ({
theme: state.theme,
language: state.language,
}),
}
)
);
4. devtools und die Redux DevTools Extension
devtools(config, { name }) verbindet den Store mit der Redux-DevTools-Browsererweiterung. Jede State-Aenderung erscheint dort als eigener Eintrag im Zeitstrahl, inklusive der Moeglichkeit, per Time-Travel-Debugging zu einem frueheren State-Snapshot zurueckzuspringen und die Anwendung in genau diesem Zustand zu inspizieren.
Fuer aussagekraeftige Eintraege im Zeitstrahl sollten set-Aufrufe mit einem zweiten und dritten Argument versehen werden, etwa set(newState, false, 'todos/add'). Ohne diesen Namen erscheinen in den DevTools nur generische, anonyme Actions, was das Nachvollziehen einer Fehlerursache in einem groesseren Store erheblich erschwert.
import { devtools } from 'zustand/middleware';
const useTodoStore = create(
devtools(
(set) => ({
todos: [],
addTodo: (text) =>
set(
(state) => ({ todos: [...state.todos, { id: crypto.randomUUID(), text }] }),
false,
'todos/add'
),
}),
{ name: 'TodoStore' }
)
);
5. Mehrere Middlewares kombinieren
Middlewares werden verschachtelt angewendet, ein uebliches Muster ist devtools(persist(immer(storeCreator))). Die Reihenfolge dieser Verschachtelung ist nicht beliebig, sie beeinflusst sowohl, was in den DevTools sichtbar wird, als auch, wie der State vor dem Speichern in persist transformiert wird.
Eine ungeschickte Reihenfolge, etwa persist ausserhalb von devtools statt innerhalb, kann dazu fuehren, dass die DevTools den bereits serialisierten oder rehydrierten State nicht korrekt oder mit falschen Action-Namen anzeigen. In TypeScript wird die Typinferenz bei mehreren verschachtelten Middlewares zudem spuerbar komplexer und profitiert von expliziten StateCreator-Typannotationen an den jeweiligen Store-Slices, insbesondere sobald zusaetzlich noch immer als dritte Middleware in derselben Kette eingesetzt wird.
import { create } from 'zustand';
import { devtools, persist } from 'zustand/middleware';
import { immer } from 'zustand/middleware/immer';
const useStore = create(
devtools(
persist(
immer((set) => ({
todos: [],
addTodo: (text) =>
set((state) => {
state.todos.push({ id: crypto.randomUUID(), text });
}),
})),
{ name: 'todo-storage' }
),
{ name: 'TodoStore' }
)
);
6. Versionierung und Migration gespeicherter States
Die Optionen version und migrate in persist erlauben es, gespeicherte States aus aelteren App-Versionen beim Laden in das aktuelle Schema zu ueberfuehren, statt sie zu verwerfen oder einen Laufzeitfehler zu riskieren, wenn erwartete Felder fehlen.
migrate(persistedState, version) erhaelt den alten State zusammen mit dessen gespeicherter Versionsnummer und gibt den transformierten, aktuellen State zurueck. Das ist besonders relevant, wenn ein Feld umbenannt, ein neues Pflichtfeld eingefuehrt oder die Struktur eines verschachtelten Objekts geaendert wurde, ohne dass Nutzer mit aelterem, lokal gespeichertem State beim naechsten Besuch einen defekten Store erhalten. Wird die Versionsnummer bei jeder inkompatiblen Schema-Aenderung konsequent erhoeht, laesst sich diese Migration zudem in kleinen, gut nachvollziehbaren Schritten statt in einem einzigen grossen Sprung durchfuehren.
const useSettingsStore = create(
persist(
(set) => ({
themeMode: 'light',
setThemeMode: (themeMode) => set({ themeMode }),
}),
{
name: 'settings-storage',
version: 2,
migrate: (persistedState, version) => {
if (version === 1) {
// Altes Feld 'theme' wurde zu 'themeMode' umbenannt
return { themeMode: persistedState.theme ?? 'light' };
}
return persistedState;
},
}
)
);
7. Eigene Storage-Adapter einsetzen
Die storage-Option akzeptiert jeden Adapter, der die Methoden getItem, setItem und removeItem implementiert. Damit laesst sich statt localStorage beispielsweise AsyncStorage in React Native einsetzen, oder ein verschluesselnder Wrapper um localStorage, wenn sensible Daten lokal abgelegt werden muessen.
createJSONStorage(() => customStorageEngine) kapselt die JSON-Serialisierung, ein eigener Adapter muss deshalb nur die rohe Storage-Schnittstelle bedienen, die Umwandlung zwischen JavaScript-Objekt und String uebernimmt Zustand automatisch. Das reduziert eigenen Code auf die reine Speicherlogik, ohne sich um Serialisierungsdetails kuemmern zu muessen.
import { createJSONStorage, persist } from 'zustand/middleware';
const encryptedStorage = {
getItem: (name) => {
const raw = localStorage.getItem(name);
return raw ? decrypt(raw) : null;
},
setItem: (name, value) => localStorage.setItem(name, encrypt(value)),
removeItem: (name) => localStorage.removeItem(name),
};
const useSecureStore = create(
persist(
(set) => ({ token: null, setToken: (token) => set({ token }) }),
{ name: 'secure-storage', storage: createJSONStorage(() => encryptedStorage) }
)
);
8. Hydration-Timing bei Server-Side-Rendering
Bei Server-Side-Rendering, etwa mit Next.js, existiert beim ersten Server-Render kein localStorage. persist markiert den Store deshalb zunaechst als nicht hydriert, der eigentliche Rehydrierungs-Zeitpunkt auf dem Client laesst sich ueber die Callback-Option onRehydrateStorage abfangen.
Eine verbreitete Loesung ist ein zusaetzliches hasHydrated-Flag im Store, das erst nach erfolgreichem Rehydrieren auf true gesetzt wird. Komponenten koennen so bis dahin einen Platzhalter oder Loading-Zustand rendern, statt einen kurzzeitigen Server-Client-Mismatch anzuzeigen, der sonst zu einem sichtbaren Flackern der Werte fuehren wuerde. Gerade bei theme-abhaengigen Werten faellt ein solcher Mismatch sofort optisch auf, weshalb sich der zusaetzliche Aufwand fuer das Flag in produktiven Anwendungen fast immer lohnt.
const useAppStore = create(
persist(
(set) => ({
hasHydrated: false,
theme: 'light',
setHasHydrated: (value) => set({ hasHydrated: value }),
}),
{
name: 'app-storage',
onRehydrateStorage: () => (state) => {
state?.setHasHydrated(true);
},
}
)
);
function ThemeSwitcher() {
const hasHydrated = useAppStore((s) => s.hasHydrated);
if (!hasHydrated) return <Skeleton />;
return <ThemeToggle />;
}
9. Haeufige Fehler bei Zustand-Middleware
Der haeufigste Fehler ist fehlendes partialize, wodurch der komplette Store inklusive temporaerer UI-Flags im localStorage landet. Das blaeht den Storage unnoetig auf und stellt beim naechsten Laden mitunter unerwartete UI-Zustaende wieder her, etwa ein Modal, das beim Neuladen ploetzlich wieder offen ist, obwohl der Nutzer es laengst geschlossen hatte.
Ein zweiter verbreiteter Fehler ist eine ungeeignete Middleware-Reihenfolge oder das Vergessen von Versionierung bei Schema-Aenderungen. Wird eine version-Erhoehung ohne passende migrate-Funktion vergessen, fuehren alte, bereits persistierte States nach einem Deployment zu Laufzeitfehlern, weil erwartete Felder im gespeicherten Objekt schlicht fehlen.
| Middleware | Zweck | Typische Stolperfalle | Kombinierbar mit |
|---|---|---|---|
| persist | State ueber Seiten-Reloads hinweg in localStorage oder eigenem Storage sichern | Fehlendes partialize blaeht den Storage mit fluechtigen UI-Flags auf | devtools, immer, subscribeWithSelector |
| devtools | Anbindung an Redux DevTools inklusive Time-Travel-Debugging | Set-Aufrufe ohne Action-Namen erscheinen als generische, unlesbare Eintraege | persist, immer |
| immer | Mutierbar wirkende Schreibweise fuer verschachtelte State-Updates | Vergessene return-Anweisung bei Nicht-Draft-Rueckgaben in komplexen Updates | persist, devtools |
| subscribeWithSelector | Gezieltes Abonnieren einzelner State-Ausschnitte ausserhalb von React | Fehlende equalityFn fuehrt zu unnoetig haeufigen Callback-Aufrufen | persist, devtools, immer |
Mironsoft
React-Architektur, Performance und Magento-Frontend-Integration
React-Frontends, die schnell bleiben statt mit jedem Feature langsamer zu werden?
Wir prüfen bestehende React-Anwendungen auf unnötige Re-Renders, aufgeblähte Bundles und fragile State-Verwaltung und bauen daraus ein Frontend, das performant bleibt und sich sauber an Magento oder andere Backends anbindet.
Performance-Audit
Re-Renders, Bundle-Größe und Ladezeiten systematisch messen und beheben.
State-Architektur
Context, Zustand und Server State sauber trennen statt alles in einen Topf zu werfen.
Magento-Integration
GraphQL- oder REST-Anbindung an Magento robust und typsicher aufbauen.
10. Zusammenfassung
Zustand Middleware persist und devtools: Das Wichtigste auf einen Blick
persist
Speichert und rehydriert den Store automatisch aus localStorage oder einem beliebigen Storage-Adapter.
partialize
Beschraenkt die Persistenz auf dauerhafte Felder, fluechtige UI-Flags bleiben ausserhalb des Storage.
devtools
Verbindet den Store mit Redux DevTools, benannte Actions machen den Zeitstrahl aussagekraeftig.
Middleware-Reihenfolge
Verschachtelung wie devtools(persist(immer(...))) beeinflusst Sichtbarkeit und Serialisierung direkt.