Redux’s actions and Reducer are complex enough, but now you need to understand Redux’s middleware as well. Why does Redux exist? Why does Redux’s middleware have so many layers of function returns? How exactly does Redux’s middleware work? Redux middleware goes from zero to “give up”.

There are only two reference websites for this article, the first being the official website of Redux. The thinking process of this article mostly refers to the official examples. There is also the classic middleware of Redux, which can be said that the middleware of Redux was created to implement it — Redux-Thunk.

Write in the front: this article is actually my understanding of the Redux middleware a thought process, in the middle can not help but come from my personal ridicule, everyone just look at Lele.

Why do we use middleware?

Why do we use middleware? That’s a good question! To answer this question, I now propose a requirement that all store.dispatches monitor state changes before and after dispatch. So what do we do? Console. log(store.getState()))

console.log('dispatching', action)
store.dispatch(getTodos({items:[]}))
console.log('next state', store.getState())
console.log('dispatching', action)
store.dispatch(getTodos({items:["aaa"]}))
console.log('next state', store.getState())
Copy the code

Yeah, we can do that. But if I exaggerate, and I have thousands of dispatches, console.log is going to have dispatch times 2. And then when we get to the point where we’re having a hard time, and the product goes live, we need to close all the breakpoints. Do we want to comment out one by one at this time?

No, I won’t. I might change my mistake. So let’s try this function out independently, so that we can achieve reuse. The common code is written to a method, and the changing arguments are extracted.

function dispatchAndLog(store, action) {
    console.log('dispatching', action)
    store.dispatch(action)
    console.log('next state', store.getState())
}
dispatchAndLog(store, getTodos({items:[]}))
dispatchAndLog(store, getTodos({items:["aaa"]}))
Copy the code

Wouldn’t it be a lot easier to annotate only two lines instead of multiplying with dispatch? But I think writing this way is not friendly to other partners, which is equivalent to writing a set of grammar by myself. It is better to use the official store.dispatch when the custom functions are executed together.

We can rewrite store.dispatch by assigning store.dispatch to Next, and then turn Diapatch into our custom function, which calls Next, the original Dispatch. This nicely rewrites Dispatch, keeping the original functionality and adding custom methods.

const next = store.dispatch
store.dispatch = function dispatchAndLog(action) {
  console.log('dispatching', action)
  let result = next(action)
  console.log('next state', store.getState())
  return result
}
Copy the code

This is when the prototype of Redux middleware appeared.

MiddleWare is a variation on the Dispatch method, a variation.

Implementation of multiple middleware

So let’s pretend that I don’t just need to monitor state, I might have other functions. And separate from the way you monitor state. I need multiple middleware, so how do I do that?

We could pass each variation store.dispatch to a new parameter, passed into the next variation, but with something like next1, next2… Does this go on and on?

const next = store.dispatch
const next1 = store.dispatch = function dispatchAndLog1(action) {
    console.log('dispatching', action)
    let result = next(action)
    console.log(result,'next state', store.getState())
    return result
}
const next2 = store.dispatch = function dispatchAndLog2(action) {
    console.log('dispatching1', action)
    let result = next1(action)
    console.log(result,'next state1', store.getState())
    returnresult } ... . .Copy the code

Isn’t that an ugly format? Let’s find a way to free up the next parameter. My idea is to write a compose that combines these methods and then return a variant of the Dispatch method.

const _dispatch=store.dispatch;
function compose() {return function(action){
        _dispatch(action)
    }
}
store.dispatch=compose(dispatchAndLog1,dispatchAndLog2)
Copy the code

Liberation of nested functions

Middlewares consists of several layers of compose, one function inside the other, so how do we get the method out of that?

function A() {function B() {function C() {}}}Copy the code

How can we avoid multi-layer nesting? It is possible to free up nesting by assigning a function to a parameter, but this is not practical because we need to create many parameters.

const CM=function C(){}
const BM=function B(){
    CM()
}
const AM=function A(){
    BM()
}
Copy the code

