Building Redux from scratch

Kai Guo
8 min readMay 27, 2018

--

A good way to learn how things work is to build it from scratch. Most of the time we are using a Javascript library as a black box. We only care about how to use the public APIs they provide without caring much about how it works. This tutorial will focus on unveiling the magic behind Redux, and walk you through how to write it from scratch. You might find it useful in the following aspects:

  1. Better understand how Redux works under the hood.
  2. Try to think from the author’s perspective. Maybe next time you will come up with your own library!
  3. Learn some new patterns to write code you have never done before.

If you don’t know what is Redux or never use it before, there are tons of tutorials you can learn. This tutorial assumes you already know basic knowledge of Redux.

Primary APIs

Before starting to write Redux, we need to figure out where to start coding! Let’s first summarize what we need to know to build Redux:

  1. Core concept of Redux is a predictable state container for Javascript apps.
  2. Store API: a store holds the whole state tree.
  • createStore(reducer, state, enhancer): create a Redux store
  • getState(): get the store state
  • dispatch(action): update the store state based on a new action
  • subscribe(listener): subscribe to the store update by adding a change listener
  • replaceReducer(nextReducer): an advanced API which we put aside in this tutorial

3. combineReducers(reducers object): combine multiple reducers into one

4. applyMiddleware(middlewares): return an enhancer for createStore to use

Apparently Redux is centered around the store. It makes sense to start building store APIs.

Store APIs

createStore

We know that createStore accepts reducer, state and enhancer as inputs, and output a store object. So we can first build a skeleton createStore:

function createStore(reducer, initialState, enhancer) {  const store = {};
const state = initialState;
const listeners = [];
store.getState = () => state;
store.dispatch = (action) => {};
store.subscribe = (listener) => listeners.push(listener);
return store;
}

Here we do not use store.state to save state. It makes sure the only way to get state from store is through the public API getState.

Dispatch

What dispatch does is to use reducer to get new state and notify each listener the state change. So dispatch can look like this:

store.dispatch = (action) => {
state = reducer(state, action);
listeners.forEach(listener => listener());
};

Subscribe

The current subscribe method allows us to add listener to the store. But it is impossible to remove the listener. We can define a unsubscribe method to achieve this:

store.unsubscribe = (listener) => {
const index = listeners.indexOf(listener);
if (index >= 0) {
listeners.splice(index, 1);
}
};

The Redux author Dan Abramov has a smart trick here. Instead of defining a new method, he makes unsubscribe method as the return value of subscribe.

store.subscribe = (listener) => {
listeners.push(listener);
return () => {
const index = listeners.indexOf(listener);
listeners.splice(index, 1);
};
}

Note that we don’t even need to check if listener exists in the listeners array because listener must already be pushed into listeners within the closure.

combineReducers

When an app is getting larger, the reducer function can be very long. It is a good practice to split the main reducer into many reducers. Each smaller reducer only handles a sub-tree of the state. Once each reducer completes its job and gets a new sub-state, we can compose all the sub-states and builds the entire new state tree. The combineReducers is just a utility function to save time for some boilerplate code. Its role is to replace the following snippet:

export default function todoApp(state = {}, action) {
return {
visibilityFilter: visibilityFilter(state.visibilityFilter, action),
todos: todos(state.todos, action)
}
}

with:

const todoApp = combineReducers({
visibilityFilter,
todos
})

The input of combineReducers is a plain object whose keys are the keys of state, and values are the name of reducers. The output is the complete reducer method. It should be easy to first set up the skeleton code:

function combineReducers(obj) {
return (state = {}, action) => {}
}

Then, we just need to loop through each key of obj and assign each key the corresponding reducer.

function combineReducers(obj) {  
return (state = {}, action) => {
const newState = {};
for (let key in obj) {
newState[key] = obj[key](state[key], action);
}
return newState;
}
}

The fun part finally comes! You might think Redux is really easy to understand and implement so far. But this section may change your mind. If you really understand how middleware works and can implement applyMiddleware, as a reward, you will have a much deeper understanding of the Redux ecosystem and why it is so easy to get extended.

Middleware provides a third-party extension point between dispatching an action, and the moment it reaches the reducer. People use Redux middleware for logging, crash reporting, talking to an asynchronous API, routing, and more.

Let’s start with a very simple logging middleware. It can log each dispatched action along with the state computed after it. How to achieve that?

The straightforward idea is to modify the dispatch method as below:

store.dispatch = (action) => {
console.log('action', action);
state = reducer(state, action);
console.log('state', state);
listeners.forEach(listener => listener());
};

It works, but adding irrelevant logging code into dispatch is a bad idea. We need a way to extract it from the core dispatch method. The second idea may look better which defines a wrapped dispatch method:

function loggingDispatch(store, action) {
console.log('action', action);
store.dispatch(action);
console.log('state', store.getState());
}

The wrapped method decorates dispatch by adding extra logging logic. But it is not convenient to define a new specific dispatch each time we need a new middleware. Let’s make another middleware filterDeleteDispatch to blacklist DELETE action:

function filterDeleteDispatch(store, action) {
if (action.type === 'DELETE') return;
store.dispatch(action);
}

We can manually merge those two middlewares into one by defining a new middleware loggingAndFilterDeleteDispatch. It is quite inflexible because user may want different combinations of different middlewares. In order to chain middlewares together, the input and output of each middleware should be the same type.

store.dispatch --> Middleware 1 --> dispatch v1 --> Middleware 2 --> dispatch v2

