André Santos
Alexandra Mendes

11. August 2026

Min Read

Umgang mit asynchronen Operationen in Redux

MacBook Pro mit Code und Terminalausgabe zur Handhabung asynchroner Operationen mit Redux

Asynchrone Logik hat in einem Redux-Reducer nichts zu suchen. Reducer sind reine Funktionen; daher müssen sämtliche API-Aufrufe, Timer und Abonnements an anderer Stelle untergebracht werden, und in Redux ist diese Stelle die Middleware. Redux Thunk ist der Standard-Einstiegspunkt. Für die meisten Teams ist es zugleich auch der Endpunkt.

Die folgenden Ausführungen basieren auf einer umfangreichen React- und Node.js-Anwendung, die wir von einer Ad-hoc-State-Schicht migriert haben. Die erste Hälfte behandelt die Entscheidungsfindung und die damit verbundenen Kosten; ab dem Abschnitt „So implementieren Sie Redux Thunk“ geht es um die praktische Umsetzung. Wenn Sie nur die Entscheidungsgrundlage benötigen, können Sie nach den Auswahlkriterien aufhören.

blue arrow to the left
Imaginary Cloud logo

Warum verwenden wir Redux?

Als wir den Tech-Stack für dieses Projekt festlegten, war React aufgrund der umfangreichen visuellen Darstellung und 3D-Modellierung die offensichtliche Wahl für das Frontend.

Was wir jedoch nicht festlegten, war eine Strategie für das State Management. Mit der Weiterentwicklung des Projekts wurde dies zum Problem, weshalb das Team die gesamte Logik zur Zustandsverwaltung in eine Klasse auslagerte, die als Singleton implementiert wurde: eine einzige, gemeinsam genutzte Instanz, auf die jede Komponente lesend und schreibend zugreift. Die Speicherung zustandsbezogener Daten und Ereignisse funktionierte damit zunächst ganz gut.

Zumindest eine Zeit lang. Irgendwann stieß die Lösung an ihre Grenzen und wir machten uns auf die Suche nach einer besseren Alternative. Diese fanden wir in Redux, unterstützt durch die Einführung von Redux Toolkit, früher bekannt als Redux Starter Kit.

blue arrow to the left
Imaginary Cloud logo

Was uns die Ad-hoc-State-Layer tatsächlich gekostet hat

Das Singleton war nie der Plan. Niemand entscheidet sich bewusst dafür. Das ist der entscheidende Punkt dieses Artikels, denn unkontrollierter State ist selten primär ein technisches Problem. Er zeigt sich in den Bereitstellungskosten.

Stellen Sie sich ein Whiteboard in einem Gemeinschaftsbüro vor, auf das jeder schreiben kann, ohne Namen und ohne Zeitstempel. Das Board zeigt Ihnen zwar immer den aktuellen Stand der Dinge, aber nie, wer ihn wann oder warum geändert hat. Um also herauszufinden, wie ein Wert dorthin gelangt ist, müssen Sie jeden fragen, der daran vorbeigegangen ist.

Whiteboard-Architekturdiagramm zum Vergleich von Singleton-Muster und State Flow für asynchrone Operationen mit Redux.

Das war unser Singleton, und die Kosten schlugen sich an drei messbaren Stellen nieder. Das Debugging dauerte länger, da das Nachverfolgen eines fehlerhaften Wertes bedeutete, jeden Schreiber manuell zu überprüfen. [Autor zur Bestätigung: Wie viele Entwicklerstunden pro Sprint hat das Team vor der Migration für diese Nachverfolgung aufgewendet?] Das Onboarding dauerte länger, da die gemeinsam genutzte Instanz keinen Vertrag hatte, den ein neuer Entwickler hätte lesen können. Der einzige Weg, das State-Modell zu verstehen, bestand also darin, jede Komponente zu lesen, die darauf zugriff. [Autor zur Bestätigung: Wie lange brauchte ein neuer Entwickler, um auf dem State-Layer produktiv zu werden, vorher im Vergleich zu nachher?] Und jedes neue Feature war mit einer kleinen Zusatzbelastung verbunden, da jemand manuell herausfinden musste, welche Teile des gemeinsamen Objekts sicher verändert werden konnten.

