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.
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.
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.
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.
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.
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.
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 });
}
};
};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.
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 }
});
}
};
};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;
}
};
};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.
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.