In order to avoid creating many unnecessary references, we can use the way of passing parameters to solve this problem, will function as a parameter to directly, it will have to pay attention to a problem, because we need to pass into function, but does not perform the function, so every function we shall return a function, is also create higher-order functions, such as are ready. Call execution starts from the outermost function.

function C() {return function(){}
}
function B(CM){
    return function(){
        CM()
    }
}
function A(BM){
    return function(){
        BM()
    }
}
Copy the code

This method is executed in A disgusting way, nesting A function behind A function, passing the function returned by C to B, passing the function returned by B to A, and finally executing (), layer by layer, without escaping callback hell.

let compose=A(B(C()))
compose()
Copy the code

Array.reduceCome on stage

At this point we can consider the array. reduce method and combine these functions together. We’ll start by creating an array, each function passing a next function, so we can execute the function layer by layer.

let array=Array(3)
array[0]=function(next){
    return function() {let res= next();
        return res
    }
}
array[1]=function(next){
    return function() {let res= next();
        return res
    }
}
array[2]=function(next){
    return function() {let res= next();
        return res
    }
}
Copy the code

Reduce just merges, not executes, notice, so we need to add a layer of return function operations before each execution. PrevFunction is a combination of the previous two functions. PrevFunction is valid only according to the format of the custom function. Otherwise only the first and second arrays will be executed, because the initial values are the result of their execution.

function dispatch(){
    console.log("dispatch")
    return "dispatch"
}
function compose(array){
    return array.reduce((prevFunction,currentFunction)=>{
        return function (next) {
            return prevFunction(currentFunction(next))
        }
    })
}
console.log(compose(array)(dispatch)());
Copy the code

Here I define a Dispatch as my initial next parameter, passed into the middleware set, and the function pushed first is executed last, so our Dispatch will be executed at the last level of function. Careful as you should have noticed. Each of my custom functions returns the value of next above. To return the value of dispatch. The result of the compose function execution is the value of dispatch. So we can get the value of the original store.dispatch. By the way, the original store.dispatch returns an action.

In accordance with the above ideas, we write down the compose function of the merged middleware. First, we send store.dispatch to _dispatch for standby, and then the first parameter of the higher-order function “compose” is the middleware, and the second layer is the initial next function, namely the original store.dispatch. We just pass in the copy _dispatch. Finally, transform store.dispatch.

const _dispatch=store.dispatch;
function compose() {let middlewares=Array(arguments.length).join(",").split(",")
    middlewares=middlewares.map((i,index)=>{
        return arguments[index];
    })
    return middlewares.reduce((prevFunction,currentFunction)=>{
        return function (next) {
            return prevFunction(currentFunction(next))
        }
    })
}
store.dispatch=compose(dispatchAndLog1,dispatchAndLog2)(_dispatch)
Copy the code

This allows us to call multiple middleware.

Integrated into thecreateStore

However, this is not the case with official middleware. I translated the official definition of applyMiddleware() as an enhancement to createStore called Enhance, or encapsulation. However, the following points need to be noted:

  • Custom middleware is availablecreateStorethedispatch(action)andgetState()Methods.
  • store.dispatch(action)When executed, the chain of middleware is also executed, that is, the bound middleware is executed.
  • The middleware executes only once and acts on thecreateStoreRather thancreateStoreObject returnedstore. In other wordsstoreAt the time of creation, the middleware is already executed.
  • applyMiddleware()To return acreateStoreThat is, after the transformationcreateStore

So let’s take a look at applyMiddleware(). First, how do you enhance createStore while maintaining functionality?

applyMiddleware()To return acreateStoreThat is, after the transformationcreateStore

function applyMiddlewareTest() {return (createStore)=>{
        return function (reducer) {
            return createStore(reducer)
        }
    }
}
Copy the code

This call to applyMiddlewareTest()(createStore)(Reducer) is not the same as createStore(reducer).

store.dispatch(action)When executed, the chain of middleware is also executed, that is, the bound middleware is executed.

