A smooth introduction to the Redux world

0

Introduction: Let me start by saying that I am mainly a back-end developer. My front-end skills were a bit outdated and limited to building some decent UIs with Javascript/jQuery and Bootstrap. At least, until one month ago. The last month I had to go through a crash course involving the following front-end technologies: ES6, React.js, Express.js, Node.js, Redux and some theory on building isomorphic applications. I have to admit that the front-end world works quite differently than the back-end. It reminds me more of the way I was building desktop application, once upon a time, with Delphi. In such environments, event-driven programming…rules.

In this post, I would try to make an introduction to Redux and explain what Redux is about from the viewpoint of a newcomer. So, let’s start with some short definitions. Redux is “a state container for Javascript apps” (as it is stated in its website) or “a library designed for managing application state” (as stated by Wikipedia). So, it’s all about state. Managing the application state is the main purpose. But why, when and how, are the interesting parts. Let’s dive.

From local state to global state: The main concept of Redux is that we are moving the local state of each Javascript component (the local state should be a familiar concept, especially if you have worked with React…and maybe other libraries/frameworks, but React is the only one I am familiar with) to a centralized place. We will just call it: “store”. So, there will be a global state that will be comprised by each component’s local state. This global state is being stored and managed by Redux store. Of course, each component is free to place only a part of its local state to the store, but let’s keep this separation out of our minds to first get a grasp of the general idea. To create such a store, we need to import the createStore function:

import { createStore } from 'redux';

We leave for a bit later any details about how exactly this function is used.

The flow: So, whenever a component wants to update/change its state, it notifies this centralized store about it . The store updates the global state and then it notifies all components about this change. How does it know the set of all components that need to be notified ? Every component that has a local state and is going to keep this state as part of the global state, it subscribes to Redux store. Subscribing means that it wants to be notified for changes happening to the global state. On notification, the component can take any required action like updating their markup or the way they work (e.g add or remove an event listener). So, no features (markup or functionality) of a component that are based on its state should be updated before the global state has been updated.

As an example, if I have a form through which I add items to a list, then, according to Redux, whenever this form is submitted, the flow of events should be:

a. The user submits the form

b. The form notifies the Redux store that a submission happened

c. The Redux store updates the global status (e.g based on the form contents, it adds a new item in the list)

d. The Redux store notifies all components about this change

e. The list is notified about this change and it updates itself in order to display the new item

Yes, the whole flow is a bit strange and it adds significant complexity to your application. But it offers some advantages and as always we are called to weight the pros and cons and decide if you want to go this way or not.

From a more technical point of view the flow can be described by the following diagram:

At the moment a notified component decides to take action, it will probably need to access some values from the updated global state. This can be done by:

const state = store.getState();

The mediator pattern: Beside what we have already said about the centralization of the state, you should have probably noticed a bit of event distribution happening as part of the whole process. We are getting there, soon. A component asking for a state change can be thought as a request for an action. And so, the component sends an action object to the Redux store. This action object should contain the action type (yes, like saying the “event type”) and information that is needed for the action to be completed (yes, like a description of the event). And this event distribution is a secondary functionality of the Redux store that is meant to support the first, the global state. It is a version of the mediator pattern where the subscribers and notified by an event but this notification carries not only information about this event. It carries information about the state of the whole UI. A component can subscribe like this:

const unsubscribe = store.subscribe(() =>
    console.log(store.getState())
)

What is returned by the store is a function that can be used by the component in order to unsubscribe. And what is passed to the store during subscription is a callback function. Usually, this callback will be part of the subscribing component (a method of the component’s class) and it contains logic that this component needs to execute when notification about a specific action type comes from the store.

