What is middleware?

The data management process in REDUx is one-way, that is to say, it is a one-way road from action distribution to publish and subscribe triggering rendering. If you want to add or change some logic in the middle, you need to find action or Reducer to modify, is there a more convenient way?

Redux’s process:

Button – Trigger Event -> Dispath – Dispatch action -> Reducer – Publish Subscription -> View

Middleware, on the other hand, is a pluggable mechanism. If you want to extend something, such as adding logs and printing state before and after updates, you can simply install the logging middleware on Redux and remove it when you don’t want to use it.

Use of middleware

Say usage first, can use only, say principle again.

redux-logger

Redux provides several off-the-shelf middleware, such as the logging middleware mentioned above, which can be used by installing it:

npm i --save redux-logger
Copy the code

The Redux package provides a way to load middleware: applyMiddleware

When creating the Store object, you can pass in a second argument, which is middleware:

import { createStore, applyMiddleware } from "redux";
import { reducer } from "./reducer";
import ReduxLogger from "redux-logger";
// Use applyMiddleware to load middleware
let store = createStore(reducer, applyMiddleware(ReduxLogger));
Copy the code

This prints the update log on the console each time state is updated:

redux-thunk

The Redux-Thunk middleware supports asynchronous actions.

Loading middleware:

import reduxThunk from "redux-thunk";
let store = createStore(reducer, applyMiddleware(reduxThunk));
Copy the code

When loaded with the Redux-Thunk middleware, the action function can support returning a function that encapsulates the asynchronous operation:

function add(payload) {
    return function(dispatch, getState) {
      setTimeout((a)= > {
        dispatch({ type: ADD, payload });
      }, 2000); // The execution delay is 2 seconds
    };
}
Copy the code

As you can see, the action returns a function with the parameters Dispatch and getState as methods provided by Redux, which gives us the right to use the two functions until the asynchronous operation is complete to finish writing the asynchronous action.

redux-promise

With the Redux-Thunk middleware we could write asynchronous actions, but we wanted to take it a step further and have asynchronous actions support Promises, where the Redux-Promise middleware would come in useful.

Again, install redux-Promise and load it:

import reduxPromise from "redux-promise";
let store = createStore(
  reducer,
  applyMiddleware(reduxPromise)
);
Copy the code

The redux-Promise middleware supports the payload returned by the action as a promise:

let action = {
  add: function(payload) {
    return {
      type: ADD,
      // Payload is a Promise object that encapsulates asynchronous operations
      payload: new Promise((resolve, reject) = > {
        setTimeout((a)= > {
          resolve(payload); // If the command is successfully executed, send the parameters to the reducer
        }, 1000); })}; },minus: function(payload) {
    return {
      type: MINUS,
      payload: new Promise((resolve, reject) = > {
        setTimeout((a)= > {
          reject(payload); // Failed to execute the reducer
        }, 1000); })}; }};Copy the code

As you can see, payload no longer returns a parameter directly. Instead, it is a Promise object that you can encapsulate your asynchronous code into.

Attention! If you use the Redux-Promise middleware, the payload is a fixed parameter

Such as:

{
  type: MINUS,
  num: new Promise((resolve, reject) = > {
    / /...})};// The redux-promise parameter is num. Payload must be called if redux-promise is used
Copy the code

Principles of Middleware

Through the above three middleware, we can know their usage. They all extend some functions before and after state update. Then what is their principle?

Taking the first middleware, Redux-Logger, for example, where logs are printed before and after state updates, rewriting the store.dispatch() method is a solution:

lettemp = store.dispatch; // Store the old dispatch method store.dispatch =function(action) {
  console.log("Old state:", store.getState()); temp(action); // Execute the original dispatch method console.log("New state:", store.getState());
};
Copy the code

As can be seen, the original dispatch method is temporarily saved to the variable first, and the existing dispatch method is rewritten to increase the log output function. Output before the state is updated, and then call the temporary dispatch to update the state. This is equivalent to implementing the Redux-Logger middleware.