Redux hat die Anwendung nicht schneller gemacht. Es hat den State-Layer lesbar gemacht. Aktionen sind benannt, Änderungen werden aufgezeichnet und die Historie ist während des Testens einsehbar, sodass die Zeit, die wir mit der Frage „Woher kommt dieser Wert?“ verloren haben, weitgehend weggefallen ist. Wenn Sie eine solche Migration in Erwägung ziehen, ist das die Rendite, die Sie messen sollten: nicht die Laufzeit-Performance, sondern die Stunden, die Ihr Team derzeit damit verbringt, den State manuell zu rekonstruieren. Es ist dieselbe Rendite, die wir erwarten, wenn wir ein Code-Audit an einer bestehenden Codebasis durchführen.

blue arrow to the left
Imaginary Cloud logo

Was ist ein Redux-Store?

Redux ist ein State-Container für JavaScript-Anwendungen, mit dem Sie den unidirektionalen Datenfluss von React umgehen können. Er fungiert als zentrale Datenquelle, auf die Sie überall in Ihrer Anwendung zugreifen können, ohne den State mühsam als Prop an andere Komponenten weiterreichen zu müssen.

Zudem ermöglicht er es, diesen State über vordefinierte Aktionen zu ändern, während gleichzeitig eine Historie dieser Aktionen und Änderungen gespeichert wird, die beim Testen eingesehen werden kann. Benannte Autoren, mit Zeitstempel versehen. Endlich ein Whiteboard mit einem dazugehörigen Logbuch.

Wer hat Redux entwickelt?

Als Facebook React der Welt vorstellte, war bereits klar, dass Props ab einem gewissen Komplexitätsgrad zu einer großen Hürde werden konnten. Um dem entgegenzuwirken, führten sie zusammen mit React ein Konzept namens Flux ein, das beschreibt, wie ein Store in Verbindung mit React funktionieren sollte. Das Redux, wie wir es heute kennen, entstand aus einem Proof of Concept, den Dan Abramov während der Arbeit mit Flux-Prinzipien für die React Europe entwickelte.

Wo wird Redux eingesetzt?

Redux wird in umfangreichen Anwendungen eingesetzt, bei denen die Interaktion mit einer Komponente Änderungen auf der gesamten Seite auslösen kann. Anstatt Callbacks auf der obersten Ebene der Anwendung zu erstellen und diese durch die Hierarchie zu schleusen, greifen Sie einfach auf den Store zu.

In meinem Projekt war es sinnvoll, da die Anwendung einen Punkt erreicht hatte, an dem zustandsbezogene Props durch mehrere Komponentenebenen gereicht wurden. Das machte den Code schwer lesbar und noch schwerer zu debuggen. (Dasselbe gilt für mobile Anwendungen. Wenn Sie Redux in React Native einbinden möchten, führen wir Sie in unserem React Native mit Redux-Guidedurch das Setup.)

blue arrow to the left
Imaginary Cloud logo

Asynchrone Aktionen mit Redux ausführen

Redux hat sofort einen Unterschied gemacht, als wir begannen, alten Code darauf zu migrieren. Der Code wurde leichter nachvollziehbar, das Team hat sich schnell eingearbeitet und die Anzahl der Props, die in der Anwendung herumschwirrten, ist drastisch gesunken.

Aber es war nicht perfekt. Die Natur von Reducern stellt ein Problem dar, sobald man versucht, das Abrufen von Informationen in ihnen abzubilden – ein Thema, mit dem sich die Redux-Community schon sehr lange beschäftigt.

Wie man mit asynchronen Aktionen in Redux umgeht

Reducer sind theoretisch reine Funktionen, laut der Dokumentation selbst.

Bei gleichen Argumenten sollten sie den nächsten Status berechnen und zurückgeben. Keine Überraschungen. Keine Seiteneffekte. Keine API-Aufrufe. Keine Mutationen. Nur eine Berechnung.

Wo sollte man also asynchrone Aufrufe in Redux unterbringen?

Die Aktionen wären die naheliegende Antwort, aber die grundlegende Implementierung einer Aktion ist nichts weiter als ein einfaches JavaScript-Objekt, mit dem man Informationen an den Store übergibt. Also hat sich die Community Middlewares ausgedacht: Funktionen, die zwischen dem Auslösen einer Aktion und dem Empfang durch den Reducer sitzen – wie eine Poststelle zwischen Briefkasten und Aktenschrank. Die Post kommt an, jemand öffnet sie, kümmert sich um das Notwendige, und erst dann wird etwas abgelegt. Sie verpacken die Logik in Funktionen und ahmen das natürliche Verhalten des Stores nach.

