</>
{ }
React · State Management · Performance · Architektur
React Context vs. Zustand vs. Jotai
Was wann und warum nutzen

Die falsche Wahl beim State Management kostet in React-Projekten mehr Performance und Wartbarkeit als fast jede andere Architekturentscheidung. Context, Zustand und Jotai lösen dasselbe Grundproblem auf grundlegend verschiedene Weisen – dieser Artikel erklärt die Unterschiede mit echten Codebeispielen und zeigt, wann welches Tool die richtige Wahl ist.

15 Min. Lesezeit Context API · Zustand · Jotai · Re-render · Atoms React 18+ · TypeScript · Vite

1. Warum State Management in React eine Architekturentscheidung ist

State Management ist in React-Projekten keine rein technische Entscheidung, sondern eine architektonische. Wer früh die falsche Lösung wählt, zahlt den Preis später in Form von unkontrollierten Re-renders, schwer testoprübarem Code und steigender Komplexität bei jedem neuen Feature. Die gute Nachricht: React bietet heute mit Context API, Zustand und Jotai drei gut dokumentierte Ansätze, die verschiedene Problemklassen gezielt lösen – ohne dass man in jedem Projekt Redux einsetzen muss.

Der entscheidende Unterschied zwischen den drei Ansätzen liegt nicht in der Syntax, sondern im mentalen Modell. Context API denkt von oben nach unten: Ein Provider hält den State, alle Consumer darunter reagieren auf Änderungen. Zustand denkt von außen: Ein Store existiert außerhalb des React-Komponentenbaums, Komponenten abonnieren gezielt Teile davon. Jotai denkt von unten nach oben: Einzelne Atome halten minimale State-Einheiten, komplexerer State wird durch Ableitung komponiert. Wer das mentale Modell versteht, kann in wenigen Minuten entscheiden, welcher Ansatz zum jeweiligen Problem passt.

In der Praxis sieht man häufig den Fehler, dass Entwickler Context API für globalen, häufig wechselnden State einsetzen – und sich dann über Performance-Probleme wundern. Oder Zustand für Theme-Einstellungen und Sprachkonfiguration verwenden, obwohl Context hier vollkommen ausreicht. Die folgenden Abschnitte klären systematisch, wann welches Werkzeug die richtige Wahl ist.

2. React Context API: Stärken, Grenzen und das Re-render-Problem

Die React Context API ist seit React 16.3 Bestandteil des Kerns und braucht keine externe Abhängigkeit. Ihr Stärkenbereich liegt bei State, der selten wechselt und tief im Komponentenbaum verfügbar sein muss: Theme-Einstellungen, Benutzerauthentifizierung, Sprachkonfiguration, Feature-Flags. In diesen Szenarien schlägt Context alle externen Bibliotheken, weil die Lösung null Zusatzbytes kostet und direkt in React integriert ist.

Das klassische Problem von Context ist das Re-render-Verhalten: Jede Änderung am Context-Value löst ein Neu-Rendern aller Consumer aus – unabhängig davon, ob der konsumierte Teil des Values sich überhaupt verändert hat. Ein Context, der ein Objekt mit zehn Feldern hält, löst bei einer Änderung an einem einzigen Feld das Neu-Rendern aller Komponenten aus, die diesen Context konsumieren. Dieses Problem lässt sich mit useMemo für den Value und React.memo für Komponenten begrenzen, aber nicht vollständig eliminieren. Für häufig wechselnden State mit vielen Konsumenten ist Context deswegen strukturell die falsche Wahl.


// auth-context.tsx — Context is ideal for infrequently changing state
import { createContext, useContext, useMemo, useState } from 'react';

interface AuthState {
  user: User | null;
  isLoading: boolean;
  login: (credentials: Credentials) => Promise<void>;
  logout: () => void;
}

const AuthContext = createContext<AuthState | null>(null);

export function AuthProvider({ children }: { children: React.ReactNode }) {
  const [user, setUser] = useState<User | null>(null);
  const [isLoading, setIsLoading] = useState(false);

  // Memoize the value object to prevent unnecessary re-renders
  const value = useMemo(() => ({
    user,
    isLoading,
    login: async (credentials: Credentials) => {
      setIsLoading(true);
      const result = await authService.login(credentials);
      setUser(result.user);
      setIsLoading(false);
    },
    logout: () => setUser(null),
  }), [user, isLoading]);

  return <AuthContext.Provider value={value}>{children}</AuthContext.Provider>;
}

