Redux is the most popular state management library when it comes to React, leading mobx and Recoil in usage.

Many people may find Redux very difficult to use at first, with a lot of rules, because if the project isn’t too complex, you won’t be able to use Redux until you have to.

You only need Redux when React isn’t really a solution.

But in fact, Redux itself is not very complex, the source code is only a few hundred lines, today we will implement a simple version of Redux, peeling off the cocoon, from shallow to deep understanding of the principle of Redux.

The simple story

First we create a React project and then add redux:

create-react-app lredux

yarn add redux
Copy the code

Create a regular Redux page,

src
 | store
    | index.js
 | pages
    | index.jsx
Copy the code

store/index.js:

import { createStore } from "redux";

const countReducer = (state = 0, action) = > {
   switch (action.type) {
       case 'ADD':
           return state + action.payload;
       case 'MINUS':
           return state - action.payload;
       default:
           returnstate; }}export default createStore(countReducer);
Copy the code

pages/index.js:

import React, { Component } from "react";
import store from ".. /store";

export default class Index extends Component {

 componentDidMount() {
   this.unsubscribe = store.subscribe(() = > {
     this.forceUpdate();
   });
 }

 componentWillUnmount() {
   this.unsubscribe();
 }

 onAdd = () = > {
   store.dispatch({ type: "ADD".payload: 2 });
 };
 
 render() {
   return (
     <div>
       <button onClick={this.onAdd}>ADD</button>
       <div>{store.getState()}</div>
     </div>); }}Copy the code

Import this page in app.js, and you can see the following page after running:

Each time you click ADD you see the number increase by 2. This is the basic redux usage, so let’s implement a custom lredux.

Create the following project structure under SRC:

The project structure

The SRC | lredux custom redux | createStore. Js | index. JsCopy the code

The focus is on createstore.js, in which we implement redux’s createStore method, which receives a reducer to generate a store, which has a dispatch method to modify data, and an array to hold callbacks for subscription updates. Once dispatch is executed, the corresponding callback is invoked.

lredux/createStore.js:

export default function createStore(reducer) {
   // Save the callback after the data change
   const listeners = [];
   / / state
   let state;
   // Get the status
   function getState() {
       return state;
   }
   // Subscribe to the callback for data updates
   function subscribe(func) {
       listeners.push(func);
       // Return unbind subscription
       return () = > {
           const index = listeners.indexOf(func);
           listeners.splice(index, 1);
       };
   }
   // Update status via action and notify callback execution
   function dispatch(action) {
       state = reducer(state, action);
       listeners.forEach(listener= > listener());
   }
   // Call Dispatch once by default when creating a store to initialize the data
   // Type is a random number to distinguish it from the user's type
   dispatch({ type: Math.random() })

   return { getState, subscribe, dispatch };
}
Copy the code

In lredux/index.js expose our lredux method:

import createStore from "./createStore";

export { createStore };
Copy the code

Change the imported redux to lredux in store/index.js:

import { createStore } from ".. /lredux";
Copy the code

When you start the project, you can see that the functionality is still working, indicating that at this point you have implemented a simple version of Redux.

Realization of middleware function

At this point, our lREdux does not have middleware capabilities, so it cannot handle some asynchronous, Promise actions. For example, we add a method for delayed execution:

onAsyncAdd = () = > {
    store.dispatch((dispatch) = >
      setTimeout(() = > {
        dispatch({ type: "ADD".payload: 2 });
      }, 1000)); };render() {
    return (
      <div>
        <button onClick={this.onAdd}>ADD</button>
        <button onClick={this.onAsyncAdd}>AsyncAdd</button>
        <div>{store.getState()}</div>
      </div>
    );
  }
Copy the code

Action is a function, we can use a middleware redux-thunk, but our lREdux does not have middleware function, we will implement it now.

Redux-thunk and Redux-Logger see how redux uses middleware:

yarn add redux-thunk redux-logger
Copy the code

Modify the store. The index:

import { createStore, applyMiddleware } from "redux";
import logger from "redux-logger";
import thunk from "redux-thunk"; .export default createStore(countReducer, applyMiddleware(thunk, logger));

Copy the code

Run the project at this point and see that AsyncAdd takes effect. You can see that the use of Redux middleware is the result of the second argument passed to createStore, applyMiddleware, which is a function passed to the middleware. We implement a middleware that mimics it.

