Skip to content

Rainbow
Apps

Rainbow
Apps

How does Redux works?

Redux is a javascript library used in a lot of projects, which helps to manage a global state. It exists some binding libraries to use Redux with React: react-redux, Angular: ng-redux, Ember: ember-redux, ...

In this article I will not explain the best practices on how to use Redux. If you want more explanation on how to us it, you can see the documentation which is awesome: https://redux.js.org/

Basically, you have a single state for your whole application and this state must stay immutable.

Reducer creation

A reducer is a pure function, it is the only one which can change the state (sometimes called also store). The first parameter of this method is the current state and the second one the action to handle:

The action is a simple object which is often represented with:

  • type: the type of the action to process
  • payload: the data useful to process the action
const initialState = { userName: undefined };

export default function userReducer(state = initialState, action) {
    switch(action.type) {
        case 'SET_USERNAME': {
            // The state must stay immutable
            return { ...state, userName: action.payload };
        }
        default:
            return state;
    }
}

Store creation

Context

To create a store, you have to use the method createStore and give it the reducer(s) in first parameter:

import { createStore } from 'redux';
import userReducer from './userReducer';

const store = createStore(userReducer);

With this store created, you can get two methods:

  • getState to get the current state
  • dispatch to dispatch actions wich will be passed to reducers
store.dispatch({ type: 'SET_USERNAME', payload: "Bob the Sponge" });

const state = store.getState();

console.log(state.userName); // Print "Bob the Sponge"

Well, Rainbow, you told us that you will explain what is under the hood and finally you explain how to use it.

Sorry guys, I needed to put some context before going deep into Redux ;)

Under the hood

In all the links you will see below I browse the code of the commit with the sha 176e66adc9a90df.

Actually createStore is a closure which has an object state et return the methods getState and dispatch:

function createStore(reducer) {
    let state;

    const getState = () => state;

    const dispatch = (action) => {
        state = reducer(state, action);

        return action;
    }

    // Populates the state with the initial values of reducers
    dispatch({ type: '@@redux/INIT'})

    return { getState, dispatch }
}

As you can see, it's executed synchronously

Note: createStore can receive a preloadedState to initialize the state. It's not useful if you have initial states on your reducers.

Multiple reducers

For the moment, we saw a simple case with a single reducer. But in applications you usually more than one otherwise redux is maybe a little bit overkill for your use case.

Redux can structure the store in a clean way, by dividing our store.

Let's go use the function combineReducers.

For example, with the previous reducer userReducer, and the new one settingsReducer:

const initialState = { maxSessionDuration: undefined };

export default function settingsReducer(state = initialState, action) {
    switch (action.type) {
        case 'SET_': {
            return { ...state, maxSessionDuration: action.payload };
        }
        default:
            return state;
    }
}

The combination of reducers will be:

import { combineReducers } from 'redux';
import userReducer from './userReducer';
import settingsReducer from './settingsReducer';

export default combineReducers({ user: userReducer, settings: settingsReducer })

We will get the state:

{
    "user": {
        "userName": undefined
    },
    "settings": {
        "maxSessionDuration": undefined
    }
}

Knowing that the code of createStore doesn't change, how does combineReducers work?

function combineReducers(reducersByNames) {
    return (state, action) => {
        let hasChanged = false;
        const nextState = {};

        Object.entries(reducersByNames).forEach(([reducerName, reducer]) => {
            // A reducer cannot access states of other ones
            const previousReducerState = state[reducerName];

            // Calculate the next state for this reducer
            const nextReducerState = reducer(previousReducerState, action);

            nextState[reducerName] = nextReducerState;

            hasChanged = hasChanged || nextReducerState !== previousReducerState;
        });

        // If there is no changes, we return the previous state (we keep the reference of the state for performance's reasons)
        return hasChanged ? nextState : state;
    }
}

We can see here that if there is mutations of the state, then Redux will be completely lost and will not see changes with the strict equality.

In the real code, there are a lot of checks to be sure everything is working correctly. I have simplified the code to explain how it works without superfluous code.

The action is passed to all reducers, it's the reducer to check if it knows how to handle it. So, we can have multiple reducers which are able to handle a same action.

Listeners

What is it?

A listener is a callback we can subscribe to potential changes of the Redux state. This listener is directly executed after an event is dispatched. Previously I talked about potential changes because, after an action has been dispatched, there is not necessarily changes, for example if none of the reducers know how to handle the event.

Once subscribed, we get a callback to be able to unsubscribe it.

An example of use case

For example if you don't want, or can't use the plugin Reduc DevTools. It can be useful to be able to see the Redux state at any time. In this you can use a listener:

import { createStore } from 'redux';
import userReducer from './userReducer';

const store = createStore(userReducer);

store.subscribe(() => window.reduxState = store.getState());

And now you can see, at any time, the state by typing in your favorite browser console: reduxState.

Let's see some code

Our createStore becomes:

