Redux – der Workshop

Redux-Workshop für Einsteiger

Avatar von Christopher Stock

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.

<!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“

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:

// 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.

João Silas

Unser React-Workshop


Avatar von Christopher Stock

Kommentare

4 Antworten zu „Redux-Workshop für Einsteiger“

  1. In meinem neuen Blog-Artikel gebe ich Euch eine schnelle und praktische Einführung in das State-Handling System Red… https://t.co/Aw7Rtphvli

  2. Nach #React folgt … #Redux. Unser @jenetic1980 mit dem großen Einsteigerworkshop! https://t.co/b7kZg05Mwa

  3. Wer sich mit #React beschäftigt, sollte sich auch #Redux angesehen haben. In unserem großen Einsteiger-Workshop hab… https://t.co/nm3XLyqa0a

Schreibe einen Kommentar

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


Für das Handling unseres Newsletters nutzen wir den Dienst HubSpot. Mehr Informationen, insbesondere auch zu Deinem Widerrufsrecht, kannst Du jederzeit unserer Datenschutzerklärung entnehmen.