Globaler State mit Zustand in React
Globaler State mit Zustand
~16 Min. Lesezeit Zuletzt aktualisiert am 8. August 2026
Zustand (deutsch für "state", aber der Name der Bibliothek, ausgesprochen wie das englische Wort "Sustained" ohne das S – kein Wortspiel-Zufall, der Autor ist deutschsprachig) ist eine sehr kleine, sehr beliebte State-Management-Bibliothek, die genau das Problem aus dem letzten Kapitel löst: gezielte Abonnements statt "alles oder nichts".
Warum Zustand statt "noch mehr Context"
Man KÖNNTE das Re-Render-Problem auch mit purem Context lösen – indem man z. B. für jeden unabhängigen State-Bereich einen EIGENEN Context anlegt (AuthContext, NotificationContext, CartContext, ...). Das funktioniert, wird aber schnell unübersichtlich (viele verschachtelte Provider) und löst das Problem nicht INNERHALB eines einzelnen, natürlich zusammengehörigen State-Bereichs (z. B. "nur user, nicht isLoading" aus DEMSELBEN Auth-State). Zustand löst beides mit einem einzigen, sehr kleinen Konzept: einem Store mit Selektoren.
Zustand installieren
npm install zustandsrc/store/authStore.js anlegen: der erste Store
Ein Zustand-Store ist erstaunlich wenig Code – keine Reducer, keine Action-Types, kein Boilerplate. create() nimmt eine Funktion entgegen, die den Anfangszustand UND die Funktionen liefert, die ihn verändern:
import { create } from 'zustand';
export const useAuthStore = create((set) => ({
user: null,
login(username) {
set({ user: { username } });
},
logout() {
set({ user: null });
},
}));Das Ergebnis von create(...) ist selbst ein React Hook – deshalb der Name useAuthStore. set(...) ist Zustands Version von "State aktualisieren" – es MERGT das übergebene Objekt in den bestehenden State (wie this.setState() in Klassenkomponenten früher), statt ihn komplett zu ersetzen.
Der entscheidende Unterschied: Selektoren
Statt (wie bei useAuth()) IMMER den kompletten Store zu holen, ruft man useAuthStore mit einer SELEKTOR-Funktion auf, die NUR den gewünschten Ausschnitt zurückgibt:
// Komplett (wie bisher Context) - re-rendert bei JEDER Store-Änderung:
const { user, login, logout } = useAuthStore();
// Selektiv (Zustands Superkraft) - re-rendert NUR, wenn 'user' sich ändert:
const user = useAuthStore((state) => state.user);
const login = useAuthStore((state) => state.login);Zustand vergleicht bei jeder Store-Änderung das ERGEBNIS jedes registrierten Selektors mit seinem vorherigen Wert (per Object.is, sehr ähnlich zu ===). Nur Komponenten, deren Selektor-Ergebnis sich WIRKLICH geändert hat, rendern neu – Komponenten, die nur state.login abonniert haben (eine stabile Funktionsreferenz, die sich nie ändert), rendern NIE neu, egal was sonst im Store passiert.
AuthContext.jsx entfernen, App.jsx umstellen
src/context/AuthContext.jsx wird jetzt nicht mehr gebraucht – löschen Sie die Datei. App.jsx verwendet ab jetzt useAuthStore mit Selektoren statt useAuth():
import { lazy, Suspense } from 'react';
import { Routes, Route } from 'react-router-dom';
import { useAuthStore } from './store/authStore';
import { useDocumentTitle } from './hooks/useDocumentTitle';
import RenderCounter from './components/RenderCounter';
import ProductListPage from './pages/ProductListPage';
import ProductDetailPage from './pages/ProductDetailPage';
import ProtectedRoute from './components/ProtectedRoute';
const ReviewsPage = lazy(() => import('./pages/ReviewsPage'));
const LoginPage = lazy(() => import('./pages/LoginPage'));
const AccountPage = lazy(() => import('./pages/AccountPage'));
function App() {
const user = useAuthStore((state) => state.user);
const login = useAuthStore((state) => state.login);
const logout = useAuthStore((state) => state.logout);
useDocumentTitle('Product Catalog');
return (
<div className="app">
<header className="app-header">
<h1>Product Catalog</h1>
<RenderCounter label="App-Header" />
{user ? (
<p>Logged in as {user.username} <button onClick={logout}>Log out</button></p>
) : (
<button onClick={() => login('Jane Doe')}>Log in</button>
)}
</header>
<Suspense fallback={<p>Loading page...</p>}>
<Routes>
<Route path="/" element={<ProductListPage />} />
<Route path="/products/:sku" element={<ProductDetailPage />}>
<Route path="reviews" element={<ReviewsPage />} />
</Route>
<Route path="/login" element={<LoginPage />} />
<Route
path="/account"
element={
<ProtectedRoute>
<AccountPage />
</ProtectedRoute>
}
/>
</Routes>
</Suspense>
</div>
);
}
export default App;Jede der drei Zeilen useAuthStore((state) => state.user) etc. ist ein EIGENES, unabhängiges Abonnement. Zustand ist schlau genug, mehrere Selektor-Aufrufe im gleichen Component-Body effizient zu behandeln, statt bei jedem einen kompletten neuen Store-Zugriff zu machen.
Weitere Verbraucher anpassen: ProtectedRoute und LoginPage
ProtectedRoute und LoginPage aus "React für Einsteiger" riefen bisher useAuth() auf – auch sie werden auf Selektoren umgestellt:
import { Navigate } from 'react-router-dom';
import { useAuthStore } from '../store/authStore';
function ProtectedRoute({ children }) {
const user = useAuthStore((state) => state.user);
if (!user) {
return <Navigate to="/login" replace />;
}
return children;
}
export default ProtectedRoute;import { useState } from 'react';
import { useNavigate } from 'react-router-dom';
import { useAuthStore } from '../store/authStore';
function LoginPage() {
const login = useAuthStore((state) => state.login);
const [username, setUsername] = useState('');
const navigate = useNavigate();
function handleSubmit(event) {
event.preventDefault();
login(username || 'Jane Doe');
navigate('/account');
}
return (
<form onSubmit={handleSubmit}>
<h2>Log In</h2>
<input
type="text"
placeholder="Username"
value={username}
onChange={(event) => setUsername(event.target.value)}
/>
<button type="submit">Log In</button>
</form>
);
}
export default LoginPage;Achtung: Keine <AuthProvider>-Komponente mehr nötig, weder in App.jsx noch in main.jsx – das ist ein grundlegender Unterschied zu Context. Ein Zustand-Store existiert AUSSERHALB des Komponentenbaums (technisch: als Modul-Singleton), er muss nirgends "umschlossen" werden.
import { StrictMode } from 'react';
import { createRoot } from 'react-dom/client';
import { BrowserRouter } from 'react-router-dom';
import App from './App.jsx';
import './index.css';
createRoot(document.getElementById('root')).render(
<StrictMode>
<BrowserRouter>
<App />
</BrowserRouter>
</StrictMode>
);Der <AuthProvider>-Import und -Wrapper sind komplett verschwunden – <App /> hängt jetzt direkt unter <BrowserRouter>. Das ist der sichtbarste strukturelle Unterschied zwischen Context und einem externen Store.
Der Beweis: RenderCounter bleibt jetzt ruhig
Wiederholen wir das Experiment aus dem letzten Kapitel gedanklich: Fügen Sie testweise einen weiteren, unabhängigen Wert zum Store hinzu (z. B. notificationCount wie zuvor) und einen Button in einer BELIEBIGEN Komponente, die NUR state.notificationCount abonniert. Sie werden sehen: Apps RenderCounter bleibt UNVERÄNDERT stehen, wenn Sie diesen Button klicken – App hat notificationCount ja nie abonniert. Genau das war mit reinem Context unmöglich.
Wann reicht Context also noch?
- Werte, die sich WIRKLICH selten ändern und global gebraucht werden, aber nicht performance-kritisch sind (z. B. ein statisches Theme-Objekt, das einmalig beim App-Start gesetzt wird).
- Sehr kleine Apps/Prototypen, bei denen die zusätzliche Abhängigkeit (Zustand ist winzig, aber trotzdem eine Bibliothek) den Aufwand nicht wert ist.
- Situationen, in denen Sie bewusst WOLLEN, dass alle Verbraucher synchron neu rendern (selten, aber es gibt sie – z. B. bei bestimmten Test-/Storybook-Szenarien).
Tipp: Faustregel für Projekte ab mittlerer Größe: Context für seltene, statische Konfiguration (Theme, i18n-Sprache), einen echten Store (Zustand/Redux Toolkit) für ALLES, was sich im Laufe der Nutzung tatsächlich häufig ändert (Auth-Status, Warenkorb, Formulardaten über mehrere Screens hinweg, UI-Zustand wie offene Modals).