Redux-Workshop für Einsteiger

In diesem Workshop gebe ich eine schnelle und praktische Einführung in das State-Handling-System Redux. Hierfür wollen wir unser bestehendes React-Projekt aus dem React-Workshop für Einsteiger so umschreiben und erweitern, dass das State-Handling unserer Task-Listen-Applikation komplett vom Redux-System übernommen wird und dessen Vorteile in der Praxis sichtbar werden.

1. Warum Redux?

Redux-Logo

Redux stellt ein State-Handling-System dar, mit dessen Hilfe die gesamte Programmlogik verwaltet und somit vom Rest der Anwendung abgekapselt werden kann. Es handelt sich um ein alleinstehendes System, das auch ohne einen View-Renderer wie React betrieben werden kann.

Das Funktionsprinzip basiert auf einem zentralen und immutablen State, der den aktuellen Zustand unserer Applikation genau definiert. Jede Veränderung an unserem Programm wird durch eine ganz bestimmte Veränderung an diesem State repräsentiert, sodass unsere Anwendung durch Verwendung dieses Konzepts vorhersehbarer, strukturierter und leichter testbar gemacht wird.

Durch den Einsatz eines immutablen States ist es beispielsweise auch sehr einfach, Funktionalitäten wie „Undo“ oder „Redo“ zu realisieren, was sich andernfalls als relativ schwierig gestaltet.

2. Vorraussetzungen für den Workshop

Wie beim React-Workshop für Einsteiger werden weiterhin lediglich Grundkenntnisse in JavaScript und HTML benötigt. Da dieser Workshop aber auf dem vorhergehenden React-Einsteiger-Workshop aufbaut, werden dementsprechend die dort vermittelten Grundkenntnisse in React vorausgesetzt.

Der letzte Stand des React-Einsteiger-Workshops liegt auf GitHub. Zur Vereinfachung der Lesbarkeit und zur besseren Nachvollziehbarkeit des Programmablaufs in der Entwicklerkonsole habe ich in diesem Projektstand aus dem Quellcode alle console.log-Statements außerhalb von render()-Funktionen sowie alle Lifecycle-Callback-Funktionen der App-Komponente entfernt.

Da in diesem Blog-Artikel alle für den Betrieb unseres React-Redux-Projektes erforderlichen Quellcodes vollständig aufgelistet sind, kann unsere Anwendung aber auch realisiert werden, ohne dabei auf der vorherigen Codebasis aufbauen zu müssen.

3. Einbinden der Redux-Bibliothek

Die Redux-Bibliothek sowie die Redux-Bindings für React können wie gewohnt als externe JavaScript-Dateien in unsere Webseite eingebunden werden.

4. Erstellen aller Redux-Bestandteile

In diesem Schritt wollen wir alle erforderlichen Bausteine für die Verwaltung unseres globalen Applikations-States mithilfe von Redux erstellen. Hierzu sind die drei Redux-Elemente State, Action und Reducer erforderlich, die im folgenden vorgestellt werden.

Damit wir diese in unserem Code sauber trennen können, wollen wir sie auf drei neue JavaScript-Dateien aufteilen, die wir in unsere index.html wie folgt einbinden:

4.1. State

Der globale State kann unter Redux ein Wert beliebigen Typs sein. Um die Möglichkeit zu schaffen mehrere Werte in unserem globalen State zu speichern, bietet sich hierfür die Verwendung eines Objekts an. Pro Anwendung sollte ausschließlich mit einer einzigen State-Implementierung gearbeitet werden.

In unserer React-Applikation ist es sinnvoll, das String-Array taskList, welches bisher in der App-Komponente über deren state-Variable verwaltet wurde, in den globalen Redux-State auszulagern, da dieser Wert auch an die Komponente TaskList übergeben und somit von ihr mitverwendet wird.

Die React-Komponente TaskInput spricht über ihre state-Variable die beiden Werte inputError und inputText an, die allerdings nicht außerhalb der TaskInput-Komponente verwendet werden. Daher ergibt es keinen Sinn, sie in unseren globalen State zu überführen.

Der von Redux verwaltete State sollte immer als read-only betrachtet und ausschließlich durch das Anwenden von Actions verändert werden. Somit können wir inkonsequente und direkte Veränderungen unseres State-Objektes von Vornherein ausschließen. Dieses sogenannte Dispatchen von Actions wird in den nächsten beiden Schritten dieses Kapitels behandelt.