// Custom hook with invariant — always use this, never useContext directly
export function useAuth(): AuthState {
  const ctx = useContext(AuthContext);
  if (!ctx) throw new Error('useAuth must be used inside AuthProvider');
  return ctx;
}

3. Zustand: Einfaches globales State Management ohne Boilerplate

Zustand (von Poimandres, denselben Entwicklern wie Jotai und React Three Fiber) ist ein minimalistisches State-Management-Tool für React, das einen globalen Store außerhalb des React-Komponentenbaums hält. Der Schlüsselunterschied zu Context: Komponenten abonnieren mit einem Selector gezielt nur den Teil des States, den sie brauchen – und werden nur neu gerendert, wenn sich genau dieser Teil ändert. Zustand hat keine Provider, braucht kein Wrapping und funktioniert auch außerhalb von React-Komponenten.

Die API von Zustand ist bewusst minimal: create definiert den Store mit initialem State und Actions, useStore(selector) abonniert einen Ausschnitt. Das ist alles. Keine Reducer, keine Actions-Creator, kein Dispatch. Diese Einfachheit macht Zustand besonders geeignet für mittlere bis große Anwendungen mit globalem State, der häufig wechselt: Shopping-Cart-State, UI-State (offene Modals, aktive Tabs), asynchrone Daten mit Loading-States. Zustand unterstützt Middleware für Logging, Persistenz (localStorage), Immer für unveränderliche Updates und DevTools.


// cart-store.ts — Zustand store with TypeScript, Immer and persist middleware
import { create } from 'zustand';
import { immer } from 'zustand/middleware/immer';
import { persist } from 'zustand/middleware';

interface CartItem { id: string; name: string; price: number; quantity: number; }

interface CartStore {
  items: CartItem[];
  total: number;
  addItem: (item: Omit<CartItem, 'quantity'>) => void;
  removeItem: (id: string) => void;
  updateQuantity: (id: string, quantity: number) => void;
  clearCart: () => void;
}

export const useCartStore = create<CartStore>()(
  persist(
    immer((set, get) => ({
      items: [],
      total: 0,

      addItem: (item) => set((state) => {
        const existing = state.items.find(i => i.id === item.id);
        if (existing) {
          existing.quantity += 1;
        } else {
          state.items.push({ ...item, quantity: 1 });
        }
        // Recalculate total after mutation
        state.total = state.items.reduce((sum, i) => sum + i.price * i.quantity, 0);
      }),

      removeItem: (id) => set((state) => {
        state.items = state.items.filter(i => i.id !== id);
        state.total = state.items.reduce((sum, i) => sum + i.price * i.quantity, 0);
      }),

      updateQuantity: (id, quantity) => set((state) => {
        const item = state.items.find(i => i.id === id);
        if (item) { item.quantity = Math.max(0, quantity); }
        state.total = state.items.reduce((sum, i) => sum + i.price * i.quantity, 0);
      }),

      clearCart: () => set({ items: [], total: 0 }),
    })),
    { name: 'cart-storage' } // persists to localStorage automatically
  )
);

// Usage in component — only re-renders when items.length changes
function CartBadge() {
  const itemCount = useCartStore(state => state.items.length);
  return <span>{itemCount}</span>;
}

4. Jotai: Atomares State Management von unten nach oben

Jotai basiert auf dem Atom-Konzept, das ursprünglich von Recoil popularisiert wurde. Ein Atom ist die kleinste State-Einheit: ein Stück State mit einem initialen Wert. Komplexerer State entsteht durch abgeleitete Atome, die andere Atome kombinieren oder transformieren. Dieses Bottom-up-Modell ist das Gegenteil von Context (top-down) und ergänzt sich gut mit Zustand (der für vertikalen Store-State optimiert ist).

