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.
- Navigation
- 1. Warum Redux?
- 2. Voraussetzungen
- 3. Einbindung
- 4. Erste Bestandteile
- 5. Redux-Store
- 6. React & Redux
- 7. Resultat
- 8. Redux DevTools
- Fazit
1. Warum Redux?
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.
<!DOCTYPE html> <html> <head> <!-- default encoding --> <meta charset="UTF-8" /> <!-- external stylesheet and page icon --> <link rel="stylesheet" href="css/styles.css"> <link rel="icon" href="favicon.ico" type="image/x-icon"> <!-- library sources --> <script src="https://github.com/facebook/react/releases/download/v16.0.0/react.production.min.js" type="text/javascript"></script> <script src="https://github.com/facebook/react/releases/download/v16.0.0/react-dom.production.min.js" type="text/javascript"></script> <script src="https://fb.me/JSXTransformer-0.13.3.js" type="text/javascript"></script> <script src="https://cdnjs.cloudflare.com/ajax/libs/redux/3.7.2/redux.js" type="text/javascript"></script> <script src="https://cdnjs.cloudflare.com/ajax/libs/react-redux/5.0.6/react-redux.js" type="text/javascript"></script> <!-- custom sources --> <script src="js/component/App.jsx" type="text/jsx"></script> <script src="js/component/TaskInput.jsx" type="text/jsx"></script> <script src="js/component/TaskList.jsx" type="text/jsx"></script> <script src="js/index.jsx" type="text/jsx"></script> </head> <body> <div id="mainContainer"></div> </body> </html>
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:
<!DOCTYPE html> <html> <head> <!-- default encoding --> <meta charset="UTF-8" /> <!-- external stylesheet and page icon --> <link rel="stylesheet" href="css/styles.css"> <link rel="icon" href="favicon.ico" type="image/x-icon"> <!-- library sources --> <script src="https://github.com/facebook/react/releases/download/v16.0.0/react.production.min.js" type="text/javascript"></script> <script src="https://github.com/facebook/react/releases/download/v16.0.0/react-dom.production.min.js" type="text/javascript"></script> <script src="https://fb.me/JSXTransformer-0.13.3.js" type="text/javascript"></script> <script src="https://cdnjs.cloudflare.com/ajax/libs/redux/3.7.2/redux.js" type="text/javascript"></script> <script src="https://cdnjs.cloudflare.com/ajax/libs/react-redux/5.0.6/react-redux.js" type="text/javascript"></script> <!-- custom sources --> <script src="js/redux/Action.jsx" type="text/jsx"></script> <script src="js/redux/Reducer.jsx" type="text/jsx"></script> <script src="js/redux/State.jsx" type="text/jsx"></script> <script src="js/component/App.jsx" type="text/jsx"></script> <script src="js/component/TaskInput.jsx" type="text/jsx"></script> <script src="js/component/TaskList.jsx" type="text/jsx"></script> <script src="js/index.jsx" type="text/jsx"></script> </head> <body> <div id="mainContainer"></div> </body> </html>
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:
/** * Defines the global application state. * * @author Christopher Stock * @version 1.0 */ class State { /** * Creates a new application state object. * * @param {string[]} taskList The task list as an array. */ constructor( taskList = [] ) { this.taskList = taskList; } }
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:
const ACTION_CREATE_TASK = 'ACTION_CREATE_TASK'; const ACTION_DELETE_TASK = 'ACTION_DELETE_TASK'; const ACTION_MOVE_TASK_UP = 'ACTION_MOVE_TASK_UP'; const ACTION_MOVE_TASK_DOWN = 'ACTION_MOVE_TASK_DOWN'; /** * Specifies all redux action creators. * * @author Christopher Stock * @version 1.0 */ class Action { /** * Specifies the redux action for creating a task. * * @param {string} taskName The name of the task to create. * * @return {Object} The action object for creating a task. */ static createTask( taskName ) { return { type: ACTION_CREATE_TASK, taskName: taskName, } } /** * Specifies the redux action for deleting a task. * * @param {number} taskIndex The index of the task to delete. * * @return {Object} The action object for deleting a task. */ static deleteTask( taskIndex ) { return { type: ACTION_DELETE_TASK, taskIndex: taskIndex, } } /** * Specifies the redux action for moving a task up. * * @param {number} taskIndex The index of the task to move up. * * @return {Object} The action object for moving a task up. */ static moveTaskUp( taskIndex ) { return { type: ACTION_MOVE_TASK_UP, taskIndex: taskIndex, } } /** * Specifies the redux action for moving a task down. * * @param {number} taskIndex The index of the task to move down. * * @return {Object} The action object for moving a task down. */ static moveTaskDown( taskIndex ) { return { type: ACTION_MOVE_TASK_DOWN, taskIndex: taskIndex, } } }
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:
/** * Specifies all redux reducers. * * @author Christopher Stock * @version 1.0 */ class Reducer { /** * Specifies the global reducer method for the entire TaskList application. * * @param {State} state The existing state object. * @param {Object} action The action to perform on the state object. * * @return {State} The new state object. */ static globalReducer( state = new State(), action ) { console.log( "Reducer.taskListReducer being invoked" ); console.log( " applying action ", action ); console.log( " old state is ", state ); let newState = null; switch ( action.type ) { case ACTION_CREATE_TASK: { newState = Reducer.createTaskReducer( state, action ); break; } case ACTION_DELETE_TASK: { newState = Reducer.deleteTaskReducer( state, action ); break; } case ACTION_MOVE_TASK_UP: { newState = Reducer.moveTaskUpReducer( state, action ); break; } case ACTION_MOVE_TASK_DOWN: { newState = Reducer.moveTaskDownReducer( state, action ); break; } default: { newState = state; break; } } console.log( " new state is ", newState ); return newState; } /** * Reduces the state in order to create a new task. * * @param {State} state The existing state object. * @param {Object} action The action to perform on the state object. * * @return {State} The new and reduced state object. */ static createTaskReducer( state, action ) { let newTasks = state.taskList.slice(); newTasks.push( action.taskName ); return new State( newTasks ); } /** * Reduces the state in order to delete a new task. * * @param {State} state The existing state object. * @param {Object} action The action to perform on the state object. * * @return {State} The new and reduced state object. */ static deleteTaskReducer( state, action ) { let newTasks = state.taskList.slice(); newTasks.splice( action.taskIndex, 1 ); return new State( newTasks ); } /** * Reduces the state in order to move a task up. * * @param {State} state The existing state object. * @param {Object} action The action to perform on the state object. * * @return {State} The new and reduced state object. */ static moveTaskUpReducer( state, action ) { let newTasks = state.taskList.slice(); let taskToMoveUp = newTasks[ action.taskIndex ]; let taskToMoveDown = newTasks[ action.taskIndex - 1 ]; newTasks[ action.taskIndex - 1 ] = taskToMoveUp; newTasks[ action.taskIndex ] = taskToMoveDown; return new State( newTasks ); } /** * Reduces the state in order to move a task down. * * @param {State} state The existing state object. * @param {Object} action The action to perform on the state object. * * @return {State} The new and reduced state object. */ static moveTaskDownReducer( state, action ) { let newTasks = state.taskList.slice(); let taskToMoveUp = newTasks[ action.taskIndex + 1 ]; let taskToMoveDown = newTasks[ action.taskIndex ]; newTasks[ action.taskIndex ] = taskToMoveUp; newTasks[ action.taskIndex + 1 ] = taskToMoveDown; return new State( newTasks ); } }
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.
// specify the application title const APPLICATION_TITLE = "React Task List"; // set page title document.title = APPLICATION_TITLE; // reference the main container let mainContainer = document.getElementById( "mainContainer" ); // create redux store let store = Redux.createStore( Reducer.globalReducer ); store.dispatch( Action.createTask( "Müll rausbringen" ) ); store.dispatch( Action.createTask( "Abwaschen" ) ); store.dispatch( Action.createTask( "Wäsche waschen" ) ); store.dispatch( Action.moveTaskUp( 2 ) ); store.dispatch( Action.deleteTask( 0 ) ); // render the App component into the main container ReactDOM.render( <App title={ APPLICATION_TITLE } /> mainContainer );
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:
// specify the application title const APPLICATION_TITLE = "React Task List"; // set page title document.title = APPLICATION_TITLE; // reference the main container let mainContainer = document.getElementById( "mainContainer" ); // create redux store let store = Redux.createStore( Reducer.globalReducer ); store.dispatch( Action.createTask( "Müll rausbringen" ) ); store.dispatch( Action.createTask( "Abwaschen" ) ); store.dispatch( Action.createTask( "Wäsche waschen" ) ); store.dispatch( Action.moveTaskUp( 2 ) ); store.dispatch( Action.deleteTask( 0 ) ); // render the App component into the main container ReactDOM.render( <ReactRedux.Provider store={ store }> <App title={ APPLICATION_TITLE } /> </ReactRedux.Provider>, mainContainer );
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:
/** * Represents the TaskList component. * * @author Christopher Stock * @version 1.0 */ class TaskListUnconnected extends React.Component { /** * Being invoked every time this component renders. * * @return {JSXTransformer} The rendered JSX. */ render() { console.log( "TaskList.render() being invoked" ); return <ul id="taskList"> { this.createItems() } </ul>; } /** * Creates and returns all items for the task list. * * @return {JSXTransformer[]} The rendered JSX elements. */ createItems() { let items = []; // browse all task list items for ( let index = 0; index < this.props.taskList.length; ++index ) { items.push( <li key={ index }> { /* The item description */ } { this.props.taskList[ index ] } { /* Button 'Delete' */ } <button onClick={ () => { this.props.onTaskDelete( index ); } } className="button" > ✖ </button> { /* Button 'Move Down' */ } <button onClick={ () => { this.props.onTaskMoveDown( index ); } } disabled={ index === this.props.taskList.length - 1 } className="button" > ▼ </button> { /* Button 'Move Up' */ } <button onClick={ () => { this.props.onTaskMoveUp( index ); } } disabled={ index === 0 } className="button" > ▲ </button> </li> ); } return items; } } const taskListMapStateToProps = (state ) => { return { taskList: state.taskList } }; const taskListMapDispatchToProps = { onTaskDelete: Action.deleteTask, onTaskMoveUp: Action.moveTaskUp, onTaskMoveDown: Action.moveTaskDown, }; const TaskList = ReactRedux.connect( taskListMapStateToProps, taskListMapDispatchToProps )( TaskListUnconnected );
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:
/** * Represents the input component that lets the user create new tasks. * This is an example for a stateful and controlled component. * * @author Christopher Stock * @version 1.0 */ class TaskInputUnconnected extends React.Component { /** * Initializes this component by setting the initial state. * * @param {Object} props The initial properties being passed in the component tag. */ constructor( props ) { super( props ); this.state = { inputError: false, inputText: "", } } /** * Being invoked every time this component renders. * * @return {JSXTransformer} The rendered JSX. */ render() { console.log( "TaskInput.render() being invoked" ); return <form onSubmit={ ( event ) => { this.onFormSubmit( event ); } }> { /* new task input */ } <input id="newTask" type="text" maxLength="50" className={ this.state.inputError ? "input error" : "input" } value={ this.state.inputText } onChange={ ( event ) => { this.onInputChange( event ); } } /> <br /> { /* new task button */ } <input id="submitButton" type="submit" value="Create Task" className="button" /> </form>; } /** * Being invoked when the input field value changes. * * @param {Event} event The event when the input field value changes. */ onInputChange( event ) { this.setState( { inputError: false, inputText: event.target.value, } ); } /** * Being invoked when the form is submitted. * * @param {Event} event The form submission event. */ onFormSubmit( event ) { // suppress page reload event.preventDefault(); // trim entered text let enteredText = this.state.inputText.trim(); // check entered text if ( enteredText.length === 0 ) { // set error state this.setState( { inputError: true, inputText: "", } ); } else { // clear error state this.setState( { inputError: false, inputText: "", } ); // invoke parent listener this.props.onTaskCreate( enteredText ); } }; } const taskInputMapStateToProps = null; const taskInputMapDispatchToProps = { onTaskCreate: Action.createTask, }; const TaskInput = ReactRedux.connect( taskInputMapStateToProps, taskInputMapDispatchToProps )( TaskInputUnconnected );
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:
/** * The entire application component. * * @author Christopher Stock * @version 1.0 */ class App extends React.Component { /** * Being invoked every time this component renders. * * @return {JSXTransformer} The rendered JSX. */ render() { console.log( "App.render() being invoked" ); return <div> { /* title */ } <h1 id="appTitle">{ this.props.title }</h1> { /* task input form */ } <TaskInput /> { /* task list */ } <TaskList /> </div>; } }
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“
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:
// create redux store let store = Redux.createStore( Reducer.globalReducer, window.__REDUX_DEVTOOLS_EXTENSION__ && window.__REDUX_DEVTOOLS_EXTENSION__() );
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.
Schreibe einen Kommentar