function createStore(reducer) {
    let state;
    let listeners = [];

    const getState = () => state;

    const dispatch = (action) => {
        state = reducer(state, action);

        listeners.forEach(listener => listener());

        return action;
    }

    const subscribe = (listener) => {
        listeners = [...listeners, listener];

        return () => {
            listeners = listeners.filter(l => l !== listener);
        };
    }

    dispatch({ type: '@@redux/INIT'})

    return { getState, dispatch, subscribe }
}

I have simplified a lot the method subscribe. In reality, there are a lot of check, especially not to be able to subscribe/unsubscribe when an action is dispatched, ensure this is a function passed as listener, be able to call the unsubscribe mutliple times without errors, ...

Observable

A bit of context

If you use RxJS, the store is an Observable, so that you can add an Observer to be notified of state's changes.

import { from } from 'rxjs';
import { createStore } from 'redux';
import userReducer from './userReducer';

const store = createStore(userReducer);

const myObserver = { next: newState => console.log('Le nouveau redux state est: ', newState)};

from(store).subscribe(myObserver);

// Let's change the username
store.dispatch({ type: 'SET_USERNAME', payload: "Bob l'éponge" });

How does it work?

To be an Observable, the store implements the symbol Symbol.observable. Its implementation is really simple because it reuses the implementation of listeners in a method observable:

function createStore(reducer) {
    let state;
    let listeners = [];

    const getState = () => state;

    const dispatch = (action) => {
        state = reducer(state, action);

        listeners.forEach(listener => listener());

        return action;
    }

    const subscribe = (listener) => {
        listeners = [...listeners, listener];

        return () => {
            listeners = listeners.filter(l => l !== listener);
        };
    }

    const observable = () => (
        {
            subscribe: (observer) => {
                // The method `observeState` only notifies the Observer of the current value of the state
                function observeState() {
                    observer.next(getState());
                }

                // As soon as the Observer subscribes we send the current value of the state
                observeState();

                // We refirster the `observeState` function as a listener to be notified of next changes of the state
                const unsubscribe = listenerSubscribe(observeState)

                return {
                    unsubscribe
                }
            }
        }
    );

    dispatch({ type: '@@redux/INIT'})

    return { getState, dispatch, subscribe, [Symbol.observable]: observable }
}

replaceReducer

Implementation

When you use code splitting, it can happened you do not have all reducers when you create the store. To be able to register new reducers after store creation, redux give us access to the method replaceReducer which allow for replacement of reducers with new ones:

function createStore(reducer) {
    let state;
    let listeners = [];

    const getState = () => state;

    const dispatch = (action) => {
        state = reducer(state, action);

        listeners.forEach(listener => listener());

        return action;
    }

    const subscribe = (listener) => {
        listeners = [...listeners, listener];

        return () => {
            listeners = listeners.filter(l => l !== listener);
        };
    }

    const observable = () => {
        const listenerSubscribe = subscribe;

        return {
            subscribe: (observer) => {
                function observeState() {
                    observer.next(getState());
                }

                observeState();

                const unsubscribe = listenerSubscribe(observeState)
                return {
                    unsubscribe
                }
            }
        }
    }

    const replaceReducer = (newReducer) => {
        reducer = newReducer;

        // Like the action `@@redux/INIT`, this one populated the state with initial values of new reducers
        dispatch({ type: '@@redux/REPLACE' });
    }

    dispatch({ type: '@@redux/INIT'})

    return { getState, dispatch, subscribe, [Symbol.observable]: observable, replaceReducer }
}

I didn't tell you previously, but like @@redux/INIT, @@redux/REPLACE should be handled in your reducers. Otherwise you will have problems with hot reload and your reducers will become unpredictable.

Actually these actions do not have these types, they are suffixed: real types

Use

Let's use this new method replaceReducer to register a new reducer. At the store creation we only register the reducer userReducer, then we register the reducer counterReducer:

export default function counterReducer(state = { value: 0 }, action) {
    switch (action.type) {
        case 'INCREMENT': {
            return { ...state, value: state.value + 1};
        }
        default:
            return state;
    }
}

The replacement of reducers will be:

import { createStore, combineReducers } from 'redux';
import userReducer from 'userReducer';
import counterReducer from 'counterReducer';

const store = createStore(combineReducers({ user: userReducer }));

console.log(store.getState()); // Prints { user: { userName: undefined } }

store.replaceReducer(combineReducers({ user: userReducer, counter: counterReducer }));

console.log(store.getState()); // Prints { user: { userName: undefined }, counter: { value: 0 } }

If in this example, I have subscribed directly a listener after the store creation, this one would have been triggered after the reducers modification.

Middleware

Presentation

A middleware is a tool that we can put between two applications. In the Redux case, the middleware will be placed between the dispatch call and the reducer. I talk about a middleware (singular form), but in reality you can put as much as middleware you want.

An example of middleware is a middleware to log dispatched actions and then the new state.

How do we write a middleware?

I'm gonna directly give you the form of a middleware without explanation because I will never do better than the official documentation.

const myMiddleware = store => next => action => {
    // With the store you can get the state with `getState` or the original `dispatch`
    // `next`represents the next dispatch
    return next(action);
}

Example: middleware of the loggerMiddleware