Der praktische Vorteil von Jotai liegt in der granularen Reaktivität: Jede Komponente, die ein Atom mit useAtom konsumiert, wird nur neu gerendert, wenn sich genau dieses Atom ändert. Abgeleitete Atome mit atom(get => …) werden automatisch neu berechnet, wenn ihre Abhängigkeiten sich ändern – ähnlich wie berechnete Eigenschaften in Vue oder MobX. Jotai eignet sich besonders für komplexe lokale State-Graphen, für State, der zwischen wenigen Komponenten geteilt wird ohne globalen Store zu benötigen, und für State mit komplexen Ableitungsketten.


// filter-atoms.ts — Jotai atoms for a product filter UI
import { atom } from 'jotai';
import { atomWithStorage } from 'jotai/utils';

// Primitive atoms — the smallest state units
export const searchQueryAtom = atom('');
export const selectedCategoryAtom = atom<string | null>(null);
export const priceRangeAtom = atom({ min: 0, max: 1000 });
export const sortOrderAtom = atomWithStorage<'asc' | 'desc'>('sortOrder', 'asc');

// Derived atom — automatically recomputes when dependencies change
export const activeFilterCountAtom = atom((get) => {
  let count = 0;
  if (get(searchQueryAtom).length > 0) count++;
  if (get(selectedCategoryAtom) !== null) count++;
  const { min, max } = get(priceRangeAtom);
  if (min > 0 || max < 1000) count++;
  return count;
});

// Async derived atom — fetches based on filter state
export const filteredProductsAtom = atom(async (get) => {
  const query = get(searchQueryAtom);
  const category = get(selectedCategoryAtom);
  const { min, max } = get(priceRangeAtom);

  const response = await fetch(
    `/api/products?q=${query}&cat=${category ?? ''}&minPrice=${min}&maxPrice=${max}`
  );
  return response.json() as Promise<Product[]>;
});

// Component only re-renders when activeFilterCountAtom changes
function FilterBadge() {
  const [count] = useAtom(activeFilterCountAtom);
  return count > 0 ? <span className="badge">{count} Filter aktiv</span> : null;
}

5. Performance-Analyse: Wer rendert wann neu?

Der praktisch wichtigste Unterschied zwischen den drei Ansätzen ist das Re-render-Verhalten. Bei Context API rendert jeder Consumer neu, wenn der Context-Value sich ändert – selbst wenn der konsumierte Teil des Values identisch bleibt. Das lässt sich nur durch Aufteilen in mehrere, spezialisierte Contexts oder durch externe Selektoren wie use-context-selector lindern. Bei Zustand rendert eine Komponente nur neu, wenn der von ihr selektierte State-Ausschnitt sich ändert – durch referenzielle Gleichheit (shallow compare). Bei Jotai rendert eine Komponente nur neu, wenn das von ihr konsumierte Atom sich ändert.

In der Praxis bedeutet das: Eine Anwendung mit einem globalen Context-Objekt, das zwanzig verschiedene State-Werte hält, löst bei jeder einzelnen State-Änderung potenziell dutzende unnötige Re-renders aus. Dieselbe Anwendung mit einem Zustand-Store oder Jotai-Atomen für dieselben Werte rendert nur die Komponenten neu, die tatsächlich den veränderten State konsumieren. In großen Anwendungen mit komplexen Komponentenbäumen ist dieser Unterschied mit dem React DevTools Profiler deutlich messbar. Die Faustregel: Context für State, der sich maximal ein- bis zweimal pro Session ändert. Zustand oder Jotai für alles andere.

6. TypeScript-Integration im Vergleich

Alle drei Ansätze bieten starke TypeScript-Unterstützung, aber auf verschiedene Weisen. Context API erfordert explizite Typannotation beim createContext-Aufruf und gibt ohne Default-Value null zurück, was einen Null-Check im Custom Hook erfordert. Das ist Boilerplate, aber typsicher. Zustand ist mit dem generischen create<StoreType>()-Muster vollständig typsiert – TypeScript inferiert die Typen aller Selektoren und Actions automatisch. Jotai ist vom Atom-Typ abhängig: Ein atom(0) ist automatisch ein PrimitiveAtom<number>, abgeleitete Atome erben die Typen ihrer Abhängigkeiten.