Welche asynchrone Redux-Middleware solltest du wählen?

Wie bei allem in der Programmierung gibt es keine Einheitslösung. Recherchiere also, welche Middleware am besten zu deinem Problem passt. Die erste Lösung, die in der Dokumentation vorgeschlagen wird, ist Redux Thunk. Ein Thunk ist eine Funktion, die eine weitere Funktion zurückgibt und die Arbeit so lange aufschiebt, bis sie aufgerufen wird – in Redux geschieht dies durch dispatch. Diese Middleware ermöglicht es dir, Aktionen zu erstellen, die mehr als nur einfache Objekte sind: Sie können andere Aktionen auslösen, andere Thunks aufrufen und asynchrone Operationen innerhalb ihrer selbst ausführen.

Andere haben seitdem an Bedeutung gewonnen. Redux-Saga modelliert asynchrone Abläufe als Sagas, also Generator-Funktionen: Funktionen, die bei jedem Schritt pausieren und die Kontrolle an eine Engine zurückgeben, die entscheidet, was als Nächstes passiert. Redux-Observable modelliert dieselben Abläufe als RxJS-Streams, bei denen jede Aktion ein Ereignis in einem Stream ist, das du mit den Operatoren der RxJS-Bibliothek filtern, kombinieren und abbrechen kannst. Unterschiedliche Anwendungsfälle und ein wichtiger Hinweis, bevor du dich entscheidest: Saga wird noch aktiv mit einer großen Community gepflegt, während Redux-Observable zwar stabil ist, sich aber mittlerweile im Wartungsmodus befindet. Wäge das also bei einer langlebigen Codebasis ab.

Eine vierte Option steht eher ergänzend als in direkter Konkurrenz dazu. RTK Query ist die Datenabrufschicht von Redux Toolkit: Sie definieren Ihre API-Endpunkte und das Tool generiert automatisch die Logik für Abruf, Caching und Laden, sodass Sie keine asynchronen Aktionen mehr manuell schreiben müssen.

Banner „Do a UX Audit“ mit blauem Smartphone, geschichteten App-Design-Fenstern und einem Button „Talk to Us“.
blue arrow to the left
Imaginary Cloud logo

Auswahlkriterien für die Middleware von Imaginary Cloud

„Recherchieren Sie“ ist kein guter Rat. Bei Kundenprojekten wählen wir zwischen vier Optionen anhand von drei Kriterien, und zwar in dieser Reihenfolge.

  1. Wie komplex ist der asynchrone Ablauf selbst? Ein Request, der startet, erfolgreich ist oder fehlschlägt, ist ein Thunk. Ein Ablauf, der abgebrochen, debounced – also bei einer Häufung von Aufrufen auf einen einzigen reduziert –, nach einem Zeitplan wiederholt oder mit anderen laufenden Requests koordiniert werden muss, ist der Punkt, an dem Saga und Observable ihren Preis wert werden.
  2. Handelt es sich um Server-State oder Client-State? Das meiste, was Teams in einem Redux-Store ablegen, ist ein Cache für Daten, die auf dem Server liegen. Wenn Sie es damit zu tun haben, ist RTK Query das richtige Werkzeug, da es den Code für das Abrufen der Daten komplett überflüssig macht. Thunks sind für Statusdaten gedacht, die tatsächlich vom Client verwaltet werden.
  3. Was kann das Team in einem Jahr noch warten? Sagas und Observables führen beide ein zusätzliches Programmiermodell ein – Generatoren oder reaktive Streams –, das jeder zukünftige Entwickler erst erlernen muss. In einem Team, das RxJS noch nicht beherrscht, fallen diese Kosten bei jeder Neueinstellung an, nicht nur einmalig zu Beginn.

In der Praxis läuft es bei den meisten unserer Projekte auf Thunk oder RTK Query hinaus, und beide lassen sich problemlos im selben Store kombinieren. Wenn die Antwort auf das erste Kriterium tatsächlich eine hohe Komplexität erfordert, führen Sie dieses Gespräch, bevor Sie den Code schreiben, nicht danach.

Was diese Entscheidung in Bezug auf die Bereitstellung kostet

