29 Redux – State Management für Architektur-Puristen

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.

29.1 Die Skepsis: Warum Redux heute oft nicht nötig ist

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?

29.2 Wann Redux wirklich brilliert

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.

29.3 Das Redux-Modell: Store, Actions, Reducers

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.

29.4 Redux Toolkit: Die moderne Form

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.

29.5 Wann Redux NICHT verwenden

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.

29.6 Alternativen zu Redux

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.

29.7 Entscheidungshilfe: Redux vs. Hooks vs. Alternativen

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.

29.8 Ein ehrliches Fazit

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.