Because we don’t control the amount of middleware. Therefore, we use arguments to get the array of middleware. After processing the array, we call the compose letter which we have already written to merge it and pass it to _Dispatch. Finally, we use object. assign to copy the store and the variable dispatch.

function applyMiddlewareTest() {let middlewares=Array(arguments.length).join(",").split(",")
    middlewares=middlewares.map((i,index)=>{
        return arguments[index];
    })
    return (createStore)=>{
        return function (reducer) {
            let store = createStore(reducer)
            let _dispatch=compose(middlewares)(store.dispatch)
            return Object.assign({},store,{
                dispatch:_dispatch
            })
        }
    }
}
Copy the code

Custom middleware is availablecreateStorethedispatch(action)andgetState()Methods.

The middleware we’re writing now doesn’t get dispatch(Action) and getState() from inside the function, so we need to write an extra layer of functions, passing in Dispatch (Action) and getState(). For brevity, we can pass in an object that contains the dispatch(action) and getState() methods

function dispatchAndLog2({dispatch,getState}){
    return function (next){
        return function (action) {
            console.log('dispatching1', action)
            let result = next1(action)
            console.log(result,'next state1', store.getState())
            return result
        }
    }
}
Copy the code

This function can be simplified as es6:

const dispatchAndLog2=({dispatch,getState})=>next=>action{
    ....
}
Copy the code

Appear! Three layers of functions, the first layer is for passing storesdispatch(action)andgetState()Method, the argument passed by the second layernextIs the next middleware to be executed, and the third layer is the function ontology, the parameters passedactionIn order to eventually pass todispatchAnd exist.

Going back to applyMiddlewareTest, the dispatch and getState required in middleware, we can add a few lines of code. Execute the first layer of middleware directly, passing in two methods. We need to pay attention to dispatches here because the dispatches we need to pass are mutated, not native. Therefore, we rewrite the method of dispatch so that when the middleware calls this method, it will be the mutant dispatch. Otherwise the dispatches executed in the middleware would not be able to execute the middleware.

function applyMiddlewareTest(){
    ...
    let _dispatch=store.dispatch
    let _getState=store.getState
    let chain = middlewares.map(function (middleware) {
        return middleware({
            dispatch:function dispatch() {
                return _dispatch.apply(undefined, arguments);
            },
            getState:_getState
        });
    });
    _dispatch=compose(chain)(store.dispatch)
    ....
}
Copy the code

The realization of the story – thunk

Finally, test whether a wave of self-written middleware is successful:

function logger({ getState }) {
    return function(next){
        return function(action){
            console.log('will dispatch', action)
            const returnValue = next(action)
            console.log('state after dispatch', getState())
            return returnValue
        }
    }
}
const ifActionIsFunction = {dispatch, getState} => next => Action => {if (typeof action === 'function') {// If it is a function, execute it and return it, and then execute it in the function, delaying the dispatch.return action(dispatch, getState);
    }else{
        let res=next(action)
        return res
    }
}
let store=applyMiddlewareTest(logger,ifActionIsFunction)(createStore)(rootReducer)
store.dispatch((dispatch,getState)=>{
    return new Promise((resolve,reject)=>{
        setTimeout(()=>{
            dispatch(getTodos({items:["aaaa"]})) console.log(getState()) resolve() },1000); })})Copy the code

The run is successful, and the middleware functionality I wrote here is to return the result of the execution of the function if the action is a function, and pass the dispatch and getState methods to the function. This allows you to call Dispatch from the Action function. You must have noticed that this is an implementation of asynchrony, the basic logic of redux-thunk. (Redux-Thunk is a reference.)

There’s a hidden feature here that I don’t know if you noticed, but I’m returning a promise, which means I can implement a chain call to THEN.

store.dispatch((dispatch,getState)=>{
    return new Promise((resolve,reject)=>{
        setTimeout(()=>{
            dispatch(getTodos({items:["aaaa"]}))
            console.log(getState())
            resolve("Did the then method call succeed?")
        },1000);
    })
}).then((data)=>{
    console.log(data)
})
Copy the code