preface

Personally, I don’t think React itself has a high learning curve, but when I learned Redux, I suddenly found many more concepts. For example, Action, ActionCreator, Dispatch, Reducer, and combineReducers.

When you’re done learning them, you suddenly discover something new, like React-Redux, which, as the name suggests, was the bridge between Redux and React. It includes the Provider component, the connect function, and the details of mapStateToProps, mapDispatchToProps, and so on.

Just when you think the end is near, there’s another big BOSS waiting for you: Redux sending asynchronous requests and middleware.

In order to clarify these concepts, this paper will be divided into the following modules:

Redux base paper

  • reduxIntroduction of basic concepts
  • reduxDesign ideas and core concepts

Armed with these basic concepts, we can start using it to complete a small project. Deepen your understanding by practicing.

Redux project in action

  • reduxImplementation counter
  • usereact-reduxImplementation counter
  • reduxAsynchronous scheme in
  • useredux-thunkThe middleware does asynchronous processing

Once this is done, the concept is clearer and Redux is easy to use.

Redux principle article

  • reduxRealize the principle of
  • redux middlewareRealize the principle of

After going through this step, you will be able to know what is and why.

Redux base paper

What is the story

It’s a state management container, where all the states are stored in one object.

Why use Redux

Redux is not a necessity, and many React projects the author worked on did not use Redux for state management.

When deciding whether to use Redux, let’s first look at their management state comparison diagrams:

Schematic diagram of not using Redux state management:

Schematic diagram of state management using Redux:

In contrast, you’ll find:

  • Don’t useRedux, the state is distributed in each component itself for management;
  • useRedux“, state is promoted to a global object for unified management, not managed in components. Each component can obtain global state.

That is, when the hierarchy is deep enough and the state operations complex enough, it is time to start thinking about using Redux for state management. Otherwise, there is no need to introduce additional complexity and learning costs.

Redux design ideas and core concepts

At first glance it looks a bit complicated, but use diagrams to block them out.

Store

A Store is a place where data is stored, you can think of it as a container. There can only be one Store for the entire app.

import { createStore } from 'redux';
import rootReducer from "./rootReducer";
const store = createStore(rootReducer);
Copy the code

The global Store object provides three things:

  • getState()A method to obtain global state;
  • dispatch(action)By passing inactionCall to the correspondingreducerTo changestate ;
  • subscribethroughreducerChanged thestateValue, the component must be notified for status updates, so the subscription center needs to be published.

Action

Action is an object that represents all actions that cause state changes.

export const toggleTodo = {
  type: 'TOGGLE_TODO'.id: 1
}
Copy the code

Pass its type into dispatch and finally decide which Reducer to execute to update the corresponding state.

ActionCreator

There are as many actions as there are messages that the View wants to send. It would be troublesome to write them all by hand. You can define a function to generate an Action. This type of function is called ActionCreator.

export const toggleTodo = id= > ({
  type: 'TOGGLE_TODO',
  id
})
Copy the code

Without reading too much into it, it is simply a function whose job, as its name suggests, is to generate Action.

dispatch() 

Store.dispatch () is the only way a View can issue an Action.

store.dispatch(toggleTodo(1))
Copy the code

Generate the action via toggleTodo and pass it into the Dispatch function. Its ultimate purpose is to remove the Reducer function execution.

Reducer

Reducer is a function that takes Action and the current state as parameters and returns a new state.

The most important feature of the Reducer function is that it is a pure function. In other words, as long as the same input, must get the same output.

const todos = (state = [], action) = > {
  switch (action.type) {
    case 'TOGGLE_TODO':
      return state.map(todo= >(todo.id === action.id) ? {... todo,completed: !todo.completed}
          : todo
      )
    default:
      return state
  }
}
export default todos
Copy the code

When an action of type = ‘TOGGLE_TODO’ is issued, code execution of the ‘TOGGLE_TODO’ branch of the TODOS function (Reducer) is triggered.

The state (which is the current state) is first iterated, and the data to which the incoming ID belongs is determined by judgment. And modify the property to return the new state.