Ein praktischer Unterschied zeigt sich bei komplexen Store-Updates: Zustand mit Immer-Middleware erlaubt mutierenden Code, der durch Immer intern zu unveränderlichen Updates übersetzt wird – TypeScript sieht den korrekten, unveränderlichen Typ. Jotai mit atomWithReducer oder atomFamily erfordert mehr explizite Typangaben, bietet dafür aber sehr präzise Typsicherheit für Ableitungsketten. Für große Teams mit strenger TypeScript-Konfiguration ist Zustand durch die explizite Store-Typisierung oft wartbarer als Jotai.

7. DevTools, Testing und Debugging

Zustand bietet die beste DevTools-Integration: Mit der devtools-Middleware werden alle State-Änderungen und Actions in den Redux DevTools sichtbar. Das ermöglicht Time-Travel-Debugging, Action-Replay und State-Inspektion direkt im Browser – ohne jegliche Konfiguration. Jotai bietet mit jotai/devtools ebenfalls eine Atom-Inspektion für Chrome und Firefox. Context hat keine speziellen DevTools, ist aber über die React DevTools Component Inspector vollständig sichtbar.

Beim Testing ist Context am einfachsten zu mocken: Eine Test-Wrapper-Komponente mit einem anderen Provider-Value ersetzt den State vollständig. Zustand-Stores können pro Test mit beforeEach(() => useStore.setState(initialState)) zurückgesetzt werden. Jotai bietet mit createStore() und dem Provider eine isolierte Store-Instanz pro Test. Alle drei Ansätze lassen sich mit React Testing Library und Vitest gut testen – der Aufwand ist vergleichbar, die Muster unterscheiden sich.

8. Migration und Koexistenz in bestehenden Projekten

In der Praxis müssen die drei Ansätze keine Alternativen sein – sie können problemlos koexistieren. Ein typisches Muster in mittelgroßen React-Anwendungen: Context für Auth und Theme (selten wechselnder, tief verschachtelter State), Zustand für den globalen UI-State und den Shopping-Cart (häufig wechselnder globaler State), Jotai für komplexe Filter- und Formular-State-Graphen (lokaler State mit Ableitungen). Jeder Ansatz in seinem Stärkenbereich ist besser als ein einzelner Ansatz für alle Szenarien.

Die Migration von Redux zu Zustand ist in der Praxis überraschend unkompliziert: Ein Redux-Reducer entspricht in Zustand einem Store-Slice, Actions werden zu direkten Funktionen. Die Migration kann inkrementell erfolgen: Store für Store, Feature für Feature. Context zu Jotai ist etwas aufwendiger, weil das mentale Modell stärker wechselt, aber auch hier ist eine schrittweise Migration möglich. Der häufigste Fehler bei Migrationen: zu versuchen, alle State-Management-Lösungen auf einmal zu wechseln, statt schrittweise vorzugehen.

9. Entscheidungsmatrix: Context vs. Zustand vs. Jotai

Die Wahl zwischen den drei Ansätzen sollte von der Art des States und seinen Nutzungsmustern abhängen, nicht von persönlichen Vorlieben oder dem Populäritätsindex der Bibliothek. Die folgende Matrix fasst die wichtigsten Entscheidungskriterien zusammen.

Kriterium React Context Zustand Jotai
Änderungshäufigkeit Selten (1–2× pro Session) Häufig (jede Interaktion) Häufig, granular
Bundle-Größe 0 KB (built-in) ~1 KB gzip ~3 KB gzip
Boilerplate Mittel (Provider + Hook) Minimal Minimal bis mittel
Re-render-Kontrolle Begrenzt Sehr gut (Selektoren) Sehr gut (Atome)
DevTools React DevTools Redux DevTools Jotai DevTools
Ideal für Auth, Theme, i18n Cart, UI-State, API-Cache Filter, Formulare, Graphen

Eine Faustregel für die tägliche Entscheidung: Wenn der State in einer Komponente entsteht und nur nach unten gereicht wird, bleibt er lokal (useState). Wenn der State von vielen Komponenten auf verschiedenen Ebenen gelesen, aber selten geschrieben wird, ist Context die richtige Wahl. Wenn der State häufig wechselt und von vielen Komponenten geschrieben und gelesen wird, ist Zustand die erste Wahl. Wenn der State komplex abgeleitet ist und granulare Reaktivität braucht, bietet Jotai die eleganteste Lösung.