Demystifying Redux pt2

September 12, 2016

Redux is a predictable state container for JavaScript apps. It borrow ideas from both the flux architecture and the elm architecture.

This article, is part of a series of posts aiming to explain the redux architecture and its internals by reimplementing parts of it (Disclaimer: naive/not-optimized implementation ahead). As we demystified createStore in the previous post, this time we’ll take a look at : applyMiddleware


applyMiddleware - express-like middleware for Redux

Earlier we saw that createStore returns an object, holding access to the state of our app, a getState method to retrieve it, a dispatch method responsible for changing our state by dispatching actions to reducers, and a subscribe mechanism allowing to be notified once an action has been dispatched.

Let’s see again how the dispatch logic work:

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

The dispatch method has access to the state before and after state reduction happens. If you have some knowledge of express.js (& Co., like Koa or Hapi) you might see where the middleware idea come from. In fact, this is the only part of Redux, that you won’t find in the Elm architecture. Redux allows you via applyMiddleware to enhance the store.dispatch logic.

applyMiddleware API

As we did with createStore, lets’ start from the API of applyMiddleware. We want applyMiddleware to take the middleware as the argument. It should return a function that we could call on createStore, that will return a new createStore function, with the middleware applied. In a sense, we want to configure how we create stores, by enhancing our createStore capabilities trough the middleware. Or simply put: hijack the dispatch method of createStore with an enhanced one, AKA: applying a middleware to our store.

function applyMiddleware(middleware) {
  return createStore =>
    (reducer, initialState) => {
     
     // return enhanced store
    }
}

To use it, simply:

const createEnhancedStore = applyMiddleware(middleware)(createStore)
const store = createEnhancedStore(reducer, initialState)

Hijacking the dispatch method

In order to be able to hijack the dispatcher we first need to save the original dispatch method. As we are doing this, let also keep a record to the getState method, as we want to be able to inject both into our middleware.

function applyMiddleware(middleware) {
  return createStore =>
    (reducer, initialState) => {
      const store = createStore(reducer, initialState); // <-
      const dispatch = store.dispatch; // <-
      const getState = store.getState; // <-
    }
}

We can now inject those method into our middleware:

function applyMiddleware(middleware) {
  return createStore =>
    (reducer, initialState) => {
      const store = createStore(reducer, initialState);
      const dispatch = store.dispatch;
      const getState = store.getState;

      const injectedMIddleware = middleware({ dispatch, getState }); // <-
      const enhancedDispatch = injectedMIddleware(dispatch); // <-
    }
}

and finally we can return the new enhanced store with the hijacked dispatcher:

function applyMiddleware(middleware) {
  return createStore =>
    (reducer, initialState) => {
       const store = createStore(reducer, initialState);
       const dispatch = store.dispatch;
       const getState = store.getState;

      const injectedMIddleware = middleware({ dispatch, getState });
      const enhancedDispatch = injectedMIddleware(dispatch);

      return Object.assign({}, store,{ dispatch:enhancedDispatch }); // <-
    }
}

And that’s it! Let’s just stop for a moment and reason about what we just did:

applyMiddleware = middleware -> createStore -> (reducer, initialState) => store

Before putting the pieces together, lets see how a middleware will look like.

Anatomy of a redux-middleware

Again, let start by the API. We saw that in order to inject the original dispatch and getState method we call our middleware passing those methods via an optionHash parameter:

injectedMiddleware({dispatch, getstate})

This should return a function that we can compose with our dispatcher in a chain-able way. Let’s see as an example a loggingMiddleware that will :

  • Log the previous state before every action is dispatched
  • Log the action type being dispatched
  • Log the state after the action has been dispatched

function createLoggingMiddleware({ getState }) {
  return dispatch =>
    action => {
      const previousState = getState();
      const dispatched = dispatch(action);
      const currentState = getState();

      console.log(`Previous state: ${previousState}`);
      console.log(`Action dispatched: ${action.type}`);
      console.log(`Current state: ${currentState}`);
      console.log("=======================")

      return dispatched;
    };
}

Putting the pieces together:

Lets import our createStore together with our counter reducer from the previous post, together with our freshly coded applyMiddleware and createLoggingMiddleware:

import createStore from ./naive-redux/createStore
import applyMiddleware from ./naive-redux/applyMiddleware
import counter from ./reducers
import createLoggingMiddleware from ./middlewares/loggingMiddleware

We can now wire all the pieces together and see that even without subscribing to our store in order to log its state after each action being dispatched, out store will automatically log previous and current state along with the type of action dispatched… powaaa!

const createEnhancedStore = applyMiddleware(createLoggingMiddleware)(createStore);
const store = createEnhancedStore(counter)

store.dispatch({ type: 'INCREMENT' })
// Previous state: 0
// Action dispatched: INCREMENT
// Current state: 1
// =======================

store.dispatch({ type: 'INCREMENT' })
// Previous state: 1
// Action dispatched: INCREMENT
// Current state: 2
// =======================

store.dispatch({ type: 'DECREMENT' })
// Previous state: 2
// Action dispatched: DECREMENT
// Current state: 1
// =======================

Conclusion and extras

The above implementation doesn’t allow us to apply multiple middlewares if you want to add that feature take a look at the beautiful : compose helper that comes with Redux.

If you want to play around with the code above, feel free to fork my redux-playground repository.

Previous related post:

comments powered by Disqus