Code Odyssey: Redux

Redux is a javascript state machine framework that focusing on immutable data and functional mutations. It is created by Dan Abramov.
It is getting popular in React world and I really like the simplicity of this framework. So again I am starting another code walkthough for this framework since my last ‘Code Odyssey’ article 2 years ago, hope it can helps you guys understand the implementation of this framework better.

createStore - the basic and everything

createStore function is the entry point of Redux framework, basically is creates a “store” with immutable state and combine “reducers” to mutate the state, here’s the usage:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26

import {createStore} from 'redux'

reducer = (state=0, action) => {
if(action.type == 'increment') {
return state + 1
} else if(action.type == 'decrement') {
return state - 1
} else {
return state
}
}

const store = createStore(reducer)

store.subscribe(() => {
console.log(store.getState())
})

console.log(store.getState()) // 0

store.dispatch({ type: 'increment' })
// 1

store.dispatch({ type: 'decrement' })
// 0

The above example creates a store that holds one number as state, accept increment and decrement as action in reducer and with one listener that listen for the state change.
When the store created, it will first run an INIT action in reducer, so we need to have an initalize state in reducer, or pass inital state to the ‘createStore’ function

Now let’s start with the source code:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39

createStore = (reducer, preloadedState, enhancer) {
var currentReducer = reducer
var currentState = preloadedState
var currentListeners = []
var nextListeners = currentListeners

function getState() {
return currentState
}


function subscribe(listener) {

nextListeners.push(listener)

return function unsubscribe() { // ... unsubscribe listener }
}

function dispatch(action) {
currentState = currentReducer(currentState, action)

var listeners = currentListeners = nextListeners
for (var i = 0; i < listeners.length; i++) {
var listener = listeners[i]
listener()
}

return action
}

dispatch({ type: ActionTypes.INIT })

return {
dispatch,
subscribe,
getState
}
}

This is a simplified version of the source code, but the core logic is basically the same.
A store has current state and reducer. When dispatch, the reducer replace the original state and then call the listeners.
It is really amazing that such a simple core can simplify complex front end logic a lot.

combineReducer - combine branch reducers

combineReducer, is the first thing people might get confuse about Redux, but the concept is actually pretty simple.
The function takes keys and reducers, then combine them into a single reducer function that, every reducer return part of the state under their key name.
For example:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
function postReducer(state=[], action) {
if (action.type == 'RECEIVE_POST') {
return [...state, action.post]
}

return state
}

function userReducer(state={}, action) { return state }

var combinedReducer = combineReducer({
posts: postReducer,
user: userReducer
})

var store = createStore(combinedReducer)

store.dispatch({
type: 'RECEIVE_POST',
post: { title: 'foo', content: 'bar' }
})
store.getState()
// {
// posts: [{ title: 'foo', content: 'bar' }],
// user: {}
// }

Above code creates a combined reducer function, each reducers return part of the state under their key name.
postReducer update the state under key ‘posts’, ‘userReducer’ update the state under key name ‘user’ and have no access to other part of the state.

Let’s look at the source code:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
function combineReducers(reducers) {
...
var reducerKeys = Object.keys(reducers)

return function combination(state={}, action) {
...
var nextState = {}
for (var i = 0; i < reducerKeys.length; i++) {
var key = reducerKeys[i]
var reducer = reducers[i]
var previousStateForKey = state[key]
nextState[key] = nextStateForKey
}

return nextState
}
}

So after combine, when the dispatch get called, it will trigger all reducers and combine the new state to a hash object, then return as the new state.
This is pretty handy when we want to store all front end data into a single store.

bindActionCreators

bindActionCreators is a syntax sugar, which helps you have a function that wrap action and store.dispatch together.

The source and example:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
function bindActionCreator(actionCreator, dispatch) {
return (...args) => dispatch(actionCreator(...args))
}

//example

var selectPost = (id) => {
return {
id,
type: 'SELECT_POST'
}
}

selectPost = bindActionCreator(selectPost, store.dispatch)

selectPost(1)
// equal to: dispatch({ id: 1, type: 'SELECT_POST'})

compose, applyMiddleware - middleware magic

The most complicated part of Redux is their middleware. Redux middleware works as chain functions, you can pass multiple middlewares to store and dispatch function will trigger middlewares one by one until end of the chain.

Before going into source code, there is another function called compose get used in the applyMiddleware function.
The compose function can receive an array of functions and returns a new function that applies return value to next function in chain, for Example:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
a = (n) => n + 1
b = (n) => n + 2
c = (n) => n + 3

a(b(c(0))) // 6

// equal to

compose(a, b, c)(0) // 6

// source:

function compose(...funcs) {
const last = funcs[funcs.length - 1]
const rest = funcs.slice(0, -1)

return (...args) => rest.reduceRight(
(composed, f) => f(composed),
last(...args)
)
}

Next, lasts see the usage of Redux middleware, for example, we want to add a “thunk” middleware which let user can pass a function that returns action object, instead of action object.
It enables Redux to do async functions like web request more easily.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19

var middlewares = [thunk]

var createStoreWithMiddleware = applyMiddleware(middlewares)(createStore)

var store = createStoreWithMiddleware(reducer)

// middleware
function thunk({dispatch, getState}) {
return function(next) {
return function(action) {
if (typeof action == 'function') {
return action(dispatch, getState)
}

return next(action)
}
}
}

Basically, middleware will wrap the dispatch function, so everytime when we call dispatch, it will go over the middleware functions and call the next function in middleware chain until the original dispatch been called.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
function applyMiddleware(...middlewares) {
return (createStore) => {
return (reducer, preloadedState) => {
var store = createStore(reducer, preloadedState)
var dispatch = store.dispatch
var chain = []

// inject getState and dispatch to all middlewares
var middlewareAPI = {
getState: store.getState,
dispatch: (action) => dispatch(action)
}

chain = middlewares.map(middleware => middleware(middlewreAPI))
dispatch = compose(...chain)(dispatch)
// chain = [thunk, logger, report]
// => dispatch = thunk(logger(report(dispatch)))

return {
...store,
dispatch
}
}
}
}

conclusion

Redux is just a simple store for state, but what make it differents is the concepts of centralized store, immutable state and pure function reducers.
I like those concepts and how well it get integrated with React, The React Redux package do a great job to integrate Redux to React. And the source code is pretty neat too. Highly recommend to read after this.

Comments