So this is essentially just putting the logic of how to update the state right here.

[note]return {... todo, completed: ! todo.completed}Be familiar withES6If you’re in grammar, you’ll know that you’ve made a shallow copy and returned a brand new object. It is not a modification made on the basis of the original. This is areduxIt is very important that you never modify the original object.

subscribe()

The state data is now available for change, so we need a mechanism for the component that subscribed to the data to know that it has changed and to get the latest state, so a publish subscription must be implemented.

import { createStore } from 'redux';
const store = createStore(reducer);

store.subscribe(() = >{
  // When state changes, the change triggers a callback
});
Copy the code

The implementation, as we’ll talk about later, is a normal publish and subscribe model.

With all that said and just a few snippets of code shown, let’s take a full introduction to Redux and see how it all works.

import { createStore } from "redux";

const reducer = (state = {count: 0}, action) = > {
  switch (action.type){
    case 'INCREASE': return {count: state.count + 1};
    case 'DECREASE': return {count: state.count - 1};
    default: returnstate; }};const actions = {
  increase: () = > ({type: 'INCREASE'}),
  decrease: () = > ({type: 'DECREASE'})}const store = createStore(reducer);
Copy the code

Resolution:

  • createreducerPure function. The first argument is initializedstate 为 {count:0}, the second parameter receives oneaction ;
  • normalactionWill contain atypeValues, andpayloadIs the data passed in (this case is too simple to pass extra data);
  • createactionsObject, there are two of themActionCreate ;
  • At last,createStoreFunction, passed inreducerAs a parameter, generatedstoreObject.
store.subscribe(() = >
  console.log(store.getState())
);

store.dispatch(actions.increase());// Trigger an increment
store.dispatch(actions.decrease());// Trigger a reduction
Copy the code

Resolution:

  • First we subscribestoreObject, so whenstateWe can be notified when changes occur;
  • We’re throughstore.dispatchA variety of ways can be issuedactionAnd it’s understandable that I’m giving all kinds of commands to say what I’m going to do.

Its workflow looks something like this:

  1. First, the user sendsAction, issued bydispatchMethods;
  2. And then,StoreAutomatically callReducerAnd pass in two arguments: currentStateAnd receivedAction , ReducerWill return a newState ;
  3. StateAs soon as there is a change,StoreThe listener function is called.

At this point you should understand how Redux works.

Let’s take a look at React and see how it works.

Actual Combat Application

Practical application is divided into 4 small examples:

React uses redux to implement a counter. React uses redux to implement a counter

By comparing these two cases, understand why react-Redux is needed

Use redux-thunk to initiate asynchronous requests in React without using any middleware

Through the above two cases, first let us have the concept of middleware and learn how to use middleware. Finally, the realization principle of middleware will be explained in detail in the principle section.

Redux counter

To demonstrate the convenience of the code, the authors try to keep the components in a single file without splitting them.

First, create-react-app XXX quickly creates a project.

Index. Js code:

import React,{useState} from 'react';
import ReactDOM from 'react-dom';
import { createStore } from 'redux';

/ / {1}
const reducer = (state = {count: 0}, action) = > {
  switch (action.type){
    case 'INCREASE': return {count: state.count + 1};
    case 'DECREASE': return {count: state.count - 1};
    default: returnstate; }};/ / {2}
const actions = {
  increase: () = > ({type: 'INCREASE'}),
  decrease: () = > ({type: 'DECREASE'})}/ / {3}
const store = createStore(reducer);

function App() {
  const { getState, dispatch, subscribe } = store; / / {4}
  const [ count, setCount ] = useState(getState().count); / / {5}
  / / {6}
  const onIncrement = () = >{
    dispatch(actions.increase());
  }
  / / {7}
  const onDecrement = () = >{
    dispatch(actions.decrease());
  }
   / / {8}
  subscribe(() = >{
    setCount(getState().count);
  });

  return (
    <div>
      <h1>{count}</h1>
      <button onClick={onIncrement}>+</button>
      <button onClick={onDecrement}>-</button>
    </div>
  );
}

