State-Management in React: Redux Toolkit, leichte Stores, MobX
Wenn ein Zustand wirklich app-weit ist, stellt sich die Werkzeugfrage – Redux Toolkit, ein leichter Store wie Zustand oder Jotai, oder das reaktive Modell von MobX. Warum diese Wahl aber erst nach der Gegenprobe kommt und meistens kleiner ausfällt als gedacht.
Ein globaler Store gehört nicht zur Grundausstattung einer React-Anwendung. Er ist die Ausnahme, die man begründen muss. So steht es im React-Überblick, wo die Besitzfrage im Mittelpunkt stand – wo gehört ein Zustand hin? Offen blieb dort, was man denn nimmt, wenn ein Zustand die Begründung tatsächlich besteht, wenn er also wirklich app-weit ist. Den Vergleich hatte ich versprochen. Hier ist er.
Redaktionelle Einordnung (Juli 2026): Der Werkzeugteil ist eine Momentaufnahme vom Oktober 2021: Redux Toolkit 1.x, Zustand 3.x, Jotai 1.x und MobX 6. Die wichtigere Trennung ist weniger vergänglich: URL-Zustand, Server-Cache und eigener Client-Zustand haben verschiedene Besitzer. Aktuelle Versionen können die Ergonomie ändern, nicht diese Zuständigkeiten.
Bevor wir über Werkzeuge reden, muss ich aber auf die Bremse treten. Die Frage „Redux, Zustand oder MobX?“ wird meistens viel zu früh gestellt – zu einem Zeitpunkt, an dem noch gar nicht feststeht, ob überhaupt ein Store gebraucht wird. Beim genauen Hinsehen entpuppt sich der vermeintlich globale Zustand oft als gar keiner. Deshalb kommt zuerst die Gegenprobe, und erst danach der eigentliche Vergleich.
Nicht jede Zustandsart braucht denselben Speicher. Die Wahl beginnt mit Besitz, Lebensdauer und Synchronisationsbedarf.
Lokaler UI-Zustand
- Besitzer · eine Komponente oder ein begrenzter Teilbaum
- Lebensdauer · an die Darstellung gebunden
- Beispiele · geöffneter Dialog und Eingabefokus
Geteilter Client-Zustand
- Besitzer · mehrere Oberflächenbereiche
- Lebensdauer · innerhalb der Anwendungssitzung
- Beispiele · lokale Auswahl und noch nicht gespeicherter Arbeitsstand
Server-Zustand
- Besitzer · Backend oder Fachsystem
- Lebensdauer · unabhängig vom Browser
- Beispiele · Datensätze, Berechtigungen und freigegebene Revisionen
Die Matrix empfiehlt keine Bibliothek. Sie verhindert zunächst, dass Serverdaten wie lokaler Zustand oder kurzlebige UI-Details wie globale Fachobjekte behandelt werden.
Die Gegenprobe zuerst
In einem Projekt, das „unbedingt einen globalen Store“ brauchte, haben wir zuerst aufgeschrieben, was da eigentlich alles hinein sollte – und dann angefangen zu streichen. Ein überraschend großer Teil davon war in Wahrheit URL-Zustand: Filter, Seite, Auswahl oder Tab, sofern sie teilbar, navigierbar oder mit einem Lesezeichen speicherbar sein sollten. Kurzlebige Darstellungsdetails dürfen lokal bleiben. Ein Store, der navigierbare Werte zusätzlich hält, baut dagegen leicht eine zweite Wahrheit neben der Adresse auf.
Der nächste große Block ist noch tückischer, weil er wie Zustand aussieht und sich wie Zustand anfühlt: geladene Daten. Die Liste der Rechnungen, das Nutzerprofil, das Suchergebnis. Das sind Cache-Kopien von etwas, dessen Wahrheit dem Backend gehört. Dafür braucht es Server-Cache-Semantik wie Schlüssel, Frische, Invalidierung und Fehlerzustände – ob sie eine spezialisierte Bibliothek oder etwa RTK Query innerhalb des Redux-Stores bereitstellt. Problematisch ist nicht der physische Speicherort, sondern ein handverwalteter Slice, der diese Semantik nachbaut.
Nimmt man diese beiden Blöcke heraus – navigierbaren Zustand in die Adresse, Serverdaten in einen Cache –, bleibt oft wenig übrig. Der echte app-weite Client-Zustand kann ein kleiner Kern sein: Theme, Sprache, ein lokaler Entwurf oder ein Warenkorb, sofern dessen Wahrheit tatsächlich im Client liegt. Identität und Rechte stammen dagegen meist vom Server; lokal liegt nur ihre Cache-Kopie. Für den verbleibenden Kern lohnt sich ein Store, und weil dieser Kern klein ist, darf auch das Werkzeug klein sein.
flowchart TB
Start["Ein Wert, der wie<br/>globaler Zustand aussieht"]
Start --> Global{"Wirklich app-weit,<br/>oder nur lokal gebraucht?"}
Global -->|nur eine Ecke| Lokal["useState / useReducer<br/>in der Komponente"]
Global -->|app-weit| URL{"Gehört er in die URL?<br/>Filter, Seite, Auswahl"}
URL -->|ja| Adresse["Router / URL<br/>teilbar, überlebt Reload"]
URL -->|nein| Server{"Gehört die Wahrheit<br/>dem Backend?"}
Server -->|ja| Cache["Server-Cache<br/>TanStack Query / SWR"]
Server -->|nein| Store["Echter Kern: erst<br/>jetzt Store-Wahl"]
Auf die Reihenfolge kommt alles an. Wer mit der Werkzeugfrage anfängt, füllt den Store mit Dingen, die dort nichts zu suchen haben, und wundert sich später über Synchronisationsbugs. Wer mit der Gegenprobe anfängt, kommt oft mit useState und der URL durch und braucht gar keinen Store. Erst wenn nach dem Streichen ein Kern übrig bleibt, lohnt der Rest dieses Beitrags.
Drei Familien für den echten Kern
Angenommen, dieser Kern ist da. Dann gibt es grob drei Denkschulen, wie man app-weiten Zustand hält. Ich zeige jede an einem winzigen Beispiel und sage dazu ehrlich, was sie kostet – denn keine ist umsonst.
Redux Toolkit: das klassische Modell, entschlackt
Redux hat ein klares Modell: Es gibt genau einen Store, einen zentralen Zustandsbaum. Man ändert ihn nie direkt, sondern schickt Actions – beschreibende Nachrichten wie „increment“ –, und reine Funktionen namens Reducer berechnen aus altem Zustand plus Action den neuen. Dieses Modell hat Redux jahrelang den Ruf eingebracht, umständlich zu sein: viel Zeremonie, Action-Typen als Konstanten, Boilerplate für jeden Handgriff.
Redux Toolkit – kurz RTK, heute die offiziell empfohlene Art, Redux zu schreiben – nimmt genau diese Zeremonie ab. Ein createSlice bündelt Zustand, Actions und Reducer in einem Stück. Bemerkenswert ist der Reducer-Code: Er sieht aus, als würde man den Zustand direkt mutieren, bleibt aber intern immutabel, weil RTK die Bibliothek Immer benutzt, die aus den scheinbaren Mutationen eine saubere neue Kopie macht.
import { configureStore, createSlice } from "@reduxjs/toolkit";
const cartSlice = createSlice({
name: "cart",
initialState: { items: [] },
reducers: {
addItem(state, action) {
// looks mutable, stays immutable via Immer
state.items.push(action.payload);
},
clear(state) {
state.items = [];
},
},
});
export const { addItem, clear } = cartSlice.actions;
export const store = configureStore({ reducer: { cart: cartSlice.reducer } });
Das configureStore setzt den Store mit sinnvollen Voreinstellungen auf – die DevTools sind schon angebunden, gängige Middleware ist dabei. In TypeScript legt man sich zusätzlich einmal typsichere Hooks an (useAppSelector, useAppDispatch), sodass Zustand und Actions durchgehend typisiert sind.
Die Stärke von RTK liegt in der Nachvollziehbarkeit. Jede Zustandsänderung ist eine benannte Action, die durch einen Reducer läuft – ein protokollierbarer, reproduzierbarer Übergang. Die DevTools zeigen jede Action in der Reihenfolge, erlauben Time-Travel durch die Zustandshistorie und machen aus „warum steht da plötzlich dieser Wert?“ eine Frage, die man beantworten kann, statt sie zu erraten. In großen Teams und bei komplexen Abläufen ist das Gold wert: Der Zustandsfluss ist eine Konvention, die alle teilen, und keine Frage des persönlichen Stils. Der Preis ist ebenso ehrlich zu nennen: mehr Struktur, mehr Dateien, ein zentraler Baum, durch den alles läuft. Für den kleinen Kern einer mittelgroßen Anwendung ist das oft mehr Apparat, als der Kern verdient.
Leichte Stores: Zustand und Jotai
Die zweite Familie setzt genau dort an: minimale API, kein Provider-Zwang, so wenig Code wie möglich. Zwei Vertreter aus demselben Umfeld, die aber unterschiedlich denken.
Zustand (ja, die Bibliothek heißt so – ein deutsches Wort als Name) gibt einem eine Funktion create, die einen Hook zurückliefert. Aus diesem Hook liest man per Selektor genau die Scheibe des Zustands, die eine Komponente braucht. Der Selektor ist der Clou: Eine Komponente, die nur count liest, rendert nicht neu, wenn sich ein anderes Feld im Store ändert. Die Re-Renders bleiben granular, ohne dass man Context-Provider verschachteln muss.
import { create } from "zustand";
const useCartStore = create((set) => ({
items: [],
addItem: (item) => set((state) => ({ items: [...state.items, item] })),
clear: () => set({ items: [] }),
}));
// in a component: read only what you need
const count = useCartStore((state) => state.items.length);
Jotai geht von der anderen Seite heran. Statt eines Stores, aus dem man Scheiben liest, gibt es viele kleine, atomare Zustandseinheiten – Atoms –, die man frei kombiniert. Ein Atom hält einen Wert, abgeleitete Atoms rechnen aus anderen Atoms. Eine Komponente abonniert genau die Atoms, die sie liest, und mehr nicht.
import { atom, useAtom } from "jotai";
const itemsAtom = atom([]);
const countAtom = atom((get) => get(itemsAtom).length);
// in a component
const [count] = useAtom(countAtom);
Die Stärke beider ist offensichtlich: sehr wenig Code, feingranulare Updates, ein kleines Bundle. Man ist in Minuten produktiv, es gibt kaum Konzepte zu lernen. Der Preis ist die Kehrseite derselben Medaille – weniger Konvention, weniger vorgegebene Struktur. Redux zwingt einen in ein Muster; ein leichter Store lässt einem alle Freiheiten, auch die, sich ein Chaos zu bauen. Die Disziplin, den Store klein und geordnet zu halten und nicht doch wieder Serverdaten hineinzukippen, liegt beim Team. Für den kleinen Kern, um den es hier geht, ist das oft genau die richtige Größe.
MobX: der reaktive Ansatz
Die dritte Familie denkt grundsätzlich anders. Redux und die leichten Stores sind im Kern immutabel: Zustand ändern heißt, einen neuen Zustand herstellen. MobX dreht das um. Man beschreibt beobachtbaren Zustand, mutiert ihn ganz direkt, und die Bibliothek verfolgt selbständig, wer was liest, und aktualisiert genau die Stellen, die es angeht.
Man macht ein Objekt mit makeAutoObservable beobachtbar. Abgeleitete Werte deklariert man als computed – hier als Getter –, und sie aktualisieren sich von allein, sobald sich ihre Grundlage ändert. Komponenten, die man in observer einwickelt, reagieren gezielt auf genau die beobachteten Werte, die sie im Rendern gelesen haben.
import { makeAutoObservable } from "mobx";
import { observer } from "mobx-react-lite";
class CartStore {
items = [];
constructor() {
makeAutoObservable(this);
}
addItem(item) {
this.items.push(item); // direct mutation is fine here
}
get count() {
return this.items.length; // derived, recomputes automatically
}
}
const cart = new CartStore();
const CartBadge = observer(() => <span>{cart.count}</span>);
MobX glänzt, wenn viel Zustand voneinander abgeleitet ist. Wo man in Redux Selektoren schreiben und memoisieren müsste, ergibt sich der abgeleitete Wert bei MobX von selbst und wird nur dann neu berechnet, wenn seine Eingaben sich ändern. Es bleibt auffällig wenig Code übrig, der nur Werte miteinander verbindet. Der Preis ist die Implizitheit: Genau weil man nichts explizit dispatcht, ist die Frage „wo kommt diese Änderung her?“ schwerer zu beantworten – die Aktualisierung passiert im Hintergrund, ausgelöst durch eine Mutation, die irgendwo im Code steht. Und es ist ein anderes mentales Modell als der Rest des React-Ökosystems: gezielte Mutation statt Immutabilität. Wer aus der Redux-Welt kommt, muss umdenken; wer objektorientiert denkt, fühlt sich sofort zu Hause.
Woran man die Wahl festmacht
Es gibt keine Rangliste, die für alle stimmt – die Wahl hängt an ein paar Eigenschaften des Projekts. Je mehr Leute an demselben Zustand arbeiten, desto mehr kann eine feste Konvention helfen. Für protokollierbare Übergänge und das Zurückspulen der Zustandshistorie sind die Redux-DevTools eine starke, aber nicht exklusive Lösung. Bei viel abgeleitetem Zustand spielt das reaktive Modell von MobX seine Stärke aus. Dazu kommen Bundle-Budget, vorhandene Tooling-Anforderungen und die Vertrautheit des Teams; harte Größenurteile sollte man am konkreten Build messen, nicht aus einer Tabelle übernehmen.
Daraus lassen sich ein paar Faustregeln ableiten, ohne dass sie Gesetze wären. Ist der Kern wirklich klein – eine angemeldete Person, ein Theme, ein Warenkorb –, reichen Context mit useReducer oder ein leichter Store wie Zustand, und mehr wäre überzogen. Steht ein großes Team vor komplexen Abläufen mit dem Wunsch nach auditierbaren Übergängen, ist Redux Toolkit die naheliegende Wahl, weil es genau diese Nachvollziehbarkeit zur Konvention macht. Lebt die Anwendung von viel abgeleitetem Zustand, der sich kaskadierend aktualisieren soll, ist MobX in seinem Element. Und wenn zwei Optionen gleich gut passen, gibt die Vertrautheit des Teams den Ausschlag.
Die Wahl ist nachrangig
Kein Werkzeug ist „das beste“. Diese Vergleiche werden mit einer Ernsthaftigkeit geführt, die der Sache nicht angemessen ist – als hinge der Projekterfolg an Redux gegen MobX. Tut er nicht. Die Wahl des Stores ist nachrangig gegenüber der Besitzfrage, die davor kommt. Wer die URL für das nutzt, was in die URL gehört, und Serverdaten als Cache behandelt statt als Zustand, hält den echten Client-Zustand so klein, dass fast jede der drei Familien reicht – und die Entscheidung dazwischen zur Geschmacksfrage schrumpft.
Der Fehler, den die Werkzeugdebatte verdeckt, ist der teurere: einen großen globalen Store zu bauen und ihn mit URL-Zustand und Serverdaten zu füllen, die dort nicht hingehören. Kein Redux Toolkit und kein MobX rettet einen vor den Bugs, die daraus entstehen. Umgekehrt kommt man mit sauber gezogenen Grenzen und dem simpelsten der leichten Stores erstaunlich weit. Deshalb steht die Gegenprobe am Anfang dieses Beitrags und nicht am Ende: Sie entscheidet mehr als die Tabelle mit den drei Spalten danach.
Weiterführende Quellen
- Repository introduction-react (Kapitel react-hooks, state-management): https://github.com/mikebild/introduction-react
- Redux Toolkit, offizielle Dokumentation: https://redux-toolkit.js.org/
- Zustand: https://github.com/pmndrs/zustand
- Jotai: https://jotai.org/
- MobX: https://mobx.js.org/
Wie fandest du diesen Beitrag?
Kommentare