As a matter of fact, the author did not have a writing plan related to Redux, but a colleague in the company recently shared the technology related to Redux, and the author undertook part of the task of reviewing the articles. In the process of review, the author spent considerable time looking up materials and implementing codes, and accumulated several thousand words of notes. Redux also had a new insight, so I wrote this article, hoping to bring you some inspiration and thinking Thanks to (· ω·) Blue Through the study of this article, readers should be able to learn and understand:

  1. Design idea and realization principle of REdux

  2. React-redux design and implementation principle

  3. Design idea and realization principle of REdux middleware


Implementation of Redux

Before we get started, we need to answer one question: Why do we need Redux, and what problem does Redux solve for us? Only by answering this question can we grasp redux’s design philosophy.

React as a component-based development framework, there is a lot of communication between components, sometimes across multiple components, or multiple components share a set of data. Simple data transfer between parent and child components does not meet our requirements. Naturally, we need a place to access and manipulate these common states. Redux provides us with a way to manage public state, and our subsequent design implementations will be built around this requirement.

Let’s think about how to manage the public state: since it’s the public state, let’s just extract the public state. We create a store.js file and store the common state directly in it. Other components simply import the store to access the common state.

const state = {    
    count: 0
}Copy the code

We store a public state count in store that the component can manipulate after importing store. This is the most direct store, and of course our store can’t be designed this way, for two main reasons:

1. Easy to misoperate

For example, someone accidentally assigned a value of {} to store, emptied the store, or mistakenly modified the data of other components, which is obviously not safe, and it is difficult to troubleshoot errors. Therefore, we need to operate store conditionally to prevent users from directly modifying the data of store.

2. Poor readability

JS is a language that relies heavily on semantics. If you change a common state in your code without commenting it, it will be difficult for others to maintain the code. In order to figure out what changing state means, you have to infer from the context, so it is best to give each operation a name.

Project handover

Us to think about how to design the state of public managers, according to our analysis of the above, we hope that can either be global access to public status, is the private cannot be directly modified, think about it, exactly the closure is just these two requirements, so we will put the public state designed to closure (understanding closures have difficulty of classmate can also skip the closure, This does not affect subsequent understanding)

Since we want to access the state, we must have getters and setters, and when the state changes, we must broadcast to the component that the state has changed. This corresponds to the three REdux apis: getState, Dispatch, subscribe. We can outline the store’s shape with a few lines of code:

export const createStore = (a)= > {    
    let currentState = {}       // Public state
    function getState() {}      // getter    
    function dispatch() {}      // setter    
    function subscribe() {}     // Publish a subscription
    return { getState, dispatch, subscribe }
}Copy the code

1. The getState implementation

The implementation of getState() is very simple and simply returns the current state:

export const createStore = (a)= > {    
    let currentState = {}       // Public state
    function getState() {       // getter        
        return currentState    
    }    
    function dispatch() {}      // setter    
    function subscribe() {}     // Publish a subscription
    return { getState, dispatch, subscribe }
}Copy the code

2. The dispatch

But with dispatch(), we need to think about it. From the above analysis, our goal is to conditionally and anonymously modify store data, so how do we achieve both? We already know that when we use dispatch, we pass an action object to Dispatch () that contains the state we want to change and the name of the operation (actionType). Depending on the type, store changes the corresponding state. We also use this design here:

export const createStore = (a)= > {    
    let currentState = {}    
    function getState() {        
        return currentState    
    }    
    function dispatch(action) {        
        switch (action.type) {            
            case 'plus': currentState = { ... state,count: currentState.count + 1}}}function subscribe() {}    
    return { getState, subscribe, dispatch }
}Copy the code

We wrote the judgment of actionType in dispatch, which was very bloated and clumsy. Therefore, we thought of pulling out the rules for modifying state in this part and putting them outside, which is the reducer we are familiar with. Let’s modify the code to let reducer pass in from outside:

import { reducer } from './reducer'
export const createStore = (reducer) = > {    
    let currentState = {}     
    function getState() {        
        return currentState    
    }    
    function dispatch(action) {         
        currentState = reducer(currentState, action)  
    }    
    function subscribe() {}    
    return { getState, dispatch, subscribe }
}Copy the code