Für alle, die die Arbeit abnehmen, statt sie selbst zu schreiben, lassen sich diese drei Kriterien auf einen Plan herunterbrechen. Die Einführung von Thunk in eine bestehende Redux-Codebasis ist eine Sache von wenigen Tagen, da es sich um gewöhnliches asynchrones JavaScript handelt, das Ihr Team ohnehin schreibt. Die Einführung von Saga oder Observable dauert Wochen, und die Kosten sind wiederkehrend: Jeder Entwickler, den Sie für diese Codebasis einstellen, muss Generatoren oder RxJS beherrschen, bevor er sicher an einem Datenfluss arbeiten kann, was das Onboarding für die gesamte Projektlaufzeit verlängert.

Die Entscheidung rückgängig zu machen, ist der teure Teil. Nach einem Jahr ist die als Sagas geschriebene asynchrone Logik über die gesamte Codebasis verteilt, und sie wieder herauszulösen ist eher eine Migration als ein Refactoring – etwa in der Größenordnung des oben beschriebenen Wechsels von Singleton zu Redux.

Diese Asymmetrie ist das Hauptargument dafür, mit der einfachsten Option zu beginnen, die die Anforderungen erfüllt. Ein späterer Umstieg von Thunk ist additiv, da beide Ansätze nebeneinander im selben Store existieren können. Ein Umstieg zurück ist das nicht.

blue arrow to the left
Imaginary Cloud logo

Warum Redux Thunk?

Von allen gängigen Lösungen für dieses Problem ist Redux Thunk am leichtesten zu verstehen. Es ist technisch gesehen recht zugänglich und zum Zeitpunkt der Erstellung dieses Artikels ist es der Ansatz, den die Redux-Dokumentation empfiehlt, wenn es um das manuelle Abrufen von Daten geht.

blue arrow to the left
Imaginary Cloud logo

Implementierung von Redux Thunk

Die folgende Anleitung verwendet eine neue App und eine öffentliche Hunde-API, damit der Code kurz genug bleibt, um ihn in einem Durchgang zu lesen. Wichtiger als die einzelnen Schritte sind jedoch die zwei grundlegenden Entscheidungen, die dahinterstecken: Was gehört in den Store, was bleibt in der Komponente und warum gibt es überhaupt die ausführliche Version?

Wir beginnen hier:

npx create-react-app doggos --template redux

Damit erhalten Sie eine neue App mit React und allen Redux-Modulen, die wir für dieses kurze Tutorial benötigen. Wir werden außerdem mit dem WoofBot-API-Dienst arbeiten.

Einrichten eines Redux Toolkit Slice für die API-Antwort

Zuerst die Entscheidung: Nur die Rassedaten und der Anfragestatus kommen in den Store, da mehr als ein Teil der Anwendung sie benötigt. Alles, was nur für eine einzelne Komponente gilt, während sie auf dem Bildschirm angezeigt wird – wie etwa die in einem Dropdown ausgewählte Rasse –, bleibt in dieser Komponente. Ignorieren Sie diesen Grundsatz, wird Ihr Store zur Müllhalde, was genau die Art von Debugging-Problem verursacht, die Redux eigentlich lösen soll.

Ein Slice ist ein Redux Toolkit-Paket, das einen Bereich des Stores zusammen mit den Reducern und Actions enthält, die ihn verändern. Dieser hier speichert alles, was mit Ihrer Hunde-API-Antwort zu tun hat.

// doggosSlice.js
import { createSlice } from '@reduxjs/toolkit';

const initialState = {
  breeds: [],
  images: {},
  loading: 'waiting',
};

export const doggosSlice = createSlice({
  name: 'doggos',
  initialState,
  reducers: {
    uploadBreeds: (state, action) => {
      state.breeds = action.payload;
    },
    uploadBreedImage: (state, action) => {
      state.images[action.payload.breed] = action.payload.image;
    },
    loadingState: (state, action) => {
      state.loading = action.payload;
    },
  },
});

export const { uploadBreeds, uploadBreedImage, loadingState } = doggosSlice.actions;

export const selectBreeds = (state) => state.doggos.breeds;
export const selectBreedImage = (breed) => (state) => state.doggos.images[breed];
export const isLoading = (state) => state.doggos.loading === 'request';

export default doggosSlice.reducer;

Hier sind unsere Actions:

  • uploadBreeds: dient als Speicherort für alle Payload-Informationen zu den Hunderassen.
  • uploadBreedImage: wird bei Bedarf zum Hochladen spezifischer Bilder für bestimmte Rassen verwendet.
  • loadingState: wird verwendet, um den Status der Anfrage zu aktualisieren.