ReactDOM.render(
  <React.StrictMode>
    <App />
  </React.StrictMode>.document.getElementById('root'));Copy the code

Resolution:

  • {1} {2} {3} passescreateStoreA global is createdstoreObject;
  • {4} in the business componentApp, getstoreAll the capabilities that are available to us;
  • {5}useState  HookInitialize onecountThe initialized value is passedstore.getState().countTo obtain;
  • {6} {7} Create twodispatchMethods;
  • {8} The key point here is that after the user clicks on the page, it triggers the correspondingdispatchMethod to execute the correspondingreducerMethod, modify the state, here to listen to get the latest state, throughsetCountUpdates the latest status to the interface.

Here we have a very simple application, code hosting address >>> please click.

In this example, we import the global Store object and use it directly, assuming that the business is more complicated now, and we have many child components, grandchild components, then we might need to manually import the store for each component like this.

import { store,actions } from "./index.js";
Copy the code

You can’t say it doesn’t work, you can just say it’s not elegant and convenient. If you’ve learned React, the first thing that comes to mind is context. React-redux is the bridge between React and Redux, as the name suggests. Let’s look at its application logic in detail.

The react – redux counter

We implement the counter example above with react-redux.

Project directory structure

├ ─ ─ the SRC ├ ─ ─ actions. Js// Write the action file├ ─ ─ reducer. JsCompile the reducer file├ ─ ─ index. Js// Start file├ ─ ─ Count. Js// The component responsible for UI rendering└ ─ ─ CountContainer. Js// Container components are responsible for managing data and business logic
Copy the code

actions.js 

export const actions = {
  increase: () = > ({type: 'INCREASE'}),
  decrease: () = > ({type: 'DECREASE'})}Copy the code

reducer.js 

const counter = (state = { count: 0 }, action) = >{
  switch (action.type){
    case 'INCREASE': return {count: state.count + 1};
    case 'DECREASE': return {count: state.count - 1};
    default: returnstate; }}export default counter;
Copy the code

Normal reducer pure function, in the project, combineReducers are often used to merge multiple reducer, which is not the main role of this paper.

Count.js 

function Count(props){
  const { count, onIncrement, onDecrement } = props;
  return (
    <div>
      <h1>{count}</h1>
      <button onClick={onIncrement}>+</button>
      <button onClick={onDecrement}>-</button>
    </div>)}Copy the code

CountContainer.js 

import { actions } from "./actions";
import { connect } from 'react-redux'
import Count from "./Count";

function mapStateToProps(state) {
  return {
    count: state.count
  }
}

function mapDispatchToProps(dispatch) {
  return {
    onIncrement: () = > dispatch(actions.increase()),
    onDecrement: () = > dispatch(actions.decrease())
  }
}

export const CountContainer = connect(
  mapStateToProps,
  mapDispatchToProps
)(Count)
Copy the code

The core of react-Redux is in the Container component. If you look at the container component, it doesn’t render any elements. Instead, it wraps the Count component and returns a new one.

MapStateToProps and mapDispatchToProps are bound to the Count component using the connect function.

index.js 

import React from 'react';
import ReactDOM from 'react-dom';
import { createStore } from 'redux'
import { Provider } from 'react-redux'
import { CountContainer } from "./CountContainer";
import reducers from "./reducer";

const store = createStore(reducers);

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

In root level files, we do two things:

  1. Globally unique was createdstoreObject;
  2. For the incomingProvderComponent properties.

The final result is the same as the previous example. But how does it do it? Let’s look at how it works.

connect

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

Connect receives both methods mapStateToProps and mapDispatchToProps, and returns a higher-order function that receives a component and returns a new component.

Internal implementation of simplified CONNECT:

import React from 'react'
import { Context } from "./Provider";