First create applymiddleware.js under lreact:

export default function applyMiddleware(. middlewares) {
    return createStore= > reducer= > {
        const store = createStore(reducer);

        const middlewareAPI = {
            getState: store.getState,
            dispatch: (action, ... args) = >dispatch(action, ... args) }const chain = middlewares.map(middleware= > middleware(middlewareAPI));

        constdispatch = compose(... chain)(store.dispatch);return{... store, dispatch }; }}function compose(. functions) {
    if (functions.length === 0) {
        return (. args) = > args;
    } else if (functions.length === 1) {
        return functions[0];
    } else {
        return functions.reducer(
            (pre, current) = > (. args) = >pre(current(args)) ); }}Copy the code

Source code is very complex, roughly explained:

ApplyMiddleware received three parameters successively, namely middlewares middleware, createStore and Reducer. CreateStore (Reducer) was called to create a store, but the next step was to strengthen dispatch. Our goal was to call middleware in sequence when dipatch was made.

What does middleware look like? Middleware is a function that takes three parameters successively. The first parameter is an object, including getState and Dispatch. The second parameter is next, indicating the next middleware. The final action is the parameter action for the user to invoke Dispatch.

So here is the middleWareAPI passed in by each middleWare middleWare — an object containing getState and Dispatch, corresponding to the first parameters of the middleWare. The resulting chain is an array of middleWare functions, and then we aggregate it. When aggregated, it becomes an Onion model function, executed one by one in middleware order.

The aggregation function is implemented using array.prototype. reduce, and many projects are implemented using Reduce onion model, such as KOA2.

Thunk, Logger, thunk, thunk, Logger, thunk, thunk, Logger

function middleWare({ getState, dispatch }) {
    return next= > action= >{... next(action); }}Copy the code

Middlewares. Map (Middleware => Middleware (middlewareAPI)) gives us a chain closure that looks something like this:


[
// thunk
next= >action= >{... next(action); },// logger    
next= >action= >{... next(action); }]Copy the code

Next comes the compose aggregation function, which aggregates the middleware in the chain into a new dispatch function:

// thunk
action => {
        ...
        next(action);
    },
Copy the code

Next is:


// logger
action => {
        ...
        next(action);
    },
Copy the code

(If you are confused about the compose execution process, you need to understand the reducer execution first)

Who is next in Logger? Is the original disptach function, which we pass in for compose: compose(… Chain) (store. Dispatch).

We expose the original Store and the new aggregated Dispath in applyMiddleWare, so once the user executes disptach, thunk and Logger will be executed step by step.

Having written applyMiddlWare, we also need to change how createStore is created:

lredux/createStore:

export default function createStore(reducer, enhancer) {
    // If there is an enhancer, create a store from enhancer
    if (enhancer) {
        returnenhancer(createStore)(reducer); }... }Copy the code

Lredux /index.js adds middleware exports:

import createStore from "./createStore";
import applyMiddleware from "./applyMiddleware";

export { createStore,applyMiddleware };
Copy the code

store/index.js:

import { createStore, applyMiddleware } from ".. /lredux"; .Copy the code

After running the project, everything is fine, and at this point we have implemented a middleware capable LREdux.

Implementation of specific middleware

Here we write a few simple versions of middleware.

logger

Logger is simple: print the values before and after the state change:

function customLogger({ dispatch, getState }) {
    return next= > action= > {
        console.log(`action  ${action.type} `);
        console.log(`prev state  ${getState()} `);
        console.log(action);
        next(action);
        console.log(`next state  ${getState()} `); }}Copy the code

thunk

Thunk is when an action is received, determine if it is a function, execute the function if it is, and pass in dispatch and getState.

function customThunk({ dispatch, getState }) {
    return next= > action= > {
        if (typeof action === 'function') {
            action(dispatch, getState);
        } else{ next(action); }}}Copy the code

promise

Promise simply determines whether an action is a promise, much like thunk.

 function customPromise({ dispatch, getState }) {
    return next= > action= > {
        if (isPromise(action)) {
            action.then(dispatch);
        } else{ next(action); }}}Copy the code

This is a minimalist version, but there are also cases where promises fail.

The last

At this point we have implemented a proposed version of Redux and its middleware, without the source code’s various fault-tolerant processes, but with a general understanding of the principles.

Please feel free to exchange any questions

Reference:

  • redux