How Redux Thunk Works

How Redux Thunk Works

August 30, 2018

Redux thunk has been quite popular as its easiness to handle asynchronous action dispatch in redux store. Redux Saga is awesome, but sometimes, especially for a medium or small size project, there is no need for us to involve Saga as it requires more consideration and structuring. Moreover, for project in medium scale, it’s not worth involving Redux-Saga as learning curve for concepts and usage of generator function and side-effect is much more than Redux Thunk.

createStore in Redux

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
export default function createStore(reducer, preloadedState, enhancer) {
if (typeof preloadedState === 'function' && typeof enhancer === 'undefined') {
enhancer = preloadedState
preloadedState = undefined
}

if (typeof enhancer !== 'undefined') {
if (typeof enhancer !== 'function') {
throw new Error('Expected the enhancer to be a function.')
}

return enhancer(createStore)(reducer, preloadedState)
}

if (typeof reducer !== 'function') {
throw new Error('Expected the reducer to be a function.')
}

let currentReducer = reducer
let currentState = preloadedState
let currentListeners = []
let nextListeners = currentListeners
let isDispatching = false

function ensureCanMutateNextListeners() {
if (nextListeners === currentListeners) {
nextListeners = currentListeners.slice()
}
}

...
}

Let’s go to key point right now.

1
return enhancer(createStore)(reducer, preloadedState)

Hence, enhancer will be a higher order function. AndcreateStore, reducer and preloadState are passed as parameters.

When creating redux store

1
2
const thunk = createThunkMiddleware()
const store = createStore(reducer, applyMiddleware(thunk))

As ya can see, applyMiddleware(thunk) will executed like applyMiddleware(thunk)(createStore)(reducer, preloadedState).
So what exactly happens in applyMiddlewared? Go deeper.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// applyMiddleware from redux
export default function applyMiddleware(...middlewares) {
return (createStore) => (reducer, preloadedState, enhancer) => {
const store = createStore(reducer, preloadedState, enhancer)
let dispatch = store.dispatch
let chain = []

const middlewareAPI = {
getState: store.getState,
dispatch: (action) => dispatch(action)
}
chain = middlewares.map(middleware => middleware(middlewareAPI))
dispatch = compose(...chain)(store.dispatch)

return {
...store,
dispatch
}
}
}

Destructuring assignment tells us thunk will be passed to middlewares in applyMiddleware as an array.
After a few steps of execution, we go to the following line:

1
chain = middlewares.map(middleware => middleware(middlewareAPI))

middlewareAPI is an Object with getState and dispatch as its properties. Here, dispatch property is different from local variable dispatch. The local variable dispatch is the real dispatching action from redux-store which we can dispatch action and change the corresponding data structure in redux store. Yet, dispatch property refers to a function which accepts action as a parameter.


So one thing left.

const thunk = createThunkMiddleware()

thunk is the middleware we create and pass to applyMiddleware as a parameter.

1
2
3
4
5
6
7
8
9
10
11
function createThunkMiddleware(extraArgument) {
return ({ dispatch, getState }) => next => action => {
if (typeof action === 'function') {
return action(dispatch, getState, extraArgument);
}
return next(action);
};
}
const thunk = createThunkMiddleware();
thunk.withExtraArgument = createThunkMiddleware;
export default thunk;

See, it’s a functionality loaded of simplicity and elegance. thunk is a function accepts middlewareAPI as the result we have already got from our previous walking through on applyMiddleware. We have missed one important thing, but I don’t wanna put too much elaboration on that right now. Just long story short on the following line:

1
2
chain = middlewares.map(middleware => middleware(middlewareAPI))
dispatch = compose(...chain)(store.dispatch)

As we decorate our middlewares, we chain’em up by invoking compose function. compose is from redux/compose. The purpose of this functionality is to chain up all functions and execute them in sequence. But here, there is only one middleware, composing it up will only return this function.
After composing it, as we can see, store.dispatch will be passed as middleware’s parameter. store.dispatch is corresponding to next in createThunkMiddleware.

Alright, after the aforementioned process is done, we expose a function like:

1
2
3
4
5
6
(action) => {
if (typeof action === 'function') {
return action(dispatch, getState, extraArgument);
}
return next(action);
}

If action is like original Action Object, we directly dispatch them as usual.
If action turns out to be a function, dispatch, getState, extraArgument will be passed into functionality as its three parameters, and do the following steps. You can put your asynchronous logics into your self defined functionality, when your final result is returned from backend, you are allowed to dispatch your real action Object.

Nail it.