We then create a reducer. Js file and write our reducer

//reducer.js
const initialState = {    
    count: 0
}
export function reducer(state = initialState, action) {    
    switch(action.type) {      
        case 'plus':        
        return {            
            ...state,                    
            count: state.count + 1        
        }      
        case 'subtract':        
        return {            
            ...state,            
            count: state.count - 1        
        }      
        default:        
        return initialState    
    }
}Copy the code

Leaving the code here, we can verify getState and Dispatch:

//store.js
import { reducer } from './reducer'
export const createStore = (reducer) = > {    
    let currentState = {}        
    function getState() {                
        return currentState        
    }        
    function dispatch(action) {                
        currentState = reducer(currentState, action)  
    }        
    function subscribe() {}        
    return { getState, subscribe, dispatch }
}

const store = createStore(reducer)  / / create a store
store.dispatch({ type: 'plus' })    // Perform the addition operation and increment count by 1
console.log(store.getState())       / / for the stateCopy the code


Running the code, we’ll see that the printed state is: {count: NaN}, this is because the initial store data is empty, state.count +1 is actually underfind+1, and outputs NaN, so we need to initialize the store data first, and we are executing the dispatch({type: The actionType of this dispatch can be filled as desired, as long as it does not duplicate the existing type and the reducer switch can go to default to initialize the store:

import { reducer } from './reducer'
export const createStore = (reducer) = > {        
    let currentState = {}        
    function getState() {                
        return currentState        
    }        
    function dispatch(action) {                
        currentState = reducer(currentState, action)        
    }        
    function subscribe() {}    
    dispatch({ type: '@@REDUX_INIT' })  // Initialize store data
    return { getState, subscribe, dispatch }
}

const store = createStore(reducer)      / / create a store
store.dispatch({ type: 'plus' })        // Perform the addition operation and increment count by 1
console.log(store.getState())           / / for the stateCopy the code


Run the code and we will print the correct state: {count: 1}



3. The subscribe implementation

Although we have access to public state, store changes do not directly cause view updates. We need to listen for store changes. Here we apply a design pattern, the observer pattern, which is widely used to listen for event implementations. But I personally think it’s more accurate to call it the observer model, and there’s a lot of discussion about the difference between an observer and a published-subscription model, so check it out.)

The concept of the observer mode is also simple: the observer monitors the changes of the observed, and notifies all the observers when the observed changes. For those who are not familiar with the observer mode implementation, we will first skip Redux and write a simple observer mode implementation code:

/ / observer
class Observer {    
    constructor (fn) {      
        this.update = fn    
    }
}
// Observed
class Subject {    
    constructor() {        
        this.observers = []          // Observer queue
    }    
    addObserver(observer) {          
        this.observers.push(observer)// Add an observer to the observer queue
    }    
    notify() {                       // Notifies all observers that the observer update() is executed
        this.observers.forEach(observer= > {        
            observer.update()            // Take the observer in turn and execute the observer update method}}})var subject = new Subject()       // Observed
const update = (a)= > {console.log('Notified by the observed')}  // The method to execute when a broadcast is received
var ob1 = new Observer(update)    // Observer 1
var ob2 = new Observer(update)    // Observer 2
subject.addObserver(ob1)          // Observer 1 subscribes to subject notifications
subject.addObserver(ob2)          // Observer 2 subscribes to subject notifications
subject.notify()                  // Issue a broadcast to execute the update method for all observersCopy the code

To explain the above code: The observer object has an update method (the method to execute when notified), and we want to execute this method when notified by the observed object; The observed has the addObserver and notify methods. The addObserver is used to collect the observers’ update methods into a queue, and when notify is executed, all the observers’ update methods are removed from the queue and executed. In this way, the function of notification is realized. We subscribe to redux’s listening-notification function as well:

The implementation of SUBSCRIBE should be easy to understand given the observer pattern example above, where we combine dispatch and notify. Each time we dispatch, we broadcast a change in the state of the store.

import { reducer } from './reducer'
export const createStore = (reducer) = > {        
    let currentState = {}        
    let observers = []             // Observer queue
    function getState() {                
        return currentState        
    }        
    function dispatch(action) {                
        currentState = reducer(currentState, action)                
        observers.forEach(fn= > fn())        
    }        
    function subscribe(fn) {                
        observers.push(fn)        
    }        
    dispatch({ type: '@@REDUX_INIT' })  // Initialize store data
    return { getState, subscribe, dispatch }
}Copy the code


Let’s try this subscribe (instead of creating a component and adding a store to it) :

import { reducer } from './reducer'
export const createStore = (reducer) = > {        
    let currentState = {}        
    let observers = []             // Observer queue
    function getState() {                
        return currentState        
    }        
    function dispatch(action) {                
        currentState = reducer(currentState, action)                
        observers.forEach(fn= > fn())        
    }        
    function subscribe(fn) {                
        observers.push(fn)        
    }            
    dispatch({ type: '@@REDUX_INIT' })  // Initialize store data
    return { getState, subscribe, dispatch }
}

const store = createStore(reducer)       / / create a store
store.subscribe((a)= > { console.log('Component 1 receives notification from store') })
store.subscribe((a)= > { console.log('Component 2 receives notification from store') })
store.dispatch({ type: 'plus' })         // Execute dispatch to trigger notification of storeCopy the code


The console successfully prints the execution result of the callback passed in store.subscribe() :



At this point, a simple Redux is complete, with input verification and other details added to the real redux source code, but the general idea is basically the same as above.

We can already introduce stores to the component for state access and subscription store changes, and count it, exactly ten lines of code (‘ ∀´) ψ. But a glance at the progress bar on the right shows that things are not simple, and we are only a third of the way through. Even though we’ve already implemented Redux, the coder is not satisfied with that, and when we use store, we need to introduce store, then getState, then Dispatch, and subscribe into each component, so the code is redundant, and we need to incorporate some repetitive operations, One way to simplify mergers is known as react-Redux.


Implementation of React-redux

As mentioned above, if a component wants to access public state from a store, it needs to perform four steps: Import introduces store, getState obtains state, Dispatch changes state, subscribe updates. The code is relatively redundant. We want to merge some repeated operations, and React-Redux provides a solution to merge operations: The react-Redux API provides both Provider and connect. Provider puts store in this. Context, omits import, and connect merges getState and dispatch into this. And automatically subscribes to updates, simplifying the other three steps. Let’s look at how these two apis are implemented:


1. The Provider implementation

We’ll start with a simpler Provider. A Provider is a component that takes a store and puts it into a global context. We’ll see why we put it in context later when we implement Connect. Now we create the Provider component and put a store in the context. There are some conventions when using the Context API (see this article for context usage)

import React from 'react'
import PropTypes from 'prop-types'
export class Provider extends React.Component {  
    // Static childContextTypes is declared to specify the properties of the context object
    static childContextTypes = {    
        store: PropTypes.object  
    } 

    // Implement the getChildContext method, which returns the context object, also fixed
    getChildContext() {    
        return { store: this.store }  
    }  

    constructor(props, context) {    
        super(props, context)    
        this.store = props.store  
    }  

    // Render the provider-wrapped component
    render() {    
        return this.props.children  
    }
}Copy the code

Once the Provider is complete, we can fetch store from the component in the form this.context.store, without importing store separately.


2. Connect to achieve

Let’s think about how to implement CONNECT. Let’s review how to use Connect:

connect(mapStateToProps, mapDispatchToProps)(App)Copy the code

We already know that connect takes two methods, mapStateToProps and mapDispatchToProps, and returns a higher-order function that takes a component, / / connect/plugins/plugins/plugins/plugins/plugins/plugins/plugins/plugins/plugins/plugins/plugins

export function connect(mapStateToProps, mapDispatchToProps) {    
    return function(Component) {      
        class Connect extends React.Component {        
            componentDidMount() {          
                // Get the store from context and subscribe to updates
                this.context.store.subscribe(this.handleStoreChange.bind(this));        
            }       
            handleStoreChange() {          
                // Trigger the update
                // There are several ways to trigger the update. For brevity, forceUpdate forces the update directly. Readers can also trigger child component updates via setState
                this.forceUpdate()        
            }        
            render() {          
                return (            
                    <Component// Passed to the componentpropsTo be,connectThis higher-order component returns the original component as is.. this.props} / / according tomapStateToPropsthestatehangthis.props{on. mapStateToProps(this.context.store.getState()} // According tomapDispatchToPropsthedispatch(action) onthis.props{on. mapDispatchToProps(this.context.store.dispatch) }                 
                    />ContextTypes = {store: proptypes. object} return Connect}}Copy the code

Having written the connect code, we need to explain two things:

1. Provider: When we look at the connect code, context is just a way for connect to obtain store. We can import store in connect instead of context. So what is the meaning of the existence of Provider, in fact, the author also thought for a while, and then remembered… React-redux connect is a wrapper that only provides an API, so you need to pass the Provider into the store.

2. Decorator pattern in CONNECT: Review how CONNECT is called: Connect (mapStateToProps, mapDispatchToProps)(App) Connect (mapStateToProps, mapDispatchToProps)(App) Why is react-Redux designed this way? As a widely used module, react-Redux must have its own meaning.

In fact, connect design is the implementation of the decorator pattern, the so-called decorator pattern is simply a wrapper on the class, dynamically expand the function of the class. Connect and HoC in React are implementations of this pattern. There is also a more immediate reason: the design is compatible with ES7 decorators, which allows us to simplify the code by using @connect. See react-Redux for the use of @connect decorators:

Class App extends React.Component{render() {return <div>hello</div>
    }
}
function mapStateToProps(state){
    return state.main
}
function mapDispatchToProps(dispatch){
    return bindActionCreators(action,dispatch)
}
export default connect(mapStateToProps,mapDispatchToProps)(App)Copy the code

@connect(state=>state.main, dispatch=>bindActionCreators(action,dispatch)
)
class App extends React.Component{
    render() {return <div>hello</div>
    }
}Copy the code


React-redux: React-redux: React-redux Create a project using create-react-app, delete useless files, and create store. Js, reducer. Js, react-redux.js to write our redux and react-redux code. In app.js, we simply write a counter, click the button to send a dispatch, make the count in store increment by one, and display the count on the page. The final file directory and code are as follows:



// store.js
export const createStore = (reducer) = > {    
    let currentState = {}    
    let observers = []             // Observer queue
    function getState() {        
        return currentState    
    }    
    function dispatch(action) {        
        currentState = reducer(currentState, action)       
        observers.forEach(fn= > fn())    
    }    
    function subscribe(fn) {        
        observers.push(fn)    
    }    
    dispatch({ type: '@@REDUX_INIT' }) // Initialize store data
    return { getState, subscribe, dispatch }
}Copy the code

//reducer.js
const initialState = {    
    count: 0
}

export function reducer(state = initialState, action) {    
    switch(action.type) {      
        case 'plus':        
        return {            
            ...state,            
            count: state.count + 1        
        }      
        case 'subtract':        
        return {            
            ...state,            
            count: state.count - 1        
        }      
        default:        
        return initialState    
    }
}Copy the code

//react-redux.js
import React from 'react'
import PropTypes from 'prop-types'
export class Provider extends React.Component {  
    // Static childContextTypes is declared to specify the properties of the context object
    static childContextTypes = {    
        store: PropTypes.object  
    }  

    // Implement the getChildContext method, which returns the context object, also fixed
    getChildContext() {    
        return { store: this.store }  
    }  

    constructor(props, context) {    
        super(props, context)    
        this.store = props.store  
    }  

    // Render the provider-wrapped component
    render() {    
        return this.props.children  
    }
}

export function connect(mapStateToProps, mapDispatchToProps) {    
    return function(Component) {      
    class Connect extends React.Component {        
        componentDidMount() {          // Get the store from context and subscribe to updates
            this.context.store.subscribe(this.handleStoreChange.bind(this));        
        }        
        handleStoreChange() {          
            // Trigger the update
            // There are several ways to trigger the update. For brevity, forceUpdate forces the update directly. Readers can also trigger child component updates via setState
            this.forceUpdate()        
        }        
        render() {          
            return (            
                <Component// Passed to the componentpropsTo be,connectThis higher-order component returns the original component as is.. this.props} / / according tomapStateToPropsthestatehangthis.props{on. mapStateToProps(this.context.store.getState()} // According tomapDispatchToPropsthedispatch(action) onthis.props{on. mapDispatchToProps(this.context.store.dispatch) }             
                />ContextTypes = {store: proptypes. object} return Connect}}Copy the code

//index.js
import React from 'react'
import ReactDOM from 'react-dom'
import App from './App'
import { Provider } from './react-redux'
import { createStore } from './store'
import { reducer } from './reducer'

ReactDOM.render(   
    <Provider store={createStore(reducer)}>        
        <App />    
    </Provider>.document.getElementById('root'));Copy the code

//App.js
import React from 'react'
import { connect } from './react-redux'

const addCountAction = {  
    type: 'plus'
}

const mapStateToProps = state= > {  
    return {      
        count: state.count  
    }
}

const mapDispatchToProps = dispatch= > {  
    return {      
        addCount: (a)= > {          
            dispatch(addCountAction)      
        }  
    }
}

class App extends React.Component {  
    render() {    
        return (      
            <div className="App">        
                { this.props.count }        
                <button onClick={() = >This. Props. AddCount ()} ></button>      
            </div>); }}export default connect(mapStateToProps, mapDispatchToProps)(App)Copy the code


Run the project, click the add button, and count correctly. OK, the entire redux and React-Redux process is complete




Redux Middleware implementation

While both Redux and React-Redux implementations are relatively simple, let’s look at the slightly more difficult implementation of redux middleware. The so-called middleware can be understood as the interceptor, which is used for intercepting and processing certain processes, and the middleware can be used in series. In REDUx, our middleware intercepts the process of dispatch submission to reducer to enhance the function of Dispatch.

I looked at a lot of information about Redux middleware, but found none more clear than the official documentation, which explained every step of the middleware from requirements to design, from concept to implementation. The following is a step-by-step analysis of the design and implementation of the Redux middleware, using a logging middleware as an example.

Let’s think about what we would do if we wanted to print the contents of the store after each dispatch:

1. Manually print store contents after each dispatch

store.dispatch({ type: 'plus' })
console.log('next state', store.getState())Copy the code

This is the most straightforward approach, but of course we can’t paste a log at the end of every dispatch in the project, we should at least extract this functionality.


2. Package dispatch

function dispatchAndLog(store, action) {    
    store.dispatch(action)    
    console.log('next state', store.getState())
}Copy the code

We can repackage a common new dispatch method to reduce some of the duplicate code. However, every time you use this new dispatch, you have to invoke it from the outside, which is still a hassle.


3. Replace the dispatch

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

If we just replaced the dispatch, wouldn’t we need to reference it externally every time we used it? For simply printing logs, this is enough, but if we also have a need to monitor dispatch errors, we can certainly add a code to catch errors after the log printing code, but with the increase of function modules, the code quantity will expand rapidly, and the middleware will not be able to maintain in the future. We want the different functions to be separate pluggable modules.


4. The modular

// Prints the logging middleware
function patchStoreToAddLogging(store) {    
    let next = store.dispatch    // We can also write anonymous functions here
    store.dispatch = function dispatchAndLog(action) {      
        let result = next(action)      
        console.log('next state', store.getState())      
        return result    
    }
}  

// Monitor error middleware
function patchStoreToAddCrashReporting(store) {    
    // The dispatches fetched here are already dispatches wrapped by the previous middleware, thus enabling middleware concatenation
    let next = store.dispatch    
    store.dispatch = function dispatchAndReportErrors(action) {        
        try {            
            return next(action)        
        } catch (err) {            
            console.error('Catch an exception! ', err)            
            throw err        
        }    
    }
}Copy the code

We split the modules of different functions into different methods, and realized the chain call by retrieving the store.dispatch wrapped by the previous middleware in the method. We can then use and combine the middleware separately by calling these middleware methods.

patchStoreToAddLogging(store)
patchStoreToAddCrashReporting(store)Copy the code

We’ve basically implemented composable, pluggable middleware at this point, but we can still make the code look a little nicer. We notice that the middleware methods we are currently writing fetch the Dispatch and then replace the Dispatch within the method, and we can simplify this repeating code a little bit: instead of replacing the Dispatch within the method, we return a new Dispatch and let the loop do the replacement at each step.


5. applyMiddleware

Modify the middleware so that it returns the new dispatch instead of replacing the old dispatch

function logger(store) {    
    let next = store.dispatch     
 
    // What we did before (replacing dispatch directly within the method):
    // store.dispatch = function dispatchAndLog(action) {    
    / /...
    // }    
  
    return function dispatchAndLog(action) {        
        let result = next(action)        
        console.log('next state', store.getState())        
        return result    
    }
}Copy the code

Add a helper method, applyMiddleware, to Redux to add middleware

function applyMiddleware(store, middlewares) {    
    middlewares = [ ...middlewares ]    // Make a shallow copy of the array to avoid reserve() below affecting the original array
    middlewares.reverse()               // Since the front middleware is in the innermost layer when the loop replaces the dispatch, the array needs to be flipped to ensure the order of the middleware calls
    // Loop to replace dispatch
    middlewares.forEach(middleware= >      
        store.dispatch = middleware(store)    
    )
}Copy the code

We can then add middleware in this form:

applyMiddleware(store, [ logger, crashReporter ])Copy the code


With that said, we can simply test the middleware. I created three middleware, namely logger1, Thunk, and Logger2. Its function is also very simple: print Logger1 -> execute asynchronous dispatch -> print Logger2. We observe the execution order of middleware through this example

//index.js
import React from 'react';
import ReactDOM from 'react-dom';
import App from './App';
import { Provider } from './react-redux'
import { createStore } from './store'
import { reducer } from './reducer'

let store = createStore(reducer)

function logger(store) {    
    let next = store.dispatch    
    return (action) = > {        
        console.log('logger1')        
        let result = next(action)        
        return result    
    }
}

function thunk(store) {    
    let next = store.dispatch    
    return (action) = > {        
        console.log('thunk')        
        return typeof action === 'function' ? action(store.dispatch) : next(action)    
    }
}

function logger2(store) {    
    let next = store.dispatch        
    return (action) = > {        
        console.log('logger2')        
        let result = next(action)        
        return result    
    }
}

function applyMiddleware(store, middlewares) {    
    middlewares = [ ...middlewares ]      
    middlewares.reverse()     
    middlewares.forEach(middleware= >      
        store.dispatch = middleware(store)    
    )
}

applyMiddleware(store, [ logger, thunk, logger2 ])

ReactDOM.render(    
    <Provider store={store}>        
        <App />    
    </Provider>.document.getElementById('root'));Copy the code


Sending asynchronous Dispatches

function addCountAction(dispatch) {  
    setTimeout((a)= > {    
        dispatch({ type: 'plus'})},1000)
}

dispatch(addCountAction)Copy the code


The output



Logger1 -> thunk -> Logger2. Logger1 -> thunk -> Logger2 At this point, we’ve basically implemented a pluggable, composable middleware mechanism, along with redux-Thunk.


6. Pure functions

The previous example has basically met our needs, but we can still improve it. The above function still looks not “pure”. The function inside the function changes the store’s own dispatch, causing the so-called “side effect”. Taking a page from the React-Redux implementation, we can use applyMiddleware as a higher-order function that enhances store, rather than replacing dispatch:

By making a small modification of the createStore, by highlighting tener (applyMiddleware), in which the createStore is received and reinforced.

// store.js
export const createStore = (reducer, heightener) = > {    
    // Heightener is a high-order function used to enhance the createStore
    // If heightener is present, perform the enhanced createStore
    if (heightener) {        
        return heightener(createStore)(reducer)    
    }        
    let currentState = {}    
    let observers = []             // Observer queue
    function getState() {        
        return currentState    
    }    
    function dispatch(action) {        
        currentState = reducer(currentState, action);        
        observers.forEach(fn= > fn())    
    }    
    function subscribe(fn) {        
        observers.push(fn)    
    }    
    dispatch({ type: '@@REDUX_INIT' })// Initialize store data
    return { getState, subscribe, dispatch }
}Copy the code


The middleware is further currified and next is passed in as a parameter

const logger = store= > next => action= > {    
    console.log('log1')    
    let result = next(action)    
    return result
}

const thunk = store= > next =>action= > {
    console.log('thunk')    
    const { dispatch, getState } = store    
    return typeof action === 'function' ? action(store.dispatch) : next(action)
}

const logger2 = store= > next => action= > {    
    console.log('log2')    
    let result = next(action)    
    return result
}Copy the code


Transform applyMiddleware

const applyMiddleware = (. middlewares) = > createStore => reducer= > {    
    const store = createStore(reducer)    
    let { getState, dispatch } = store    
    const params = {      
        getState,      
        dispatch: (action) = > dispatch(action)      
        // Why not dispatch directly: dispatch
        // Because using dispatch directly will generate a closure, which will cause all middleware to share the same dispatch. If one of the middleware modiizes the dispatch or performs asynchronous dispatch, an error may occur
    }    

    const middlewareArr = middlewares.map(middleware= >middleware(params)) dispatch = compose(... middlewareArr)(dispatch)return { ...store, dispatch }
}

The compose step, which corresponds to middlewares.reverse(), is a common combinational method for functional programming
function compose(. fns) {
    if (fns.length === 0) return arg= > arg    
    if (fns.length === 1) return fns[0]    
    return fns.reduce((res, cur) = >(. args) = >res(cur(... args))) }Copy the code

The code should not be hard to understand, and we have made two major changes based on the previous example

1. Middlewares.reverse () is replaced by the compose method, which is a common way of composing functions in functional programming. arg) => mid1(mid2(mid3(… Arg)))

2. Instead of replacing Dispatch directly, createStore is enhanced as a higher-order function, which returns a new store


7. Onion rings

The onion ring model is mentioned later because the onion ring is not closely related to the implementation of the middleware above, so it is mentioned here to avoid confusion. We simply put out three middleware pieces that print logs and watch the output, making it easy to read the onion ring model.

const logger1 = store= > next => action= > {    
    console.log('enter the log1')    
    let result = next(action)    
    console.log('leave log1')    
    return result
}

const logger2 = store= > next => action= > {    
    console.log('enter the log2')    
    let result = next(action)    
    console.log('leave log2')    
    return result
}

const logger3 = store= > next => action= > {    
    console.log('enter log3')    
    let result = next(action)    
    console.log('leave log3')    
    return result
}Copy the code


The execution result



Since our middleware is structured like this:

logger1(    
    console.log('enter logger1')    
        logger2(        
            console.log('enter logger2')        
                logger3(            
                    console.log('enter logger3')            
                    //dispatch()            
                    console.log('leave logger3'))console.log('leave logger2'))console.log('leave logger1'))Copy the code

So we can see that the middleware execution order actually looks like this:

Log in to log1 and run next. Log in to log2 and run next. Log in to log3 and run next. Go back to the middleware log1 and execute the statement after next for log1 -> leave log1

This is called the onion ring model.





Iv. Summary & Acknowledgements

The redux, React-Redux, and REdux middleware implementations are not complicated, with only a dozen lines of core code each. But between these lines, It contains a series of programming ideas and design paradigms — observer pattern, decorator pattern, middleware mechanism, function currization, and functional programming. The meaning of our reading source code is to understand and experience these ideas.

The whole article has been written for a month. The main reference materials are shared by colleagues and many related articles. Here, I would like to express my special thanks to Longchao and Yu Zhong for their sharing. In the process of the details of the examination, also got a lot of never met friends to solve their doubts, especially thanks to Frank1e big man in the middleware of corrified understanding to give help. Blue Blue thank you all