export function connect(mapStateToProps, mapDispatchToProps) {
  return function(Component) {
    class Connect extends React.Component {
      componentDidMount() {
        // Subscribe to global state and update all components when it changes.
        this.context.subscribe(() = >this.forceUpdate());
      }
      render() {
        return (
          <Component{/ * passthroughprops* /} {. this.props} {/ * callsmapStateToProps, the return value is passed in as an attribute */} {. mapStateToProps(this.context.getState())} {/* callmapDispatchToProps, the return value is passed in as an attribute */} {. mapDispatchToProps(this.context.dispatch) }
          />
        )
      }
    }
    Connect.contextType = Context;
    return Connect
  }
}
Copy the code

To understand how it is written, use the connect source code:

export const CountContainer = connect(
  mapStateToProps,
  mapDispatchToProps
)(Count)
Copy the code

Is it suddenly clear?

The Provider component

It’s relatively simple, and what it does is it takes the store object and stores it into the context. Let’s look at the simplified implementation:

import React from 'react';
export const Context = React.createContext(null);

function Provider({ store, children }) {
  return <Context.Provider value={store}>{children}</Context.Provider>
}
Copy the code

React-redux is a bridge between React and Redux.

If you are careful, you will notice that all of the above cases are synchronous, but in real projects, when we trigger the increase or decrease button, we usually need to store the state in the database, so we have to call the background interface, which is the asynchronous process we are familiar with. Let’s look at how to make an asynchronous request in a component.

Asynchronous requests without middleware

Consider a simple scenario: Click the “Get Data” button to request the background interface to return the list. The loading state should be displayed at the beginning of the request, and the list should be displayed after the request is successful. If the request fails, a failure message will be displayed.

Code hosting address >>> Click to view

index.js 

import React from 'react';
import ReactDOM from 'react-dom';
import {App} from './App';
import { createStore } from "redux";
import { Provider } from 'react-redux'
/ / {1}
export const actionTypes = {
  FETCH_START:'FETCH_START'.FETCH_SUCCESS:'FETCH_SUCCESS'.FETCH_ERROR:'FETCH_ERROR'
}
/ / {2}
const initState = {
  isFetching: false.// Control the loading state
  newsList: [].// News list
  errorMsg:"" // Error message is displayed
}
/ / {3}
const reducer = (state= initState,action) = >{
  switch (action.type) {
    case actionTypes.FETCH_START:
      return{... state,isFetching:true};
    case actionTypes.FETCH_SUCCESS:
      return{... state,isFetching:false.newsList:action.news};
    case actionTypes.FETCH_ERROR:
      return{... state,isFetching:false.newsList: [].errorMsg:'Service exception'};
    default:
        returnstate; }}/ / {4}
const store = createStore(reducer);

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

Resolution:

  • {1} actionTypesIt makes sense that one contains manyactionThe object;
  • {2} initializationstateValue;
  • {3} reducerA pure function that accepts the initialization state andaction, returns the new state;
  • {4}reducerTo create a globalstore, and through theProviderComponents in thecontextFor descendant components to call.

App.js 

import { connect } from 'react-redux'
import News from "./News";
import {actionTypes} from "./index";
/ / {1}
function mapStateToProps(state) {
  return state
}
/ / {2}
function mapDispatchToProps(dispatch) {
  return {
    fetchNews:() = >{
      dispatch({type:actionTypes.FETCH_START}) / / {3}
      / / {4}
      new Promise(function (resolve,reject) {
        setTimeout(() = >{
          resolve([{title:"111"}, {title:"222"}]);
        },2000)
      }).then((response) = >{
        / / {5}
        dispatch({
          type:actionTypes.FETCH_SUCCESS,
          news:response
        })
      }).catch(() = >{
        / / {6}
        dispatch({
          type:actionTypes.FETCH_ERROR
        })
      })
    }
  }
}

export const App = connect(
  mapStateToProps,
  mapDispatchToProps
)(News)
Copy the code

Resolution:

  • {1} mapStateToPropsIts role has been mentioned above and will bestateMapped to the output componentprops ;
  • {2} mapDispatchToPropsdispatchMapped to the output componentprops ;
  • {3}FETCH_START 的 actionChange,state 中 isFetchingThe status oftrue, indicating the start of the request.
  • {4} for thisPromiseIs an asynchronous method that requests to the background;
  • {5} Issued after the request is successfulFETCH_SUCCESS 的 actionAnd pass in the latest data list;
  • {6} If an error occurs during the requestFETCH_ERROR 的 action 。