Und unsere Selectors, also die Funktionen, die einen Wert aus dem Store lesen, damit Komponenten niemals direkt auf dessen Struktur zugreifen müssen:

  • selectBreeds: gibt ein Array aller Rassen im Store zurück.
  • selectBreedImage: gibt das Bild für eine bestimmte Rasse zurück.
  • isLoading: gibt den Status der Anfrage zurück.

Datenabruf in einem useEffect-Hook ohne Redux Thunk

Wie würdest du diesen Austausch zwischen der API und dem Store normalerweise implementieren? Ich würde das alles in einen useEffect-Hook packen, ähnlich wie diesen hier:

useEffect(() => {
  dispatch(loadingState('request'));

  fetch('https://dog.ceo/api/breeds/list/all')
    .then((response) => response.json())
    .then((data) => {
      dispatch(uploadBreeds(Object.keys(data.message)));
      dispatch(loadingState('waiting'));
    })
    .catch(() => {
      dispatch(loadingState('error'));
    });
}, [dispatch]);

Was passiert hier genau?

  1. Die Komponente wird eingebunden (mounted).
  2. Der Ladestatus wird durch einen Action-Dispatch auf „Request“ gesetzt.
  3. Die Daten werden über einen einfachen Fetch angefordert.
  4. Die Daten werden von der API empfangen und in das gewünschte Objekt umgewandelt.
  5. Die Rasseninformationen im Slice werden durch einen weiteren Dispatch mit den erhaltenen Daten aktualisiert.
  6. Der Ladestatus wird wieder auf „Waiting“ zurückgesetzt.

Alternativ könnten wir einen Fehler von der API erhalten, was den Ablauf bei Schritt 4 unterbricht und den Ladestatus auf „Error“ setzt.

Das funktioniert und ist in Ordnung. Es hat jedoch einige Nachteile. Hauptsächlich verlagert es zu viel Logik in die Komponente: Sie ist nicht wiederverwendbar, und wenn du diese Informationen an einer anderen Stelle benötigst, musst du immer sicherstellen, dass diese Komponente zuerst geladen wurde.

Verschieben desselben Fetch-Vorgangs in einen Redux Thunk

Die Komponentenlogik sieht dann so aus:

useEffect(() => {
  dispatch(fetchBreeds());
}, [dispatch]);

Wir müssen eine neue fetchBreeds-Action erstellen, die der Logik, die wir zuvor in der Komponente hatten, sehr ähnlich ist:

export const fetchBreeds = () => async (dispatch) => {
  dispatch(loadingState('request'));

  try {
    const response = await fetch('https://dog.ceo/api/breeds/list/all');
    const data = await response.json();

    dispatch(uploadBreeds(Object.keys(data.message)));
    dispatch(loadingState('waiting'));
  } catch (error) {
    dispatch(loadingState('error'));
  }
};

Diese einfache Änderung des Speicherorts behebt die meisten unserer Probleme. Wir haben Code aus der Komponente ausgelagert und diesen spezifischen Logikteil in der gesamten Codebasis wiederverwendbar gemacht. Die Informationen sind nicht mehr an das Einbinden der Komponente gebunden, sodass du eine fetchBreeds-Action von überall auslösen kannst und die Daten geladen werden.

export const fetchBreedImages = () => async (dispatch, getState) => {
  const breeds = selectBreeds(getState());

  for (const breed of breeds) {
    const response = await fetch(`https://dog.ceo/api/breed/${breed}/images/random`);
    const data = await response.json();

    dispatch(uploadBreedImage({ breed, image: data.message }));
  }
};

Dies ermöglicht es uns auch, Thunks zu verketten, indem wir einen aus einem anderen heraus aufrufen, wenn die Logik unserer Actions komplexer wird. Wir können über getState direkt auf den State zugreifen, anstatt Selektoren zu benötigen. Dennoch solltest du weiterhin Selektoren verwenden, damit Änderungen an der Struktur deines Redux-States deine Thunks nicht beschädigen.

Verwende createAsyncThunk anstelle eines manuell geschriebenen Thunks

Alles oben Genannte wurde zur Veranschaulichung ausführlich geschrieben. Es zeigt, was ein Thunk eigentlich ist: eine Funktion, die du dispatchest und der dispatch sowie getState übergeben werden. In einem Projekt, das Redux Toolkit verwendet, würdest du es jedoch nicht auf diese Weise schreiben. createAsyncThunk generiert die ausstehenden, erfüllten und abgelehnten Aktionen für Sie, sodass der Ladezustand, den wir gerade dreimal beim Auslösen beobachtet haben, vom Reducer zentral verarbeitet wird.