Wir wollen nun eine neue Klasse erstellen, die unseren globalen State repräsentiert und in der unser String-Array taskList festgehalten wird. Der Aufbau der Klasse ist simpel, da sie aus dem einzelnen, nicht-statischen Feld taskList besteht und lediglich dessen Zuweisung innerhalb eines Konstruktors erfolgt.

Die Klasse mit der genannten Funktionalität erstellen wir unter js/redux/State.jsx:

4.2. Action

Eine Action stellt unter Redux ein Datenpaket dar, das alle erforderlichen Informationen zur Veränderung unseres globalen States beinhaltet. Konkret handelt es sich hierbei um ein gewöhnliches JavaScript-Objekt, das in dem obligatorischen Feld type eine eindeutige ID eines beliebigen Datentyps für diese Action definiert. Optional können beliebig viele weitere Felder angegeben werden.

Unsere App-Komponente definiert vier nicht-statische Methoden, die jeweils eine spezifische Manipulation an dem im State der Komponente gehaltenen taskList-Array vornehmen. Diese vier Manipulationen wollen wir nun als Redux-Actions definieren.

Hierfür überlegen wir uns für jede dieser vier Aktionen eine eindeutige ID und einen entsprechenden Funktionsparameter. Zur Sicherstellung einer guten Lesbarkeit beim Debuggen unserer Action-Objekte entscheiden wir uns für einen eindeutigen String für deren ID sowie naturgemäß für sprechende Parameter-Namen:

Action-ID Action-Beschreibung Parameter-Name Parameter-Beschreibung
ACTION_CREATE_TASK Erstellen eines Tasks taskName Name des neuen Tasks
ACTION_DELETE_TASK Löschen eines Tasks taskIndex Index zu löschenden Tasks
ACTION_MOVE_TASK_UP Aufpriorisieren eines Tasks taskIndex Index zu aufzupriorisierenden Tasks
ACTION_MOVE_TASK_DOWN Abpriorisieren eines Tasks taskIndex Index zu abzupriorisierenden Tasks

Anhand der hier zusammengetragenen Informationen können wir unsere vier erforderlichen Action-Objekte definieren und für deren leichte Wiederverwertbarkeit entsprechend parametrisierte Creator-Funktionen erstellen.

Wir halten die konstanten Action-IDs sowie die Klasse mit den Funktionen zum Erstellen unserer Actions in der neuen Datei js/redux/Action.jsx fest:

4.3. Reducer

Der Reducer stellt eine Funktion dar, in der genau spezifiziert ist, welche Action welche Änderung am State durchführt. Der Reducer wird vom Redux-System jedesmal dann aufgerufen, wenn eine Action dispatcht wird. Übergeben wird der Funktion bei deren Aufruf der bestehende State sowie die zu dispatchende Action. Als Rückgabewert liefert die Reducer-Funktion dann den durch die angegebene Action veränderten State.

Da der Redux-State als read-only behandelt werden muss, ist es zwingend erforderlich, bei Veränderungen des States durch den Reducer immer eine neue Instanz des State-Objektes zurückzugegeben.

Zur besseren Lesbarkeit unseres Quellcodes kann unsere Reducer-Funktion für die Behandlung jeder Action auch eine eigens hierfür geschriebene Funktionen aufrufen. Diese Unterfunktionen werden ebenfalls als Reducer bezeichnet.

Im nächsten Schritt können wir nun unsere gesamte Logik zur Manipulation unseres taskList-Arrays, die zuvor in der Komponente App behandelt wurde, in eine neue Klasse unter js/redux/Reducer.js auslagern:

Durch die Angabe des console.log-Statements unserer Reducer-Funktion gobalReducer können wir bei Betriebnahme unseres Redux-Systems den Zustand unseres States zu Beginn und zum Abschluss unserer Reducer-Funktion in der Entwicklerkonsole kontrollieren.

5. Redux-Store

Mit unseren drei neuen Klassen zur Abbildung der Redux-Bestandteile State, Action und Reducer haben wir nun unsere gesamte Anwendungslogik in der von Redux vorgegebene Struktur nachgebaut und können das Redux-System jetzt verwenden. Im ersten Schritt wollen wir dies völlig losgelöst von unseren bestehenden React-Komponenten tun.

Der Store stellt unter Redux den globalen State-Container für unsere Anwendung dar. Innerhalb dieses Stores wird der globale State verwaltet und Actions dispatcht. Beim Erstellen des Stores muss lediglich die erstellte Reducer-Methode angegeben werden.

Sobald der Redux-Store erstellt wurde, kann mittels dessen dispatch()-Funktion eine Action darauf angewendet werden. Zur Demonstration dieser Funktionsweise können wir in unserer js/index.jsx einen Redux-Store erstellen und testweise ein paar Actions darauf dispatchen.