The whole system is like an onion. store.dispatch is the core of the onion. We then keep padding with different middlewares. So in Redux, the input and output of each middleware are dispatch like functions:

function middleware(dispatch) {
return function newDispatch(action) {
dispatch(action);
}
}

Let’s write our logging middleware based on the template above:

function logger(dispatch) {
return function newDispatch(action) {
console.log('action', action);
dispatch(action);
console.log('state', store.getState());
}
}

Wait?! Do we miss something in the code above? Where can we access the context store? The code above is the expected logging middleware we want to have. The trick is: if we know how the method looks like but it misses context, we can build a factory method to generate this method. In our case, we can build a createLoggerMiddleware method to generate this logger middleware.

function createLoggerMiddleware(store) {
return function logger(dispatch) {
return function newDispatch(action) {
console.log('action', action);
dispatch(action);
console.log('state', store.getState());
}
}
}

It looks so simple, right? What we just did is to create a wrapper on top of function logger with store passed in. Let’s do the same thing for filterDelete middleware as well:

function createFilterDeleteMiddleware(store) {
return function filterDelete(dispatch) {
return function newDispatch(action) {
if (action === 'DELETE') return;
dispatch(action);
}
}
}

Suppose we want to wrap store.dispatch with logger middleware first and then filterDelete middleware, we can do the following:

const logger = createLoggerMiddleware(store);
const filterDelete = createFilterDeleteMiddleware(store);
const newDispatch = filterDelete(logger(store.dispatch));

Apparently newDispatch contains the logic of both middlewares as well as the original store.dispatch. Is there a way to make the above code more generic? We can define a function decorateDispatch which takes store and a list of middleware factories, and then output a decorated dispatch.

function decorateDispatch(store, middlewareFactories) {
let dispatch = store.dispatch;
middlewareFactories.forEach(factory => {
dispatch = factory(store)(dispatch);
});
return dispatch;
}

Redux user should never worry about decorating dispatch. It needs to be done in the store initialization stage createStore based on the list of middlewares. So we can modify createStore so that it can take middleware factories as input.

function createStore(reducer, initialState, middlewareFactories=[]) {
const store = {};
const state = initialState;
const listeners = [];
store.getState = () => state;
store.dispatch = (action) => {
state = reducer(state, action);
listeners.forEach(listener => listener());
};
store.subscribe = (listener) => listeners.push(listener);
return store;
};
store.dispatch = decorateDispatch(store, middlewareFactories);
return store;
}

Extras

createStore API

The Redux author Dan Abramov defines a slightly different API for createStore. The major reason is to ensure store can only be decorated once.

function createStore(reducer, initialState, enhancer) {
if (typeof enhancer === 'function') {
return enhancer(createStore)(reducer, initialState);
}
...
}

enhancer takes createStore and outputs a decorated createStore. The idea is very similar to how the middleware is designed. In order to implement it, we can first build a skeleton code:

function enhancer(createStore) {
return function newCreateStore(reducer, initialState) {
createStore(reducer, initialState);
}
}

Again, the enhancer has no idea about middlewares. Remember the trick we mentioned before? We can reuse it here and create a enhancer factory that injects the middlewares as context:

function enhancerFactory(...middlewares) {
return function enhancer(createStore) {
return function newCreateStore(reducer, initialState) {
createStore(reducer, initialState);
}
}
}

Don’t feel surprised. enhanceFactory is just the same as the Redux API applyMiddleware! Let’s finish the rest of the code in applyMiddleware:

function applyMiddleware(...middlewareFactories) {
return function enhancer(createStore) {
return function newCreateStore(reducer, initialState) {
const store = createStore(reducer, initialState);
let dispatch = store.dispatch;
middlewareFactories.forEach(factory => {
dispatch = factory(store)(dispatch);
});
store.dispatch = dispatch;
return store;
}
}
}

Here we borrowed the logic from decorateDispatch to decorate dispatch. The implementation in Redux is slightly different but the core idea is the same.

Curry functions

Curry functions are usually hard to understand. But we already have curry functions in our code above, have you noticed?

function createLoggerMiddleware(store) {
return function logger(dispatch) {
return function newDispatch(action) {
console.log('action', action);
dispatch(action);
console.log('state', store.getState());
}
}
}

is the same as

const createLoggerMiddleware = store => dispatch => action => {
console.log('action', action);
dispatch(action);
console.log('state', store.getState());
}

applyMiddleware can also be written in this style:

const applyMiddleware = (...middlewareFactories) => createStore => (...args) => {
const store = createStore(...args);
let dispatch = store.dispatch;
middlewareFactories.forEach(factory => {
dispatch = factory(store)(dispatch);
});
store.dispatch = dispatch;
return store;
}

Redux-thunk middleware

With the knowledge about how middleware works, it is pretty easy to build the redux-thunk middleware which can handle the async request. From the logger middleware example, we can easily write something like this:

const createThunkMiddleware = store => dispatch => action => {
if (typeof action === 'function') {
return action(dispatch, store.getState());
}
return dispatch(action);
}

Wrap up

This tutorial is a walkthrough of building the main Redux APIs from scratch. Some implementation details are not the same as official Redux library, but the core idea is the same. Redux also includes a lot of error handling which I think is not important in this tutorial. The middleware section may not make sense on the first reading. That’s okay, it will make more sense with time.

If you found this article helpful, please tap the 👏.

--

--