import { createAsyncThunk } from '@reduxjs/toolkit';

export const fetchBreeds = createAsyncThunk('doggos/fetchBreeds', async () => {
  const response = await fetch('https://dog.ceo/api/breeds/list/all');
  const data = await response.json();
  return Object.keys(data.message);
});

extraReducers: (builder) => {
  builder
    .addCase(fetchBreeds.pending, (state) => {
      state.loading = 'request';
    })
    .addCase(fetchBreeds.fulfilled, (state, action) => {
      state.breeds = action.payload;
      state.loading = 'waiting';
    })
    .addCase(fetchBreeds.rejected, (state, action) => {
      state.loading = 'error';
      state.error = action.error.message;
    });
}

Fehlerbehandlung und Abbruch in einem Redux Thunk

Zwei Dinge, die in Tutorials meist übersprungen werden. Genau die zwei Dinge, die in der Produktion zu Fehlern führen.

Fehler. Ein abgelehnter Thunk sollte eine Nachricht im Store hinterlegen, nicht nur einen Status. action.error.message oben ist das Minimum. In echten Projekten behalten wir das Fehler-Payload der fehlgeschlagenen Anfrage bei, indem wir rejectWithValueverwenden, damit die Komponente zwischen einem 404-Fehler und einem Netzwerkproblem unterscheiden und dem Benutzer eine hilfreiche Rückmeldung geben kann.

Abbruch. Die oben genannte fetchBreedImages-Schleife läuft weiter, nachdem die Komponente ausgehängt wurde, und eine langsame Antwort, die nach einer neueren eintrifft, würde diese überschreiben. createAsyncThunk stellt Ihnen ein Signal zur Verfügung, das Sie für das erste Problem an fetch übergeben können, sowie eine condition-Option, um den Start einer doppelten Anfrage für das zweite Problem zu verhindern. Benötigen Sie mehr? Abbruch ist genau das Kriterium, das für Saga oder Observable spricht.

So testen Sie einen Thunk und einen createAsyncThunk

Thunks sind gerade deshalb testbar, weil sie einfache Funktionen sind. Das ist der praktische Grund, sie einer Logik vorzuziehen, die in einer Komponente vergraben ist. Kein Rendern von Komponenten. Kein Simulieren von Hooks.

Für einen manuell geschriebenen Thunk rufen Sie ihn mit einem gefälschten dispatch und einem gefälschten getState auf und prüfen dann, was in welcher Reihenfolge ausgelöst wurde:

it('dispatches request, then breeds, then waiting', async () => {
  const dispatch = jest.fn();
  global.fetch = jest.fn().mockResolvedValue({
    json: () => Promise.resolve({ message: { husky: [], beagle: [] } }),
  });

  await fetchBreeds()(dispatch, () => ({}));

  expect(dispatch.mock.calls.map(([action]) => action)).toEqual([
    loadingState('request'),
    uploadBreeds(['husky', 'beagle']),
    loadingState('waiting'),
  ]);
});

Bei einem createAsyncThunk gibt es zwei getrennte Dinge zu testen, und es lohnt sich, diese getrennt zu halten. Der Thunk selbst wird getestet, indem er gegen einen echten Store ausgelöst wird und der resultierende Zustand überprüft wird, was den pending- und fulfilled-Pfad vollständig abdeckt. Der Reducer wird als reine Funktion getestet, indem er mit einer fetchBreeds.rejected-Aktion aufgerufen wird, um sicherzustellen, dass der Fehler dort landet, wo Sie ihn erwarten:

it('records the error message when the request is rejected', () => {
  const action = { type: fetchBreeds.rejected.type, error: { message: 'Network error' } };
  const state = doggosReducer(initialState, action);

  expect(state.loading).toBe('error');
  expect(state.error).toBe('Network error');
});

Der Pfad für abgelehnte Aktionen ist der, den Teams oft zu testen vergessen. Es ist jedoch genau der, den Benutzer zu sehen bekommen.

Wie Thunks und Selektoren interagieren, wenn der Store wächst