Wenn wir die bisher durchgeführten Änderungen auf unserer Webseite testen, können wir die Aufrufe des Redux-Reducers in unserer Entwicklerkonsole kontrollieren. Hier sehen wir, dass nach dem Dispatchen der fünf aufgelisteten Actions lediglich die beiden ToDos „Wäsche waschen“ und „Abwaschen“ übrigbleiben.

Da wir bisher keinerlei Änderungen an unseren bestehenden React-Komponenten durchgeführt haben, arbeiten sie zu diesem Zeitpunkt noch wie in unserem letzten Workshop und zeigen somit noch die innerhalb des Konstruktors unserer App-Komponente vorgegebenen vier Task-Items an.

6. Verbinden unseres React-Projektes mit Redux

In diesem Kapitel wollen wir unsere bestehenden React-Komponenten sowie deren Einhängen in das DOM so umschreiben, dass das neu definierte Redux-System zum Einsatz kommt. Somit kann auch die bestehende Anwendungslogik, die aktuell noch direkt in den React-Komponenten definiert ist, komplett entfallen.

6.1. Einsetzen des Redux-Providers

Damit unsere React-Komponenten auf den Redux-Store zugreifen können, müssen wir diesen über eine React-Redux Provider-Komponente zur Verfügung stellen, indem wir diese um das Tag unserer App-Komponente legen.

Der Store wird mittels des Attributs store an die React-Komponente Redux.Provider übergeben und steht somit allen darin befindlichen React-Komponenten zur Verfügung. Auch diese Erweiterung führen wir in unserer js/index.jsx durch:

6.2. Verbinden aller React-Komponenten

Beim Verbinden einer React-Komponente mit dem Redux-Store werden die beiden Mappings mapStateToProps und mapDispatchToProps festgelegt.

mapStateToProps bestimmt, welche Werte des globalen Redux-States innerhalb der React-Komponente über deren Properties verfügbar gemacht werden.

mapDispatchToProps bestimmt, welche Actions des Redux-Systems innerhalb der React-Komponente über deren Properties als Funktionen dispatcht werden können.

Die Funktion connect aus dem Redux-Framework verbindet eine React-Komponente mit genau diesen beiden Mappings und gibt eine verbundene Instanz dieser React-Komponente zurück.

6.2.1. Verbinden der Komponente TaskList

Die Komponente TaskList griff bisher auf das an sie übergebene String-Array taskList über ihre Property-Variable zu. Zudem wurden drei Callbacks zum Aufpriorisieren, Abpriorisieren und Löschen von Tasks ebenfalls über ihre Property-Variable aufgerufen. Somit müssen wir bei der Überführung der Klasse in unser Redux-System an ihrem Körper keinerlei Veränderungen durchführen.

Da der Zugriff auf das String-Array taskList nun auf das Feld unseres globalen Redux-States erfolgen soll, müssen wir dies beim Verbinden dieser Komponente mittels der connect-Funktion in der mapStateToProps angeben. Zudem werden die drei definierten Callbacks als Dispatcher der entsprechenden Actions über die mapDispatchToProps übergeben.

Da wir die Komponente weiterhin unter dem Bezeichner TaskList verwenden wollen, sie aber nach dem Verbinden einen anderen Bezeichner erhalten muss, ändern wir einfach ihren ursprünglichen Namen in TaskListUnconnected.

Die folgenden Änderungen führen wir somit an der js/component/TaskList.tsx durch:

6.2.2. Verbinden der Komponente TaskInput

In unserer React-Komponente TaskInput müssen wir lediglich das Dispatchen der Action ACTION_CREATE_TASK mit der Property onTaskCreate über die mapDispatchToProps verbinden. Da wir innerhalb dieser Komponente nicht auf den globalen State zugreifen, müssen wir dementsprechend keine State-Variable auf eine Property mappen und können somit für die Angabe mapStateToProps den Wert null übergeben.

Da diese Komponente mit dem Redux-System verbunden wird, müssen wir auch ihren ursprünglichen Namen ändern. Wir machen dies nach dem gleichen Schema wie bei der Komponente TaskList und ändern somit den ursprünglichen Namen der Klasse in TaskInputUnconnected.

Die Änderungen an der js/component/TaskInput.tsx sehen somit folgendermaßen aus:

6.2.3. Verbinden der Komponente App

