26 Redux Thunk - Asynchrone Actions in Redux

Redux verarbeitet standardmäßig nur synchrone Actions. Für asynchrone Operationen wie API-Aufrufe benötigen wir eine Middleware. Redux Thunk ist die einfachste und am weitesten verbreitete Lösung für dieses Problem.

Redux Thunk ermöglicht es, Funktionen statt Action-Objekte zu dispatchen. Diese Funktionen, genannt “Thunks”, erhalten dispatch und getState als Parameter und können damit asynchrone Operationen durchführen.

26.1 Das Problem mit asynchronen Operationen in Redux

In purem Redux können Action Creators nur einfache Objekte zurückgeben. Ein typischer Action Creator sieht so aus:

const addUser = (user: User) => ({
  type: 'ADD_USER',
  payload: user
});

Diese Struktur funktioniert perfekt für synchrone Operationen. Was aber, wenn wir einen Benutzer von einer API laden müssen? Wo platzieren wir den API-Aufruf? Wie kommunizieren wir Loading-States? Wie behandeln wir Fehler?

Ohne Middleware müssten wir diese Logik außerhalb von Redux implementieren, was zu unübersichtlichem Code führt und die Vorteile des zentralisierten State Managements zunichtemacht. Die Komponenten würden direkt API-Aufrufe durchführen und dann Actions dispatchen - ein Ansatz, der schnell unübersichtlich wird und schwer zu testen ist.

26.2 Was ist ein Thunk?

Der Begriff “Thunk” stammt aus der funktionalen Programmierung und bezeichnet eine Funktion, die eine Berechnung kapselt, ohne sie sofort auszuführen. In der Redux-Welt ist ein Thunk eine Funktion, die von einem Action Creator zurückgegeben wird und Zugriff auf dispatch und getState erhält.

Ein Thunk Action Creator sieht folgendermaßen aus:

const fetchUsers = () => {
  return async (dispatch, getState) => {
    // Hier können wir beliebige asynchrone Logik ausführen
    dispatch({ type: 'FETCH_USERS_START' });
    
    try {
      const response = await fetch('/api/users');
      const users = await response.json();
      dispatch({ type: 'FETCH_USERS_SUCCESS', payload: users });
    } catch (error) {
      dispatch({ type: 'FETCH_USERS_ERROR', payload: error.message });
    }
  };
};

Die äußere Funktion ist der Action Creator, die innere Funktion ist der eigentliche Thunk. Diese Struktur ermöglicht es uns, Parameter an den Action Creator zu übergeben und diese im Thunk zu verwenden.

26.3 Wie funktioniert Redux Thunk Middleware?

Redux Thunk ist bemerkenswert einfach in seiner Implementierung. Die gesamte Middleware besteht im Kern aus nur wenigen Zeilen Code:

const thunkMiddleware = store => next => action => {
  if (typeof action === 'function') {
    return action(store.dispatch, store.getState);
  }
  return next(action);
};

Die Middleware prüft, ob die dispatchte Action eine Funktion ist. Falls ja, ruft sie diese Funktion mit dispatch und getState als Argumenten auf. Falls nicht, leitet sie die Action normal weiter. Diese Einfachheit macht Redux Thunk zu einer der beliebtesten Middleware-Lösungen.

26.4 Praktische Anwendungsmuster

26.4.1 Loading States und Fehlerbehandlung

Ein häufiges Muster bei asynchronen Operationen ist die Verwaltung von drei Zuständen: Loading, Success und Error. Mit Redux Thunk können wir diese elegant handhaben:

interface AsyncState<T> {
  data: T | null;
  loading: boolean;
  error: string | null;
}

const fetchData = (endpoint: string) => {
  return async (dispatch) => {
    dispatch({ type: 'FETCH_START' });
    
    try {
      const response = await fetch(endpoint);
      if (!response.ok) {
        throw new Error(`HTTP error! status: ${response.status}`);
      }
      const data = await response.json();
      dispatch({ type: 'FETCH_SUCCESS', payload: data });
    } catch (error) {
      dispatch({ 
        type: 'FETCH_ERROR', 
        payload: error instanceof Error ? error.message : 'Unbekannter Fehler' 
      });
    }
  };
};

Dieses Muster ermöglicht es der UI, auf jeden Zustand angemessen zu reagieren: einen Ladeindikator anzuzeigen, die Daten darzustellen oder eine Fehlermeldung zu präsentieren.

26.4.2 Bedingte Dispatches

Mit Zugriff auf getState können Thunks Entscheidungen basierend auf dem aktuellen State treffen:

const fetchUsersIfNeeded = () => {
  return (dispatch, getState) => {
    const state = getState();
    
    if (state.users.data && !state.users.expired) {
      // Daten sind bereits vorhanden und noch gültig
      return Promise.resolve();
    }
    
    return dispatch(fetchUsers());
  };
};

Diese Technik verhindert unnötige API-Aufrufe und implementiert effektives Caching auf Redux-Ebene.

26.4.3 Verkettung von Actions

Thunks können mehrere Actions in einer bestimmten Reihenfolge dispatchen:

const createUserAndRefreshList = (userData: UserData) => {
  return async (dispatch) => {
    dispatch({ type: 'CREATE_USER_START' });
    
    try {
      const response = await fetch('/api/users', {
        method: 'POST',
        body: JSON.stringify(userData),
        headers: { 'Content-Type': 'application/json' }
      });
      
      const newUser = await response.json();
      dispatch({ type: 'CREATE_USER_SUCCESS', payload: newUser });
      
      // Nach erfolgreichem Erstellen die Liste aktualisieren
      dispatch(fetchUsers());
      
      // Und eine Erfolgsmeldung anzeigen
      dispatch({ 
        type: 'SHOW_NOTIFICATION', 
        payload: { message: 'Benutzer erfolgreich erstellt', type: 'success' }
      });
      
      // Notification nach 3 Sekunden ausblenden
      setTimeout(() => {
        dispatch({ type: 'HIDE_NOTIFICATION' });
      }, 3000);
      
    } catch (error) {
      dispatch({ type: 'CREATE_USER_ERROR', payload: error.message });
    }
  };
};

26.5 TypeScript und Redux Thunk

Die Typisierung von Thunks in TypeScript erfordert besondere Aufmerksamkeit. Eine robuste Typdefinition sieht folgendermaßen aus:

import { Action, Dispatch } from 'redux';
import { ThunkAction as ReduxThunkAction } from 'redux-thunk';

// Basis-Typen
type AppState = {
  // Ihre State-Definition
};

type AppAction = 
  | { type: 'FETCH_START' }
  | { type: 'FETCH_SUCCESS'; payload: any }
  | { type: 'FETCH_ERROR'; payload: string };

// Thunk Action Type
type ThunkAction<ReturnType = void> = ReduxThunkAction<
  ReturnType,
  AppState,
  unknown,
  AppAction
>;

// Verwendung
const fetchData = (): ThunkAction<Promise<void>> => {
  return async (dispatch, getState) => {
    // Typsicherer Zugriff auf State und Dispatch
    const currentState = getState(); // Typ: AppState
    dispatch({ type: 'FETCH_START' }); // Nur gültige Actions erlaubt
  };
};

Diese Typisierung stellt sicher, dass nur gültige Actions dispatched werden können und der State-Zugriff typsicher ist.

26.6 Common Patterns

26.6.1 Fehlerbehandlung standardisieren

Erstellen Sie wiederverwendbare Thunk-Wrapper für konsistente Fehlerbehandlung:

const createAsyncThunk = <T>(
  actionType: string,
  asyncFunction: () => Promise<T>
): ThunkAction<Promise<void>> => {
  return async (dispatch) => {
    dispatch({ type: `${actionType}_START` });
    
    try {
      const result = await asyncFunction();
      dispatch({ type: `${actionType}_SUCCESS`, payload: result });
    } catch (error) {
      dispatch({ 
        type: `${actionType}_ERROR`, 
        payload: error instanceof Error ? error.message : 'Unbekannter Fehler' 
      });
      
      // Optional: Globale Fehlerbehandlung
      dispatch({ 
        type: 'GLOBAL_ERROR', 
        payload: { error, source: actionType } 
      });
    }
  };
};

26.6.2 Abbruchbare Requests

Für abbruchbare API-Aufrufe können Sie AbortController verwenden:

let currentRequest: AbortController | null = null;

const fetchSearchResults = (query: string): ThunkAction => {
  return async (dispatch) => {
    // Vorherige Anfrage abbrechen
    if (currentRequest) {
      currentRequest.abort();
    }
    
    currentRequest = new AbortController();
    
    try {
      dispatch({ type: 'SEARCH_START' });
      
      const response = await fetch(`/api/search?q=${query}`, {
        signal: currentRequest.signal
      });
      
      const results = await response.json();
      dispatch({ type: 'SEARCH_SUCCESS', payload: results });
      
    } catch (error) {
      if (error.name !== 'AbortError') {
        dispatch({ type: 'SEARCH_ERROR', payload: error.message });
      }
    } finally {
      currentRequest = null;
    }
  };
};

26.7 Häufige Fehlerquellen und deren Vermeidung

Ein häufiger Fehler ist das Vergessen des return Statements bei Thunks. Ohne Return kann die aufrufende Komponente nicht auf das Promise warten:

// Falsch
const fetchData = () => (dispatch) => {
  fetch('/api/data').then(data => dispatch(setData(data)));
};

// Richtig
const fetchData = () => (dispatch) => {
  return fetch('/api/data').then(data => dispatch(setData(data)));
};

Ein weiterer Fehler ist die direkte Mutation des States in Thunks. Thunks sollten niemals direkt auf den State zugreifen und ihn verändern, sondern immer über Actions arbeiten.

26.8 Performance-Überlegungen

Redux Thunk selbst hat minimalen Performance-Overhead. Die Performance-Herausforderungen entstehen meist durch die asynchronen Operationen selbst. Beachten Sie folgende Punkte:

Vermeiden Sie übermäßige API-Aufrufe durch intelligentes Caching und Debouncing. Nutzen Sie getState um zu prüfen, ob Daten bereits vorhanden sind. Implementieren Sie Request-Deduplication für gleichzeitige identische Anfragen.

Bei komplexen asynchronen Flows mit vielen abhängigen Operationen kann Redux Thunk an seine Grenzen stoßen. Für solche Fälle existieren mächtigere Alternativen wie Redux-Saga oder Redux-Observable, die auf Generators bzw. RxJS basieren.