Ein Thunk, der getState aufruft, liest den gesamten Store – eine Kopplung, die man frühzeitig verwalten sollte. Wenn Sie über einen Selektor lesen, wie es fetchBreedImages tut, hängt der Thunk vom Vertrag des Selektors ab und nicht von der Struktur Ihres State-Trees. Wenn Sie den Slice später umstrukturieren, müssen Sie nur den Selektor anpassen, anstatt jeden Thunk zu durchsuchen, der darauf zugegriffen hat.

Das zweite Problem betrifft die Größe. Ein Selektor, der einen Wert ableitet, Rassen filtert oder ein Lookup erstellt, wird bei jeder Store-Änderung ausgeführt und gibt jedes Mal ein neues Objekt zurück. Das führt dazu, dass Komponenten neu rendern, selbst wenn sich die angezeigten Daten nicht geändert haben. Redux Toolkit bietet hierfür createSelector: Es memoisiert das Ergebnis, speichert also die letzte Ausgabe zwischen und berechnet sie nur dann neu, wenn sich die Eingabewerte tatsächlich geändert haben.

import { createSelector } from '@reduxjs/toolkit';

export const selectBreedsWithImages = createSelector(
  [selectBreeds, (state) => state.doggos.images],
  (breeds, images) => breeds.filter((breed) => images[breed]),
);

Einfache Selektoren für direkte Lesezugriffe, memoisiert für alles Abgeleitete. Bei einem kleinen Store ist der Unterschied nicht spürbar. Bei dem Store, den dieses Projekt am Ende hatte, ist es jedoch der Unterschied zwischen einer Seite, die sofort auf eine Aktion reagiert, und einer, die ruckelt.

Wenn RTK Query Redux Thunk vollständig ersetzt

Wenn der Status, den Sie abrufen, vom Server und nicht vom Client verwaltet wird, RTK Query entfernt den oben genannten Code vollständig. Sie definieren den Endpunkt, und der generierte Hook übernimmt die Anfrage, das Caching, die Lade-Flags sowie die Deduplizierung gleichzeitiger Anfragen – das bedeutet, dass zwei Komponenten, die gleichzeitig dieselben Daten anfordern, nur einen Netzwerkaufruf statt zwei auslösen. Zudem erfolgt die Invalidierung beim Schreiben automatisch: Ein erfolgreiches Update markiert die betroffenen zwischengespeicherten Daten als veraltet und lädt sie neu.

Keine Thunks, die geschrieben werden müssen. Kein Ladezustand, der manuell verwaltet werden muss. Die Redux-Dokumentation empfiehlt RTK Query mittlerweile als Standardansatz für das Datenabrufen, und da die Hunderassen in diesem Artikel Server-Status darstellen, wäre dies in einem echten Projekt unsere erste Wahl.

blue arrow to the left
Imaginary Cloud logo

Was sollte man also wählen, um asynchrone Vorgänge in Redux zu verarbeiten?

Hier ist das Argument in Kürze: Reducer müssen rein bleiben, daher gehört asynchrone Arbeit in die Middleware. Redux Thunk ist die richtige Standardwahl, da es kein neues Programmiermodell einführt und den typischen Fall von erfolgreichen oder fehlgeschlagenen Anfragen abdeckt, aus denen die meisten Anwendungen bestehen – und createAsyncThunk ist der Weg, wie man es schreiben sollte. Wenn die Daten von einem Server stammen, verwenden Sie stattdessen RTK Query und verzichten Sie ganz auf Thunks. Greifen Sie nur dann zu Saga oder Observable, wenn Ihre Abläufe Abbruch-, Koordinations- oder Planungsfunktionen erfordern, die mit den ersten beiden nicht umsetzbar sind, und nur dann, wenn Ihr Team dieses Modell auch in einem Jahr noch beherrschen kann.

Für die Probleme, mit denen ich zuletzt konfrontiert war, waren Thunks mehr als ausreichend, um jeden Grenzfall abzudecken. Fangen Sie damit an. Lassen Sie dann eine konkrete Anforderung – nicht eine bloße Vorliebe – den Grund dafür sein, warum Sie davon abweichen.

blue arrow to the left
Imaginary Cloud logo

Häufig gestellte Fragen

Kann man asynchrone Aufrufe in einem Redux-Reducer tätigen?

Nein. Reducer müssen reine Funktionen sein: Bei gleichen Argumenten geben sie immer den gleichen neuen Status zurück, ohne Seiteneffekte und ohne API-Aufrufe. Asynchrone Aufgaben gehören in Middleware, die zwischen dem Absenden einer Aktion und dem Empfang durch den Reducer liegt. Redux Thunk ist hierfür die Standard-Middleware.