Unsere Komponente App hatte bisher unser String-Array taskList über ihre State-Variable verwaltet und dieses Array zudem an die Komponente TaskList über deren Properties weitergeleitet. Zudem wurden die vier Callback-Funktionen createTask, deleteTask, moveTaskUp und moveTaskDown definiert, die zur Änderung des States der Komponente App an die beiden Komponenten TaskInput und TaskList übergeben wurden.

Da nach dem Auslagern der Anwendungslogik in das Redux-System die beiden Komponenten TaskList und TaskInput direkt mit dem globalen State kommunizieren, hat unsere App-Komponente nun keine Berührung mehr mit dem globalen State. Somit müssen wir diese Komponente auch nicht mit dem Redux-System verbinden. Auch die Definition der Callbacks sowie deren Übergabe an die beiden Komponenten TaskInput und TaskList kann nun komplett entfallen.

Zuguterletzt kann sogar der Konstruktor mit der Definition des initialen States entfallen, da auch dieser durch die initialen Aufrufe der Dispatcher nach Erstellung des Stores obsolet geworden ist.

Somit sieht der neue Quellcode unserer js/component/App.jsx nach der Einführung von Redux sehr übersichtlich aus:

7. Resultat

Nach dem Umschreiben unserer React-Komponenten haben wir nun wieder eine funktionierende Task-Listen-Anwendung, bei der die gesamte Applikationslogik in die von Redux zum State-Handling prädestinierte Struktur ausgelagert wurde.

In der Entwicklerkonsole können wird genau beobachten, wie sich das State-Objekt beim Dispatchen von Actions verändert, indem wir in unserer Web-Applikation neue Tasks erstellen und bestehende Tasks gelöscht oder umpriorisiert werden.

Die React-Redux-Bibliothek sorgt selbstständig dafür, dass entsprechende Änderungen am State nur an die jeweils betroffenen Komponenten weitergeleitet werden. In unserer Applikation wird somit beispielsweise beim Löschen oder Umpriorisieren von Tasks lediglich die Komponente TaskList neu gerendert – die Komponente TaskInput ist hiervon nicht betroffen. Lediglich beim Erstellen neuer Tasks werden beide Komponenten neu gerendert. Die Komponente App wird im Gegensatz zum vorherigen Verhalten in keinem der genannten Fälle neu gerendert, da sie von keiner Änderung des globalen States betroffen ist.

Das fertige Projekt mit allen durchgeführten Änderungen ist auf GitHub abgelegt.

8. Browser-Erweiterung „Redux DevTools“

Redux-DevTools-Logo

Mit der für Chrome verfügbaren Erweiterung „Redux DevTools“ können alle Bestandteile des Redux-Systems genau verfolgt und analysiert werden. Somit ist es über das Entwicklerfenster der „Redux DevTools“ möglich, eine Zeitreise durch alle mitgeloggten States der Applikation zu unternehmen und die einzelnen Veränderungen am globalen State genau zu untersuchen. Das ermöglicht ein sehr gutes Debugging unseres Anwendungsverhaltens.

Damit der Store von den „Redux Dev Tools“ aufgezeichnet wird ist es erforderlich, den folgenden optionalen zweiten Parameter beim Erstellen des Redux-Stores hinzuzufügen:

So long for now ..

Es würde mich sehr freuen, wenn ich in meinem Workshop einen schnellen Einstieg in Redux in Kombination mit React geben und anhand unserer Beispielanwendung die Funktionsweise und Vorteile eines State-Handling-Systems vermitteln konnte.

Für Feedback bin ich wie immer sehr gerne unter christopher.stock@mayflower.de erreichbar.

João Silas

Unser React-Workshop


Dieser Eintrag wurde veröffentlicht in Development und verschlagwortet mit , , , von Christopher Stock. Permanenter Link zum Eintrag.

Über Christopher Stock

Christopher ist als Developer bei der Mayflower GmbH tätig und entwickelt dort hochwertige Web-Applikationen, unter anderem mit Java, TypeScript und PHP. Zu seinen Hobbys gehören neben der Programmierung auch Designen, Joggen, Skifahren, Schwimmen, Fitness und Verreisen. Trotz mehr als 15 Jahren Berufserfahrung als Softwareentwickler im Web-, Desktop- und Mobile Application-Bereich liebt er die Programmierung noch immer wie bei seinem allerersten GW-BASIC Programm.

Für neue Blogupdates anmelden:


4 Gedanken zu “Redux-Workshop für Einsteiger

Schreibe einen Kommentar

Deine E-Mail-Adresse wird nicht veröffentlicht. Erforderliche Felder sind mit * markiert.