Let’s look at the final component, news.js

import React from 'react';

function News({isFetching,newsList,errorMsg,fetchNews}) {

  return (
    <div className="news">{/ * 1 * /}<button onClick={fetchNews}>Get the news list</button>
      {
        isFetching ?
          <div>Data loading...</div> :
          (
            <ul>
              {
                newsList.map((item,index)=>{
                  return <li key={index}>{item.title}</li>})}</ul>)}</div>
  );
}

export default News;
Copy the code

Resolution:

  • {1} This is a pure UI component that executes when a button is clickedfetchNewsFunction, which is essentially executing more than onedispatchMethod to change the state.

Normally you can handle asynchrony in this way, but redux provides middleware for us to use. By using middleware to handle asynchrony issues, the team also has specifications to fall back on.

Commonly used asynchronous processing: redux-Thunk, Redux-saga and other middleware. But that’s not the point of this article. The point is to understand how the entire middleware system works by using middleware, so today we’ll just cover the use of Redux-Thunk for its simplicity.

Asynchronous requests using the Redux-Thunk middleware

I’m going to use the example above, and not change much. Take a look at the code:

import thunk from 'redux-thunk'

export constactionTypes = { ... Same as above}/ / {1}
export const actionCreator = {
  fetchNews:() = >{
    return (dispatch) = >{
      dispatch({type:actionTypes.FETCH_START})
      new Promise(function (resolve,reject) {
        setTimeout(() = >{
          resolve([{title:"111"}, {title:"222"}]);
        },2000)
      }).then((response) = >{
        dispatch({
          type:actionTypes.FETCH_SUCCESS,
          news:response
        })
      }).catch(() = >{
        dispatch({
          type:actionTypes.FETCH_ERROR
        })
      })
    }
  }
}

constinitState = { ... Same as above}const reducer = (state= initState,action) = >{... Same as above}/ / {2}
const store = createStore(reducer,applyMiddleware(thunk));
Copy the code

Resolution:

  • {1} actionCreatorThe object containsfetchNewsThe entire function body is the same as in the above example, except that,returnReturn is a function;
  • {2} createStore(reducer,applyMiddleware(thunk))createstoreWhen passed inthunkMiddleware.

App.js 

import {actionCreator} from "./index";

function mapDispatchToProps(dispatch) {
  return {
    fetchNews:() = >{ dispatch(actionCreator.fetchNews()); }}}Copy the code

Resolution:

  • So let’s create a new onefetchNewsMethod, insidedispatchCall theactionCreator.fetchNews()The function returned. Actually, that’s what WE’re talking aboutdispatchIt can only execute an object of the form{type:'FETCH_SUCCESS'}But this is a functiondispatch((dispatch)=>{... })The reason is useredux-thunk 。

All other codes are consistent, go here to complete the transformation >>> click to view the completed code.

Of course, there are many, many excellent middleware that can help us complete projects quickly, such as Redux-Saga and Redux-Promise.

But the focus of this article is to understand redux and its middleware principles. Let’s take a look at how Redux is implemented.

Realize the principle of

Redux implementation principles

To recap, the createStore method creates a store object that contains getStat(), dispatch(), and subscribe(), so its initial structure should look like this:

export const createStore = () = > {    
    let currentState = {}
    function getState() {}
    function dispatch() {}
    function subscribe() {}
    return { getState, dispatch, subscribe }
}
Copy the code

getState

Get the state value

 function getState() {
   return currentState;
 }
Copy the code

dispatch

Call dispatch({type:’FETCH_SUCCESS’}), which is a function whose argument is an action. The reducer function was executed to change the state:

function dispatch(action) {
  A new state is returned after the reducer execution.
  currentState = reducer(currentState,action);
}
dispatch({ type: "INIT" }) / / {1}
Copy the code