This is gross, but this is the principle of the Redux middleware: suspend the old Dispatch method, modify the Dispatch extension and return.

A generic way of writing middleware

The principle is clear, but it is too much trouble to override dispath manually every time. Is there a general way to write it? Apparently there is.

The redux source code uses higher-order functions to implement a piece of middleware, and its method signature looks like this:


let middleware = store= > next => action= > {
    // Specific middleware logic...
};
Copy the code

As you can see, the arrow function is elegantly written as a three-level nested function, that is, a higher-order function, whose final return value is still a method, which is the “dispatch” method that finally “extends” the functionality.

Hard to understand? We can write it as a normal function to make it easier to see the logic:

function middleware(store) {
  // Next is the old dispatch method
  return function(next) {
    // Action is the action object passed into the dispatcher
    return function(action) {
      // The middleware logic is written here...
    };
  };
}
Copy the code

That is, middleware is to rewrite the original dispath method, so we can think about, to extend the original Dispath all need what? It should be the following:

  • Store repository object (the store object is needed to override the previous dispath method)
  • Dispatch method (previous Dispatch)
  • Action object (Action object required for dispatching actions)

The above three objects are essential, as you can see they are the parameters of the three-level function. The first level store argument is actually the return value of createStore(), which is the repository; The next argument in the second layer is the original Dispatch method; The innermost function argument is the Action object.

Knowing the structure of the method signature, we can write our own middleware for Redux-Logger:

export function reduxLogger(store) {
  // Next is the old dispatch method
  return function(next) {
    // Action is the action object passed into the dispatcher
    return function(action) {
      console.log("Before Update :", store.getState());
      next(action);
      console.log("After update :", store.getState());
    };
  };
}
Copy the code

Simply print the state of state before and after next().

ApplyMiddleware method

We wrote a piece of middleware by hand and loaded it as needed. In Redux we provide an applyMiddleware method to load middleware:

applyMiddleware(reduxLogger);
Copy the code

The middleware can be loaded by passing in the required middleware in turn. So how does it work? Have a look, too.

ApplyMiddleware’s method signature is still a three-tier, higher-order function,

let applyMiddleware = middlewares= > createStore => reducer= > {
  // Load middleware logic...
};
Copy the code

Again, let’s rewrite it as a normal function to analyze:

function applyMiddleware(middlewares) {
  CreateStore is the method provided by Redux
  return function(createStore) {
    Reducer is the reducer function passed in to update state
    return function(reducer) {
      // Load middleware logic...
    };
  };
}
Copy the code

For example, in the process of applying middleware, the purpose is to overwrite the original store.dispatch with the new dispath method of the middleware incoming from the outside. In this way, the dispatch method of the store object returned to the user has been extended by the middleware, for example, the log is printed here.

So what does applyMiddleware need?

  • Middleware that needs to be applied
  • CreateStore method (with which to createStore objects)
  • Reducer (Reducer parameters are required when creating store objects)

We can see that these are the arguments to the three-level function, so we can write applyMiddleware’s logic:

function applyMiddleware(middlewares) {
  return function(createStore) {
    return function(reducer) {
      let store = createStore(reducer); // Get the store object
      let dispatch = middlewares(store)(store.dispatch); // Get the new dispatch method
      return { ...store, dispatch }; // Overwrite the old store.dispatch with the new dispatch
    };
  };
}
Copy the code

The key is this statement:

let dispatch = middlewares(store)(store.dispatch);
Copy the code

This statement is the middleware rewritten dispatch method, remember the middleware signature? Take a look at it and you’ll see:

let middleware = store= > next => action= > {};
Copy the code

Middleware requires that the first parameter store object be passed in and created by createStore(Reducer). The middleware requires a second argument, next, which is the original dispatch, so store.dispatch is the dispatch of the original warehouse. The result is the new dispath method, which overwrites the dispatch on the original store object using the expansion operator and returns it.

At this point, all the principles of handwriting middleware and application middleware have been analyzed.