Was ist der Unterschied zwischen Redux Thunk und Redux-Saga?

Ein Thunk ist eine einfache Funktion, die dispatch und getState empfängt – es ist also nur asynchrones JavaScript, das du bereits kennst. Eine Saga ist eine Generator-Funktion, und Redux-Saga ist eine Engine zum Ausführen dieser Generatoren. Dadurch erhältst du Funktionen wie Abbruch, Debouncing, Wiederholungsversuche und die Koordination zwischen gleichzeitigen Abläufen als erstklassige Features. Thunk ist einfacher. Saga ist leistungsfähiger für komplexe Abläufe, erfordert jedoch einen höheren Lern- und Wartungsaufwand.

Sollte ich createAsyncThunk verwenden oder meinen eigenen Thunk schreiben?

Verwende createAsyncThunk. Es generiert automatisch die Aktionen für pending, fulfilled und rejected, standardisiert die Fehlerstruktur und bietet Unterstützung für Abbrüche durch signal und condition. Schreibe einen Thunk nur dann manuell, wenn er gar keine Anfrage auslöst, zum Beispiel wenn er lediglich den Status liest und bedingt eine einfache Aktion auslöst.

Brauche ich 2026 noch Redux?

Seltener, als Teams annehmen. Wenn es sich bei dem verwalteten Status um einen Cache für Serverdaten handelt, decken RTK Query oder eine Datenabruf-Bibliothek dies ab. Wenn der Status lokal in einem kleinen Komponentenbaum liegt, reichen React Context oder der Komponenten-Status meist aus. Redux ist dann sinnvoll, wenn eine große Menge an clientseitigem Status über weit entfernte Teile einer großen Anwendung hinweg geteilt wird und du die Nachvollziehbarkeit durch benannte Aktionen sowie eine überprüfbare Historie benötigst.

Wie gehe ich mit Fehlern in einem Redux-Thunk um?

Fange den Fehler innerhalb des Thunks ab und sende ihn als Daten in den Store, nicht nur als Status-Flag. Verwende bei createAsyncThunk rejectWithValue(error), damit die rejected-Aktion die eigene Fehler-Payload der API enthält, und verarbeite dann .rejected in extraReducers. Auf diese Weise kann eine Komponente zwischen einem Validierungsfehler und einem Netzwerkfehler unterscheiden und dem Benutzer eine spezifische Meldung anzeigen.

Die Wahl einer State-Management-Strategie für Ihre Codebasis

Die meisten Probleme mit dem State, die wir beheben sollen, haben genauso angefangen wie unsere eigenen. Es wurde keine Entscheidung getroffen, und eine Ad-hoc-Lösung wurde schleichend zum Standard-State-Layer. Wenn Ihnen das bekannt vorkommt, können unsere Teams analysieren, was Sie Ihr State-Management tatsächlich an Zeit für Debugging und Einarbeitung kostet, bevor wir Empfehlungen aussprechen. Sprechen Sie mit uns darüber und erfahren Sie, wie wir bei der Umsetzung vorgehen auf unseren Software-Engineering- und Code-Audit- Seiten, oder stöbern Sie in unseren Fallstudien, einschließlich unserer React- und Redux-Arbeit für Elephants Don't Forget, um den Prozess in der Praxis zu sehen.

André Santos
André Santos

Ein ganz normaler Webentwickler, der sich gerne im Backend versteckt. Javascript und Ruby sind mein Ding. Ich tue mich noch schwer mit Docker und meine Builds gehen ziemlich oft kaputt.

Read more posts by this author
Alexandra Mendes
Alexandra Mendes

Alexandra Mendes ist Senior Growth Specialist bei Imaginary Cloud und verfügt über mehr als 3 Jahre Erfahrung in der Erstellung von Texten über Softwareentwicklung, KI und digitale Transformation. Nach Abschluss eines Frontend-Entwicklungskurses erwarb Alexandra einige praktische Programmierkenntnisse und arbeitet nun eng mit technischen Teams zusammen. Alexandra ist begeistert davon, wie neue Technologien Wirtschaft und Gesellschaft prägen. Sie liebt es, komplexe Themen in klare, hilfreiche Inhalte für Entscheidungsträger umzuwandeln.

LinkedIn

Read more posts by this author

People who read this post, also found these interesting:

Dropdown caret icon