Why dispatch first? Let me post the code from the previous example to give you an idea:

const initState = {
  isFetching: false.newsList: [].errorMsg:""
}

const reducer = (state= initState,action) = >{
  switch(action.type) { ... omitdefault:
        returnstate; }}Copy the code

The state of initState defined by us is not known in REdux, so calling dispatch({type: INIT}) first will execute the default branch of the Reducer method and return the initial state of initState.

function dispatch(action) { currentState = reducer(currentState,action); } # execute equals dispatch({type: "INIT" }) 

currentState = {
  isFetching: false.newsList: [].errorMsg:""
}
Copy the code

subscribe

Now that you can change the state, all you need is a set of code to publish and subscribe

import { reducer } from './reducer'
export const createStore = (reducer) = > {        
    let currentState = {}        
    let observers = [] // {1} Subscribe queue
    function dispatch(action) {                
        currentState = reducer(currentState, action)   
      	// {3} Execute all subscription methods when the state changes
        observers.forEach(fn= > fn())        
    }
    // {2} queues the subscribed methods
    function subscribe(fn) {                
        observers.push(fn)        
    }
}
Copy the code

Here is a version of Redux that does not include middleware systems:

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

This version of Redux is still very simple and easy to understand, but it will be a bit more difficult to analyze the middleware implementation.

Redux middleware implementation principles

Middleware (English: Middleware) is the software that provides the connection between system software and application software, so as to facilitate the communication between software components, especially the centralized logic of application software to system software. It is widely used in modern information technology application frameworks such as Web services and service-oriented architecture. For example, database, Tomcat of Apache, WebSphere of IBM, WebLogic application server of BEA, Tong series middleware of Dongfang Tong and Kingdee company all belong to middleware.

It is a software architecture idea in its own right, and it is also widely used in front-end applications, such as KOA, Express, and redux, the subject of this article, all implementing their own middleware systems. One result of this implementation is the Onion model.

Consider a simple example: after issuing a Dispatch ({type:”T1″}) in a view, what do I do if I want to print a log?

store.dispatch({ type: 'T1' })
console.log('Output log 1');
Copy the code

Normal thinking logic is to write it directly behind. There will be more and more requirements like this, so let’s assume that you want to output three types of logs.

store.dispatch({ type: 'T1' })
console.log('Output log 1');
console.log('Output log 2');
console.log('Output log 3');
Copy the code

But the need for logging is something that exists in every project, right? Is there anything you can do to make this easier?

Such as:

store.dispatch({ type: 'T1' })
showLog1(store)
showLog2(store)
showLog3(store)

function showLog1(store){
  console.log('Output log 1', store.getState());
}

function showLog2(store){
  console.log('Output log 2', store.getState());
}

function showLog3(store){
  console.log('Output log 3', store.getState());
}
Copy the code

Wrapping it in a function makes it easier to call. It can also be reused.

At this point, the requirements change again. I want to log not only on store.dispatch({type: ‘T1’}), but on T2, T3… And so on all want to log out.

execuT1();
execuT2();
execuT3();

function execuT1(){
  store.dispatch({ type: 'T1' })
  showLog1(store)
  showLog2(store)
  showLog3(store)
}

function execuT2(){
  store.dispatch({ type: 'T2' })
  showLog1(store)
  showLog2(store)
  showLog3(store)
}

function execuT3(){
  store.dispatch({ type: 'T3' })
  showLog1(store)
  showLog2(store)
  showLog3(store)
}
Copy the code

This is fine, so that no matter how many dispatches you have in the future, these three logs will be printed. But this is clearly not reasonable. And wrote a lot of duplicate code.

ShowLog1, showLog2, showLog3 are middleware. It’s essentially a function.

Now we want a middleware system that has two functions:

  1. These middleware can be quickly executed
  2. And realize information transfer between middleware

Let’s code the first point:

let store = createStore(reducer)

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

applyMiddleware(store, [ showLog1, showLog2, showLog3 ])

Copy the code

