Premise: The company is compatible with two technology stacks Vue and React. Vue is more studied, but React is still in the official document checking stage. Recently, it just maintained a React project, which used Redux, so as to review Redux

Redux

Redux:Redux is a predictable JavaScript state management container.

The three principles

Understanding Redux depends on these three principles

Single data source

The state of the entire application is stored in an object tree that exists in only one store.

State is read-only

The only way to change state is to trigger an action, which is a generic object that describes events that have occurred.

store.dispatch({

  type'COMPLETE_TODO'.

  index1

})

Copy the code

Use pure functions to perform modifications

To describe how an action changes the State Tree, you need to write reducers. Action describes the modification operation, while actually doing the modification state is reducers

function reducer(state = [], action{

  switch (action.type) {

    case 'ADD_TODO':

      return [

. state,

        {

          text: action.text,

          completedfalse

        }

      ]

    case 'COMPLETE_TODO':

      return state.map((todo, index) = > {

        if (index === action.index) {

          return Object.assign({}, todo, {

            completedtrue

          })

        }

        return todo

      })

    default:

      return state

  }

}

Copy the code

Through the above analysis of 👆, we have obtained three key information points: State, Action and reducers

concept

Before reviewing workflow, let’s clear up a few concepts. The following concepts are from the glossary, which I have copied for ease of reading

State

State(also known as a State tree) is a broad concept, but in the Redux API it usually refers to a unique State value, managed by the Store and obtained by the getState() method. It represents the entire state of a Redux application, usually as a multi-tiered nested object.

conventional

The top-level state can be either an object or a set of key-values like a Map, or it can be any data type. However, you should make sure that the state can be serialized as much as possible, and don’t throw anything in that makes it impossible to easily convert the state to JSON.

Action

Action is a generic object that represents the intention to change state. It’s the only way to get data into the Store. Data, whether it is retrieved from UI events, network callbacks, or other data sources such as Websockets, will eventually be dispatched as actions.

conventional

Actions must have a type field that specifies the action type to be executed. Type can be defined as a constant and imported from other modules. Instead of using Symbols for type, String is a better method because String can be serialized.

Reducer

The Reducer (also known as Reducing Function) function takes two parameters: the result of the previous cumulative operation and the current cumulative value, and returns a new cumulative result. This function merges a set into a single value.

In Redux, the cumulative result is the state object, and the cumulative value is action. Reducer calculates a new state from the last result state and the current actions that have been accumulated. These reducers must be pure functions and return the same results when the inputs are the same. They should not cause any side effects. That’s what makes cool features like thermal reloading and time travel possible.

The dispatch function

Dispatching Function (or dispatch function in short) is a function that receives actions or asynchronous actions. The function is dispatching either one or more actions to the Store, or no actions at all.

Action Creator

Action Creator is simple, just a function that creates an Action. Don’t confuse action with Action Creator. Action is a payload of information, and Action Creator is a factory for creating actions.

Asynchronous Action

The asynchronous action is a value sent to the Dispatching function, but this value cannot be consumed by the Reducer yet. Middleware converts asynchronous actions into one or a group of actions before sending them to the Base Dispatch () function. Asynchronous actions can have many types, depending on which middleware you use. It is usually an asynchronous native data type such as Promise or Thunk, and while data is not immediately passed to the Reducer, a distribution event for the action is triggered once the operation is complete.

Middleware

Middleware is a high-order function that combines dispatch functions and returns a new Dispatch function, usually converting asynchronous actions into actions.

❗️❗️ this is also the focus of our next analysis

Finally, a memory brain map is drawn

Redux workflow

Now that you understand the three principles and concepts, take a look at the Redux workflow

The picture below makes sense

We know that Redux is a one-way data flow, so let’s design our Redux directory structure based on what we learned above

As shown above, the Redux directory structure for most companies should look something like this. We need the actionCreators file to create our action. Action objects must have a type field, and reducer triggers corresponding operations based on different types, so create actionTypes file, and then deal with reducer to modify our state, so create reducer file 📃, so I understand the above concept. It helps us understand the directory structure, rather than just following somebody else’s directory structure, at least understand why it’s divided like this, right

Compose aggregation function

For the middleware principle, we will implement the compose function. It is very helpful to understand the middleware principle

dispatch=fn1(fn2(fn3))

dispatch=compose(fn1,fn2,fn3)

Copy the code

We expect to have an aggregative method, for example, compose, which takes the first parameter, fn3, as a parameter to the second parameter, fn2, and the result of the run as a parameter to the third parameter, fn1, recursively, from right to left, and returns a new function based on the base function f3. Given all the higher-order functions,🤔 wonders: Suppose the F3 parameter is replaced by a dispatch function? Now let’s move on to the implementation of the compose function

// Compose aggregate functions are ordered from right to left

const compose = function (. funcs{

    return funcs.reduce((a, b) = > {

        return (. args) = > {

           returna(b(... args))

        }

    })

}

Copy the code

These series functions are not elegant. ES6 arrow function short for a more comfortable look

 function compose(. funcs{

  return funcs.reduce((a, b) = >(... args) => a(b(... args)))

}

Copy the code

So compose(Fn1, Fn2, FN3) (… Args is equivalent to FN1 (FN2 (FN3 (… args)))

Redux Middleware principles

After making clear the basic content of 👆 above, we come to the key point of this question, that is, what is Redux middleware used for, what is the principle of ❓

After the previous analysis of Redux workflow, Reducer immediately calculated the State, which is a synchronous process, then think about how to support asynchronous operation, not only support asynchronous operation, but also support error handling, log monitoring, so which link of the Redux workflow to intercept operation ❓, The answer is the Dispatch process, which dispatches the action for interception processing

In Redux, the method associated with the middleware implementation is applyMiddleware, so let’s take a look at this method (here I provide a debug middleware code, click to the repository)

/ / call applyMiddleware

applyMiddleware(thunk, logger)



export default function applyMiddleware(. middlewares{

  return createStore= >(... args) => {

    conststore = createStore(... args)

    let dispatch = (a)= > {

      throw new Error(

        'Dispatching while constructing your middleware is not allowed. ' +

        'Other middleware would not be applied to this dispatch.'

      )

    }



    const middlewareAPI = {

      getState: store.getState,

      dispatch(. args) = > {

        returndispatch(... args)

      }

    }

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

dispatch = compose(... chain)(store.dispatch)

    return {

. store,

      dispatch

    }

  }

}

Copy the code

You can see that dispatch = compose(… In order to support middleware, Redux internally rewrote the original dispatch method of Chain (Store. dispatch), and finally passed the original Store. dispatch method, that is, the original disptach method was also treated as middleware

From the analysis, we can know that the incoming middleware is aggregated by the compose function to rewrite the Dispatch method, so we can also understand that the middleware is an enhancement to the Dispatch method, for example, to enhance the dispatch function’s processing of actions as functions and promises

For example, 🌰

Change into this code in the warehouse

function logger(store{

    return function wrapDispatchToAddLogging(next{

        return function dispatchAndLog(action{

            let result = next(action)

            return result

        }

    }

}



function thunk({ dispatch, getState }{

    return function wrapDispatchToThunk(next{

        return function dispatchThunk(action{

            if (typeof action === 'function') {

                return action(dispatch, getState);

            }

            return next(action);

        }

    }

}



applyMiddleware(thunk, logger)  WrapDispatchToThunk (wrapDispatchToAddLogging(dispatch))

Copy the code

Print Dispatch method

dispatchThunk(action) {

            if (typeof action === 'function') {

                return action(dispatch, getState);

            }

            return next(action);

    }

Copy the code

Transformation next

/ / conversion next

dispatchThunk(action) {

            if (typeof action === 'function') {

                return action(dispatch, getState);

            }

            // next(action)

            // The logger method returns the dispatchAndLog function

            return (function dispatchAndLog(action{

            let result = next(action)

            return result

        })(action)

   }   

Copy the code

Go ahead and convert next

We see next in the dispatchAndLog function, so let’s go ahead and convert

/ / conversion next

dispatchThunk(action) {

            if (typeof action === 'function') {

                return action(dispatch, getState);

            }

            // next(action)

            // The logger method returns the dispatchAndLog function

            return (function dispatchAndLog(action{

            // let result = next(action)

           // Dispatch is the original dispacth

            let result=(function(action){dispatch(action)})(action)             

            return result

        })(action)

   }   

Copy the code

The compose method combines the new Middlewares with the store.dispatch method. Generate a new Dispatch method, and by rewriting the Dispatch method, you can determine that the Redux middleware is also based on the Onion model

The execution order of the middleware

The execution sequence follows the Onion model

  applyMiddleware( 

The logger,

    thunk

  )

Copy the code

How to write Redux middleware

Work we understand the case of Redux workflow, in fact, the most interference may be Redux middleware, commonly used redux-Thunk, Redux-SOga, Redux-Promise and so on, so it is very important to master the principle of middleware, so how to write a middleware ❓, We continue to analyze the applyMiddleware method

export default function applyMiddleware(. middlewares{

  return createStore= >(... args) => {

    conststore = createStore(... args)

    let dispatch = (a)= > {

      throw new Error(

        'Dispatching while constructing your middleware is not allowed. ' +

        'Other middleware would not be applied to this dispatch.'

      )

    }



    const middlewareAPI = {

      getState: store.getState,

      dispatch(. args) = > {

        returndispatch(... args)

      }

    }

    // A layer of middleware is executed to receive {store.getState,dispatch}

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

    // compose(... Chain (store.dispatch) equivalent to FN1 (Fn2 (Fn3 (store.dispatch))

    // Another layer of middleware is executed. This layer receives the next parameter, which is the next middleware parameter

dispatch = compose(... chain)(store.dispatch)

    return {

. store,

      dispatch

    }

  }

}

Copy the code

From the above analysis at 👆, the basic form (structure) of a Redux middleware is as follows

// The middleware logic code needs to be currified three times

store => next= > action => {

  // Middleware logic code

}

Copy the code

Redux supports asynchronous operations

  • strengtheningDispacth functionSo that it can parseAction is a functionTo allow Redux to support asynchronous operations
  • strengtheningDispacth functionSo that it can parseAction is in the form of a Promise objectTo allow Redux to support asynchronous operations
redux-thunk

According to this middleware structure we will analyze the source code of redux-Thunk middleware

function createThunkMiddleware(extraArgument{

  return ({ dispatch, getState }) = > (next) => (action) = > {

    // if thunk is used

    if (typeof action === 'function') {

      return action(dispatch, getState, extraArgument);

    }

    // If you can't handle anything else, pass it to the next middleware

    return next(action);

  };

}

const thunk = createThunkMiddleware();

Copy the code

Uh huh… ,redux-thunk just a few lines of source code to support asynchronous redux operations

redux-promise

usage

  const testRes = (a)= > {

      return new Promise((res, rej) = > {

          res({

              type'TEST_RESOLVE'

          })

      });

  }

  store.dispatch(testRes());

Copy the code
// Redux-PROMISE simple source code

const vanillaPromise = store= > next => action= > {

  // Decide that this is not a Promise object and hand it to the next middleware

  if (typeofaction.then ! = ='function') {

    return next(action)

  }

  // Action is a Promise object, which the Promise middleware can handle

  // Execute store. Dispatch --> (... args) => dispatch(... args)



  return Promise.resolve(action).then(store.dispatch)

}

Copy the code

Reinventing our Redux project

In a word, Redux is too cumbersome to use. When I started to write the React project during my internship last year, I just wrote it in the style of others. Until I maintained my colleague’s React project, I suddenly realized that the bad Redux writing method and the abuse of Redux in the project led to the disaster level

And then I realized if I could double encapsulate Redux, I could simplify it

Such as:

import { createModel } from ".. /.. /.. /model.js";



const model = {

  namespace'counter'.

  state: {

    count10

  },

  reducer: {

    add(state: any, action: any) { // counter/add

      state.count += 1

    },

    minus(state: any, action: any) {

      state.count--

    },

  }

}



export default createModel(model)

Copy the code

Enennene… Finally, I chose Remacth program. This library is also very friendly and compatible with the old writing method. Specifically, I can take a closer look at this document, and I will not do the analysis here