Redux ist das Schwergewicht unter den State-Management-Lösungen. Ein zentraler Store, strikte Regeln, explizite Actions, pure Reducer. Ein System, das Disziplin erzwingt und Struktur garantiert. Aber auch ein System, das für viele moderne React-Anwendungen schlicht überdimensioniert ist.
Die Frage ist berechtigt: Brauchen wir Redux überhaupt noch? React
hat useState, useReducer,
useContext. Wir können globalen State mit Context
verwalten, komplexe Logik mit Reducern kapseln, asynchrone Operations
mit useEffect handhaben. Für die meisten Anwendungen reicht
das völlig aus.
Redux ist keine technische Notwendigkeit. Es ist eine architektonische Entscheidung. Ein Statement: “Wir wollen unseren State streng kontrolliert, zentral verwaltet, vollständig nachvollziehbar und unabhängig von der UI.” Das bringt Vorteile – aber auch Kosten.
Dieses Kapitel erklärt Redux ehrlich: Wann es brilliert, wann es übertrieben ist, und welche Alternativen existieren.
React 2015 hatte kein Context-API, keine Hooks, kein
useReducer. State-Management war schwierig. Prop Drilling
war unvermeidbar. Globaler State erforderte komplexe
Higher-Order-Components oder Render Props. Redux bot die einzige
praktikable Lösung für zentrales State Management.
React 2025 ist anders. Hooks haben das Spiel verändert. Wir können
State lokal verwalten (useState), komplexe Logik kapseln
(useReducer), Daten global teilen
(useContext), asynchrone Operations handhaben
(useEffect, Custom Hooks). Für kleine bis mittlere
Anwendungen ist das ausreichend.
// Globaler State ohne Redux: useContext + useReducer
const AuthContext = createContext<AuthState | null>(null);
function AuthProvider({ children }: { children: ReactNode }) {
const [state, dispatch] = useReducer(authReducer, initialState);
return (
<AuthContext.Provider value={{ state, dispatch }}>
{children}
</AuthContext.Provider>
);
}
function useAuth() {
const context = useContext(AuthContext);
if (!context) throw new Error('useAuth must be within AuthProvider');
return context;
}
// Verwendung
function LoginButton() {
const { state, dispatch } = useAuth();
const handleLogin = async () => {
const user = await loginAPI();
dispatch({ type: 'LOGIN_SUCCESS', payload: user });
};
return <button onClick={handleLogin}>Login</button>;
}
Das funktioniert. Kein Redux, keine Actions-Dateien, keine Store-Konfiguration. Nur React-Bordmittel.
Warum sollte man sich dann Redux antun?
Redux ist kein Allzweck-Tool. Es ist ein Spezialwerkzeug für spezifische Szenarien. Wenn diese Szenarien zutreffen, ist Redux überlegen. Wenn nicht, ist es Overhead.
1. Debugging auf Zeit-Reise-Niveau
Redux DevTools sind unschlagbar. Jede Action wird aufgezeichnet. Jeder State-Change ist nachvollziehbar. Sie können durch die Zeit navigieren – jeden State zu jedem Zeitpunkt inspizieren, Actions rückgängig machen, den kompletten Action-Log exportieren.
graph LR
A[Action: LOGIN_START] --> B[State: loading=true]
B --> C[Action: LOGIN_SUCCESS]
C --> D[State: user=Alice, loading=false]
D --> E[Action: FETCH_DATA]
E --> F[State: data=[...]]
style A fill:#e1f5ff
style C fill:#e1f5ff
style E fill:#e1f5ff
style D fill:#e1ffe1
Für komplexe Business-Anwendungen mit vielen gleichzeitigen States (Authentifizierung, UI-Modi, Daten-Cache, Error-States, Loading-States) ist das unverzichtbar. Sie können einen Bug reproduzieren, indem Sie einfach den Action-Log exportieren und wieder importieren. Deterministisch, jedes Mal.
Mit useContext + useReducer? Keine solche
Tooling-Unterstützung. Sie loggen manuell, hoffen auf gute Console-Logs,
debuggen blind.
2. Testbarkeit ohne UI
Redux trennt State-Logik vollständig von UI. Reducer sind pure
Functions: (state, action) => newState. Keine
React-Komponenten, keine Hooks, kein Rendering. Sie können Reducer
isoliert testen.
// Redux Reducer (ohne React)
const authReducer = (state: AuthState, action: AuthAction): AuthState => {
switch (action.type) {
case 'LOGIN_SUCCESS':
return { ...state, user: action.payload, isAuthenticated: true };
case 'LOGOUT':
return { ...state, user: null, isAuthenticated: false };
default:
return state;
}
};
// Test (ohne React-Komponente)
test('LOGIN_SUCCESS sets user and isAuthenticated', () => {
const state = { user: null, isAuthenticated: false };
const action = { type: 'LOGIN_SUCCESS', payload: { id: 1, name: 'Alice' } };
const newState = authReducer(state, action);
expect(newState.user).toEqual({ id: 1, name: 'Alice' });
expect(newState.isAuthenticated).toBe(true);
});
Reducer-Tests sind schnell, deterministisch, einfach. Keine Komponenten mounten, keine Hooks mocken, keine DOM-Interaktionen.
Mit useReducer in React-Komponenten? Sie können den
Reducer extrahieren und separat testen, aber der übliche Weg ist, die
gesamte Komponente zu testen. Langsamer, komplexer.
3. Struktur erzwingen in großen Teams
Redux zwingt zur Disziplin. State-Changes laufen über Actions. Actions laufen über Reducer. Es gibt einen klaren Datenfluss, dokumentiert durch Action-Typen.
// Redux erzwingt Struktur
type AuthAction =
| { type: 'LOGIN_START' }
| { type: 'LOGIN_SUCCESS'; payload: User }
| { type: 'LOGIN_FAILURE'; payload: string }
| { type: 'LOGOUT' };
// Jede State-Änderung ist explizit dokumentiert
In großen Teams mit vielen Entwicklern ist das wertvoll. Jeder folgt dem gleichen Muster. Code-Reviews sind einfacher (“Wo ist die Action für diesen State-Change?”). Neue Entwickler verstehen den Datenfluss schnell.
Mit useContext + useReducer? Sie
können die gleiche Struktur erzwingen, aber React zwingt Sie
nicht dazu. Jeder Entwickler kann seinen eigenen Stil wählen. In kleinen
Teams kein Problem. In großen Teams Chaos.
4. Persistenz und Hydration
Redux-State ist ein einfaches JavaScript-Objekt. Serialisierbar,
speicherbar, wiederherstellbar. Persistenz-Libraries wie
redux-persist machen es trivial, State in LocalStorage zu
speichern und beim nächsten Seitenaufruf wiederherzustellen.
// Redux Persist (konzeptuell)
const persistedState = localStorage.getItem('redux-state');
const store = createStore(rootReducer, JSON.parse(persistedState || '{}'));
store.subscribe(() => {
localStorage.setItem('redux-state', JSON.stringify(store.getState()));
});
Server-Side Rendering mit Redux? State auf dem Server generieren, serialisieren, zum Client senden, hydratisieren. Redux ist dafür designt.
Mit useContext + useReducer? Möglich, aber
manuell. Sie müssen Persistenz selbst implementieren, Serialisierung
handhaben, Hydration koordinieren.
5. Middleware für Querschnittslogik
Redux Middleware ist ein mächtiges Konzept. Logger, Analytics, API-Calls, Error-Tracking – alles als Middleware implementierbar, zentral, wiederverwendbar.
// Logging-Middleware
const logger: Middleware = store => next => action => {
console.log('Action:', action);
const result = next(action);
console.log('New State:', store.getState());
return result;
};
// Analytics-Middleware
const analytics: Middleware = store => next => action => {
trackEvent(action.type, action.payload);
return next(action);
};
Middleware läuft bei jeder Action, automatisch. Kein manuelles Logging in jeder Komponente.
Mit React-Hooks? Kein äquivalentes Konzept. Sie loggen manuell in Custom Hooks oder Higher-Order-Components.
Redux basiert auf drei Konzepten:
Store – Der zentrale Speicher für den gesamten Application State. Ein einziges JavaScript-Objekt. Immutabel – es wird nie direkt modifiziert.
{
auth: { user: { id: 1, name: 'Alice' }, isAuthenticated: true },
todos: { items: [...], filter: 'all' },
ui: { theme: 'dark', sidebarOpen: false }
}
Actions – Plain Objects, die beschreiben,
was passiert ist. Ein type (String) und optional
ein payload.
{ type: 'ADD_TODO', payload: { id: 1, text: 'Learn Redux' } }
{ type: 'TOGGLE_TODO', payload: 1 }
{ type: 'SET_FILTER', payload: 'completed' }
Reducers – Pure Functions, die den neuen State berechnen basierend auf dem alten State und einer Action.
function todosReducer(state: TodosState = initialState, action: TodoAction): TodosState {
switch (action.type) {
case 'ADD_TODO':
return { ...state, items: [...state.items, action.payload] };
case 'TOGGLE_TODO':
return {
...state,
items: state.items.map(todo =>
todo.id === action.payload ? { ...todo, completed: !todo.completed } : todo
)
};
default:
return state;
}
}
Der Datenfluss ist unidirektional:
Actions sind synchron. Für asynchrone Operationen (API-Calls) braucht
man Middleware – ursprünglich redux-thunk, heute Redux
Toolkit’s createAsyncThunk.
Klassisches Redux ist verbose. Für jede Feature braucht man: Action Types (Konstanten), Action Creators (Funktionen), Reducer (Switch-Statement), Store-Konfiguration. Das ist Boilerplate.
Redux Toolkit (RTK) eliminiert 70% davon.
// Klassisches Redux: ~40 Zeilen
const ADD_TODO = 'ADD_TODO';
const TOGGLE_TODO = 'TOGGLE_TODO';
const addTodo = (text: string) => ({ type: ADD_TODO, payload: { id: Date.now(), text } });
const toggleTodo = (id: number) => ({ type: TOGGLE_TODO, payload: id });
const todosReducer = (state = initialState, action) => {
switch (action.type) {
case ADD_TODO: return { ...state, items: [...state.items, action.payload] };
case TOGGLE_TODO: return { ... };
default: return state;
}
};
// Redux Toolkit: ~15 Zeilen
const todosSlice = createSlice({
name: 'todos',
initialState,
reducers: {
addTodo: (state, action: PayloadAction<{ id: number; text: string }>) => {
state.items.push(action.payload); // Immer.js macht es immutabel
},
toggleTodo: (state, action: PayloadAction<number>) => {
const todo = state.items.find(t => t.id === action.payload);
if (todo) todo.completed = !todo.completed;
}
}
});
RTK verwendet Immer.js – Sie können State direkt “mutieren”, Immer macht es unter der Haube immutabel. Kein Spread-Operator, keine manuellen Copies.
Für asynchrone Operations:
// createAsyncThunk generiert automatisch pending/fulfilled/rejected Actions
const fetchTodos = createAsyncThunk('todos/fetch', async () => {
const response = await fetch('/api/todos');
return response.json();
});
const todosSlice = createSlice({
name: 'todos',
initialState: { items: [], loading: false, error: null },
reducers: { /* ... */ },
extraReducers: builder => {
builder
.addCase(fetchTodos.pending, state => {
state.loading = true;
})
.addCase(fetchTodos.fulfilled, (state, action) => {
state.loading = false;
state.items = action.payload;
})
.addCase(fetchTodos.rejected, (state, action) => {
state.loading = false;
state.error = action.error.message;
});
}
});
Automatisches Loading-State-Management, Error-Handling, TypeScript-Support. Das ist modernes Redux.
Kleine Anwendungen
Wenn Ihre App 5-10 Komponenten hat, ist Redux Overhead.
useState + Props reichen.
Lokaler UI-State
Modal offen/geschlossen? Dropdown expanded? Tab aktiv? Das ist
lokaler UI-State. Gehört in useState, nicht in Redux.
// ❌ Übertrieben
dispatch({ type: 'MODAL_OPEN' });
// ✓ Angemessen
const [isOpen, setIsOpen] = useState(false);
Formulare
Formular-State (Input-Values, Validierung, Fehler) ist lokal. Redux
ist hier kontraproduktiv. Verwenden Sie Formular-Libraries (React Hook
Form, Formik) oder useState.
Wenn das Team React-Hooks bevorzugt
Redux hat eine Lernkurve. Wenn Ihr Team mit React-Hooks produktiver ist und die Anwendung nicht komplex genug ist, um Redux zu rechtfertigen, verwenden Sie Hooks.
Zustand – Minimalistischer Store mit Hooks-API. Kein Boilerplate, kein Middleware-Konzept, aber auch kein DevTools-Time-Travel.
import create from 'zustand';
const useStore = create(set => ({
count: 0,
increment: () => set(state => ({ count: state.count + 1 }))
}));
function Counter() {
const { count, increment } = useStore();
return <button onClick={increment}>{count}</button>;
}
Jotai – Atom-basiertes State Management. Feingranularer als Context, weniger Boilerplate als Redux.
Recoil – Von Facebook, ähnlich wie Jotai. Experimentell, aber mächtig.
TanStack Query (React Query) – Für Server-State. Caching, Background-Fetching, Stale-While-Revalidate. Ersetzt Redux für API-Daten komplett.
const { data, isLoading } = useQuery('todos', fetchTodos);
Für die meisten Apps: React Query für Server-State, Zustand oder Context für Client-State.
| Kriterium | React Hooks + Context | Redux (RTK) | Zustand | React Query |
|---|---|---|---|---|
| Lernkurve | Niedrig | Mittel | Niedrig | Niedrig |
| Boilerplate | Minimal | Niedrig (RTK) | Minimal | Minimal |
| DevTools | Basis | Exzellent | Gut | Gut |
| Testbarkeit | Gut | Exzellent | Gut | Gut |
| Persistenz | Manuell | Einfach (redux-persist) | Middleware | N/A |
| Team-Größe | Kleine Teams | Große Teams | Kleine-Mittlere | Alle |
| Server-State | Manuell | Möglich | Möglich | Optimal |
| Middleware | Nein | Ja | Ja (Plugins) | Nein |
Verwenden Sie Redux, wenn: - Ihre App groß ist (>50 Komponenten, mehrere Features) - Sie in einem großen Team arbeiten - Sie strikte Struktur wollen - Sie umfangreiches Debugging benötigen - Sie komplexe State-Maschinen haben (Undo/Redo, Workflows) - Sie State persistieren müssen
Verwenden Sie React Hooks, wenn: - Ihre App klein ist (<20 Komponenten) - Sie schnell prototypen wollen - Das Team React bevorzugt - State hauptsächlich lokal ist
Verwenden Sie Zustand, wenn: - Sie globalen State wollen ohne Redux-Komplexität - Middleware unwichtig ist - Das Team Hooks-Patterns bevorzugt
Verwenden Sie React Query, wenn: - Der meiste State von APIs kommt - Caching wichtig ist - Sie Background-Fetching wollen
Oft die beste Kombination: React Query für Server-State + Zustand/Context für Client-State.
Redux ist nicht tot. Aber es ist auch nicht für jede App nötig. Die
React-Hooks-Revolution hat State Management demokratisiert. Was früher
Redux erforderte, geht heute oft mit useContext +
useReducer.
Redux brilliert in großen, komplexen Anwendungen mit vielen Entwicklern, strengen Anforderungen an Testbarkeit und Debugging, und langfristiger Wartbarkeit. Für diese Szenarien ist Redux nach wie vor die beste Lösung.
Für kleinere bis mittlere Apps? React Hooks + React Query + vielleicht Zustand. Weniger Overhead, schnellere Entwicklung, ausreichend für die meisten Anwendungsfälle.
Die Frage ist nicht “Redux oder nicht?”, sondern “Rechtfertigt die Komplexität meiner App Redux’s Overhead?” Wenn die Antwort “Nein” ist, verwenden Sie etwas Einfacheres. Wenn “Ja”, ist Redux immer noch konkurrenzlos.
Redux ist keine technische Notwendigkeit. Es ist eine architektonische Entscheidung. Treffen Sie sie bewusst.