The current middleware system, applyMiddleware, can quickly execute registered middleware by passing in parameters. There is a problem, however, that the system will quickly execute all middleware as soon as the entire application is initialized. What we expect is that they are executed when the Dispatch function is executed.

The dispatch method now called by the view layer looks like this:

function dispatch(action) {                
  currentState = reducer(currentState, action)                
  observers.forEach(fn= > fn())        
}
Copy the code

This dispatch will only trigger the reducer method. If we want to execute dispatch, we will execute middleware according to the order of middleware registration.

Now suppose we modify the behavior of Dispatch as follows:

newDispatch = showLog1(showLog2(showLog3(oldDispatch)));
Copy the code

So when we call any dispatch from the view layer, we call the newDispatch function.

This not only links all the middleware together, but also enables “message transformation” between the middleware, since they are all in the same scope.

Look at the format of our middleware:

function showLog1(store){
  console.log('Output log 1', store.getState());
}
Copy the code

Its structure is too simple to meet our current needs, we need to transform it:

function showLog1(store) {    
    let next = store.dispatch    
    return (action) = > {        
      console.log('Enter log 1');    
      const result = next(action)  
      console.log('Exit log 1');
      return result    
    }
}
function showLog2(store) {    
    let next = store.dispatch    
    return (action) = > {        
      console.log('Enter log 2');    
      const result = next(action)  
      console.log(Exit log 2);    
      return result    
    }
}

function showLog3(store) {    
    let next = store.dispatch    
    return (action) = > {        
      console.log('Enter log 3');    
      const result = next(action)       
      console.log('Exit log 3');    
      returnResult}} newDispatch = showLog1(showLog2(oldDispatch));function showLog1(store) {    
    let next = store.dispatch    
    return (action) = > {        
      console.log('Enter log 1');    
        (action) = > {        
            console.log('Enter log 2');         
            (action) = > {        
                console.log('Enter log 3');    
                oldDispatch(action);
              	console.log('Exit log 3');    
            }
            console.log(Exit log 2,);
        }
        console.log('Exit log 1'); }}Copy the code

If this is the case, then this is fine. The question now is how do we make each next in showLog store the next middleware function call? Let’s transform the middleware:

function applyMiddleware(store, middlewares) {   
  middlewares = [ ...middlewares ] 
  middlewares.reverse()     
  middlewares.forEach(middleware= >      
    store.dispatch = middleware(store)    
  )
  return store;
}
Copy the code

Let’s analyze its execution:

[showLog3,showLog2,showLog1].forEach(middleware= >Store.dispatch = showLog3(store); Store.dispatch = showLog3(store);// Store. Dispatch is the original dispatchSecond call: store.dispatch = showLog2(store);ShowLog3 (store)Third call: store.dispatch = showLog1(store);ShowLog2 (store)# by the characteristics of the closure, and finally the next in each showLog function are saved under a middleware layer method # in the view of above disaptch actual instead: (action) = > {console.log('Enter log 1');    
  ((action) = > {        
    console.log('Enter log 2',);         
    ((action) = > {        
      console.log('Enter log 3');    
      ((action) = > {
        currentState = reducer(currentState,action);
        observers.forEach(fn= >fn());
      })(action)
      console.log('Exit log 3');    
    })(action)   
    console.log(Exit log 2,);         
  }(action) 
  console.log('Exit log 1'); } The entire method is executed by the user simply by executing the dispatch, and all middleware executes in order.Copy the code

At present, this middleware system has a preliminary model:

Standard format for middleware:function showLog1(store) {    
    let next = store.dispatch    
    return (action) = > {        
      console.log('Enter log 1');  
      const result = next(action);
      console.log('Exit log 1');  
      returnResult}} # Middleware handlerfunction applyMiddleware(store, middlewares) {    
    middlewares = [ ...middlewares ]      
    middlewares.reverse()     
    middlewares.forEach(middleware= >Store. Dispatch = middleware(store)} # Call applyMiddleware(store, [showLog1, showLog2, showLog3])Copy the code

This is already a fully executable middleware system.

And the implementation of this middleware, brought an interesting phenomenon, that is the Onion model.

The graphical representation is as follows:

Enhanced middleware systems

In the process of use, we found that using our own middleware system can only use our own handwriting middleware, there is no way to use third-party middleware. That’s because the format is inconsistent:

Redux middleware specification format:const showLog1 = store= > next= > action= > {    
    console.log('Enter log 1')    
    let result = next(action) 
    console.log('Exit log 1')    
    return result
}
Copy the code

Pass in the next middleware message, next, as a parameter.

This is what happens when you create middleware using standard Redux:

const store = createStore(reducer,applyMiddleware(logger,logger2,logger3));
Copy the code

CreateStore (createStore, createStore);

export const createStore = (reducer,applyMiddleware) = >{
  if(applyMiddleware){
    return applyMiddleware(createStore)(reducer);
  }
  ... 省略
}
Copy the code

Parsing: Determines if there is an entry to the middleware function, and if so, creates a store through the middleware function.

Let’s take a look at the transformation of middleware functions:

export const applyMiddleware = (. middlewares) = > createStore= > reducer= > {
  const store = createStore(reducer) // Create a store using createStore
  let { getState, dispatch } = store // Remove store getState and Dispatch
  
  // Reassemble a new parameter as a store entry for middleware
  const params = {
    getState,
    dispatch: (action) = > dispatch(action)
  }
  // This step is to pass params to each middleware to get a new list of middleware.
  const middlewareArr = middlewares.map(middleware= > middleware(params))
  
  Middlewares.reverse ().foreach (middleware => store.dispatch = middleware(store))dispatch = compose(... middlewareArr)(dispatch);return { ...store, dispatch }
}

function compose(. fns) {
  if (fns.length === 0) return arg= > arg
  if (fns.length === 1) return fns[0]
  return fns.reduce( (res, cur) = > {
    return (. args) = > {
      returnres(cur(... args)) } }) }Copy the code

Let’s analyze compose’s execution:

  • fns = [showLog1,showLog2,showLog3] ;
  • resRepresents the result of the last operation,curRepresents the current input parameter;
  • argsThe first time I execute it iscompose(... middlewareArr)(dispatch)In the initialdispatch ;
  • return res(cur(... args))It is obvious that the result will be the following:
middleware1(middleware2(middleware3(dispatch)));
Copy the code

So the end result is the same as the base middleware. I just wrote it in a more advanced way. The function is more thoroughly currified.

At this point, you should be able to understand the Redux middleware system. If you still have problems, you should be able to understand the code by yourself. This is something to master, as middleware patterns are widely used in Node.js express, KOA2 and other frameworks.

Missing redux – thunk

Middleware are enumerated showLog1, showLog2 in front of such a simple example, the goal is to more simple interpretation of the middleware system, after all, add on asynchronous middleware, understand it even more difficult.

The redux-Thunk implementation is, after all, quite simple and clever.

const thunk = store= > next= >action= > {
    return typeof action === 'function' ? action(store.dispatch) : next(action)
}
Copy the code

If action is passed in as a function, then action(store.dispatch) is executed, which equals the following function:

// This is the example above
(dispatch)=>{
  dispatch({type:actionTypes.FETCH_START})
  new Promise(function (resolve,reject) {
    setTimeout(() = >{
      resolve([{title:"111"}, {title:"222"}]);
    },2000)
  }).then((response) = >{
    dispatch({
      type:actionTypes.FETCH_SUCCESS,
      news:response
    })
  }).catch(() = >{
    dispatch({
      type:actionTypes.FETCH_ERROR
    })
  })
}
Copy the code

This function, when executed, is equivalent to executing the various dispatches in the function body as well as the dispatches in the asynchronous method.

conclusion

In the beginning of the basic part, we went to understand the concepts of action, dispatch, reducer and so on, and then explained the synchronous and asynchronous application of Redux respectively through two actual cases. Finally, the realization principle of REdux is also studied in depth.

After reading this article, you will understand Redux more thoroughly.

Please give a like if you like it!