Magento 2 Experten — Hyvä Theme, Tailwind CSS & SEO aus einer Hand ›

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 zustand

src/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:

src/store/authStore.js
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():

src/App.jsx
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:

src/components/ProtectedRoute.jsx
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;
src/pages/LoginPage.jsx
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.

src/main.jsx
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).