const loggerMiddleware = store => next => action => {
    console.log(`I'm gonna dispatch the action: ${action}`);
    const value = next(action);
    console.log(`New state: ${value}`)
    return value;
}

redux-thunk middleware example

Until now we dispatched actions synchronously. But in an application it can happened we would like to dispatch actions asynchronously. For example, after having resolved an AJAX call with axios (or another library, or directly with XMLHttpRequest if you are juste the boss :p).

The implementation is really simple, if the action dispatched is a function, it will execute it with getState and dispatch as parameters. And if it's not a function, it passes the action to the next middleware or reducer (if there is no next middleware).

const reduxThunkMiddleware = ({ getState, dispatch }) => next => action => {
    if (typeof action === 'function') {
        return action(dispatch, getState);
    }

    return next(action);
}

The thunk action creator will be:

function thunkActionCreator() {
  return ({ dispatch }) => {
    return axios.get("/my-rest-api").then(({ data }) => {
      dispatch({
        type: "SET_REST_DATA",
        payload: data
      });
    });
  };
}

Store configuration

Before talking about how to configure middlewares with redux, let's talk about Enhancer. An enhancer (in redux) is in charge of 'overriding' the original behavior of redux. For example if we want to modify how works the dispatch (with middlewares for instance), enrich the state with extra data, add some methods in the store...

The enhancer is in charge of the creation of the store with the help of the createStore function, then to override the store created. Its signature is:

// We find the signature of the `createStore` method: function(reducer, preloadedState){}
const customEnhancer = createStore => (reducer, preloadedState) => {
    const store = createStore(reducer, preloadedState);

    return store;
}

As you may notice, to use middlewares we need an enhancer which is provided by redux (the only one enhancer provided by redux) which is named applyMiddleware:

// Transform first(second(third))(myInitialValue) with compose(first, second, third)(myInitialValue)
function compose(...functions) {
    return functions.reduce((f1, f2) => (...args) => f1(f2(...args)));
}

const applyMiddleware = (...middlewares) => createStore => (reducer, preloadedState) => {
    const store = createStore(reducer, preloadedState);

    const restrictedStore = {
        state: store.getState(),
        dispatch: () => console.error('Should not call dispatch while constructing middleware')
    }
    const chain = middlewares.map(middleware => middleware(restrictedStore));
    // We rebuild the dispatch with our middlewares and the original dispatch
    const dispatch = compose(chain)(store.dispatch);

    return {
        ...store,
        dispatch
    }
}
Note: Perhaps you used to use the method `reduce` with an accumulator initialized with a second parameter:
    const myArray = [];
    myArray.reduce((acc, currentValue) => 
        // Do some process
    }, initialValue);

If you do not give an initial value (no second paramter), the first value of your array will be taken for initial value.

Looking at the applyMiddleware implementation, you can notice that the middleware's signature could be (store, next) => action. You can see these PRs which are about this: PR 784, PR 1744

The createStore becomes:

function createStore(reducer, preloadedState, enhancer) {

    // We can pass the enhancer as 2nd parameter at the place of preloadedState
    if (typeof preloadedState === 'function' && enhancer === undefined) {
        enhancer = preloadedState;
        preloadedState = undefined;
    }

    // If we have an enhancer, let's use it to create the store
    if (typeof enhancer === 'function') {
        return enhancer(createStore)(reducer, preloadedState);
    }

    let state = preloadedState;
    let listeners = [];

    const getState = () => state;

    const dispatch = (action) => {
        state = reducer(state, action);

        listeners.forEach(listener => listener());

        return action;
    }

    const subscribe = (listener) => {
        listeners = [...listeners, listener];

        return () => {
            listeners = listeners.filter(l => l !== listener);
        };
    }

    const observable = () => {
        const listenerSubscribe = subscribe;

        return {
            subscribe: (observer) => {
                function observeState() {
                    observer.next(getState());
                }

                observeState();

                const unsubscribe = listenerSubscribe(observeState)
                return {
                    unsubscribe
                }
            }
        }
    }

    const replaceReducer = (newReducer) => {
        reducer = newReducer;

        dispatch({ type: '@@redux/REPLACE' });
    }

    dispatch({ type: '@@redux/INIT'})

    return { getState, dispatch, subscribe, [Symbol.observable]: observable, replaceReducer }
}

An now we can use our middlewares:

import loggerMiddleware from './loggerMiddleware';
import { createStore, applyMiddleware } from 'redux';
import userReducer from './userReducer';

const store = createStore(userReducer, applyMiddleware(loggerMiddleware));

Conclusion

We just see how is developed Redux, its implementation is quite simple to understand and so powerful to use, although it's now possible to use the React context :)

We also see how to use it a little bit, including with ReactiveX.

In the near future we will how is implemented the library which bind Redux with React named react-redux.

11-28-2020

Rainbow Apps

Get it on Google Play

© Copyright 2021 Rainbow Apps. All rights reserved.

Rainbow Apps

© Copyright 2021 Rainbow Apps.
All rights reserved.

Get Numbersion app

Get it on Google Play

Follow me