Note: The difference between the “event” and “action” terminology is, in my opinion, not significant here (it is not like the “event” and “action” concepts in CQRS/Event Sourcing where they represent two different things: https://github.com/reactjs/redux/issues/891 ). Here, the first one works passively, by emphasizing the need to respond when somethings happens, and the second works actively, by emphasizing the we are asking for something to happen (hiding the reasoning behind this intention).

Of course, no matter if information about state changes is included in the action object, the role of Redux store as an event manager does not change. I just wanted to make this distinction so that I can put aside for a moment the “manager of global state” functionality of the Redux store. Event management is, of course, the means for implementing the global state management.
Dispatching an action: The update of global state and the notification of all subscribed components about an action (and anything else that should also take place as part of this action) by the Redux store is called “dispatching”. This dispatching is not necessarily triggered by a component. You can call store.dispatch(action) from anywhere in your app, including components and XHR callbacks, or even at scheduled intervals.

var action = {
    type : "ADD_NEW_USER",
    email : "tomas@hellomail.com",
    password: "rgh3rqte"
};

store.dispatch(action);

Action creators: In order to better organize our code and because sometimes an action may be fired in many places, it is a common practice to define functions that return action objects. Such a function is called “action creator”.

function addNewUser(text) {
    return {
        type : ADD_NEW_USER,
        email : "tomas@hellomail.com",
        password: "rgh3rqte"
    }
}

The Reducer: The Redux store function that handles the actions is called a “reducer”. We are responsible of building this function. Redux does not automatically update anything in the global state. There should always be at least one reducer. The top-level reducer. We need its definition there before we create the store, because it is an internal part of the store’s functionality. Let’s see an example:

const reducer = (currentState, action) => {
    switch(action.type) {
        case 'NEW_USER_FORM_SUBMISSION':

            const newUser = {
                "email" : action.email,
                "password": action.password
            };

            const newUserList = currentState.users.concat([newUser ]);

            state = {
                ...currentState,
                users: newUserList
            };

            break;
            
        case 'SOME_OTHER_ACTION':
            ...
            break;
    }

    return state;
}

const store = createStore(reducer);

Notice that the current (global) state is implicitly passed as the first parameter to the top-level reducer. Also notice that the reducer does not modify the existing state object but instead it creates a new state object. This happens in order not to destroy the old state. This allows us to travel back in time for the state and follow the changes (e.g for debugging purposes). After the reducer returns the new state, Redux store internally replaces the state with the one returned by the reducer.

What is the state the first time a reducer will be called ? Nothing! We are responsible for initializing the state at the reducer definition:

const initialState = {
    users: []
};

const reducer = (state = initialState, action) => {
    ...
};

or during the creation of the store:

const initialState = {
    users: []
};

const store = createStore(reducer, initialState);

Using multiple reducers: Usually a lot of actions are defined in an application and so the job of the top-level reducer is split to more than one (sub)reducers. This is usually done in one of the two following ways. Firstly, we can use multiple reducers where each one handles a specific action or a set of actions (separation based on actions). Secondly, we can delegate the update of a specific part of the global state to a separate reducer (separation based on state parts). An example follows that uses multi-level reducers where each reducer handles a specific part of the global state.

The state:

var theState = {
    menu: [
        {
            label: 'Home',
            url: '/home'
        },
        {
            label: 'About',
            url: '/about'
        }
    ]
}

The reducers:

const ADD_MENU_ITEM = 'Add submenu item';

function menuReducer(menuState, action) {
    switch (action.type) {
        case ADD_MENU_ITEM:
            var newMenuState = Objectd.assign({}, menuState);
            newState.push(action.menuItem);
            return newMenuState;
    }
}

function topLevelReducer(state, action) {
    switch (action.type) {
        case ADD_MENU_ITEM:
            return {
                ...state,
                "menu": menuReducer(state.menu, action)
            }
    }
}

Take notice that each reducer gets as parameter just the part of the state it is interested in. Of course, we can initialize the state only for the top-level reducer (global state) or use a distinct initial state for each sub-reducer (as far as the set of sub-reducers covers all the possible actions and state parts.

Ok! This is the general idea. It does not describe the whole functionality of Redux.js but it gives a big picture of the Redux architecture.