Article series

Hand-written stupid React family bucket Redux

The React-Redux of the React family bucket

In this paper, the code

What is a Redux

Predictable State Container for JS Apps

Predictable: actually refers to pure functions (each input has a fixed output with no side effects). This is guaranteed by the Reducer and also facilitates testing

State container: In a Web page, each DOM element has its own state. For example, the pop-up box has two states: show and hide. The current page of the list and how many items are displayed on each page are the state of the list.

Although Redux is part of the React family, Redux is not necessarily related to React. It can be used on any other library, but it is popular with React.

The core concept of Redux

  1. Store: A container that stores state, a JS object
  2. Actions: Object that describes what Actions to take on the state
  3. Reducers: Function that operates on a state and returns a new state
  4. React Component: A Component, or view

Redux workflow

  1. The component fires the Action through the Dispatch method
  2. Store Receives the Action and distributes the Action to the Reducer
  3. The Reducer changes the state based on the Action type and returns the changed state to the Store
  4. The component subscribes to the state in the Store, and status updates from the Store are synchronized to the component

Here are the following:

React Components

It’s the React component, which is the UI layer

Store

Manage the warehouse of data and expose some apis to the outside world

let store = createStore(reducers);
Copy the code

Has the following responsibilities:

  • Maintain the state of the application;
  • providegetState()Method to obtain the state;
  • providedispatch(action)Method update state;
  • throughsubscribe(listener)Register listeners;
  • throughsubscribe(listener)The returned function unlogs the listener.

Action

An action is an action in a component that dispatches (action) to trigger changes in the store

Reducer

Action is just a command that tells the Store what changes to make. The reducer really changes the data.

Why use Redux

By default, React can only pass data from top to bottom, but when the lower component wants to pass data to the upper component, the upper component needs to pass the method of modifying the data to the lower component. When the project gets bigger and bigger, this way of passing will be very messy.

The reference to Redux, because Store is independent of components, makes the data management independent of components, to solve the problem of difficult data transfer between components

counter

Define store container files and generate stores according to the Reducer

import { createStore } from "redux";
const counterReducer = (state = 0, { type, payload = 1 }) = > {
  switch (type) {
    case "ADD":
      return state + payload;
    case "MINUS":
      return state - payload;
    default:
      returnstate; }};export default createStore(counterReducer);
Copy the code

In the component

import React, { Component } from "react";
import store from ".. /store";
export default class Redux extends Component {
  componentDidMount() {
    this.unsubscribe = store.subscribe(() = > {
      this.forceUpdate();
    });
  }
  componentWillUnmount() {
    if (this.unsubscribe) {
      this.unsubscribe();
    }
  }

  add = () = > {
    store.dispatch({ type: "ADD".payload: 1 });
  };
  minus = () = > {
    store.dispatch({ type: "MINUS".payload: 1 });
  };
  render() {
    return (
      <div className="border">
        <h3>Adder subtracter</h3>
        <button onClick={this.add}>add</button>
        <span style={{ marginLeft: "10px", marginRight: "10px}} ">
          {store.getState()}
        </span>
        <button onClick={this.minus}>minus</button>
      </div>); }}Copy the code
  • The getState command displays state
  • When add or Minus is clicked, dispatch is triggered and action is passed
  • And listens for state changes on componentDidMount and forces rendering on forceUpdate
  • ComponentWillUnmount Clears listeners

3. Start writing

As you can see from the above, the main function is the createStore function, which exposes the getState, Dispatch and subScribe functions, so let’s go down and create the createstore.js file

export default function createStore(reducer) {
  let currentState;
  // Obtain the state of the store
  function getState() {}
  / / change the store
  function dispatch() {}
  // Subscribe to store changes
  function subscribe() {}
  return {
    getState,
    dispatch,
    subscribe,
  };
}
Copy the code

Then perfect the next method

getState

Returns the current state

function getState() {
    return currentState
}
Copy the code

dispatch

Receive the action and update the store, by whom: Reducer

  / / change the store
  function dispatch(action) {
    // Pass the current state and action to the Reducer function
    // Return the new state stored in currentState
    currentState = reducer(currentState, action);
  }
Copy the code

subscribe

Function: Subscribe to changes in state

How to do it: In observer mode, the component listens to the SUBSCRIBE, passes in a callback function, registers the callback in the subscribe, and fires the callback in the Dispatch method

let curerntListeners = [];
// Subscribe to the state change
function subscribe(listener) {
  curerntListeners.push(listener);
  return () = > {
    const index = curerntListeners.indexOf(listener);
    curerntListeners.splice(index, 1);
  };
}
Copy the code

The dispatch method executes a subscription event after updating the data.

  / / change the store
  function dispatch(action) {
    // The data in the store is updated
    currentState = reducer(currentState, action);

    // Execute the subscription event
    curerntListeners.forEach(listener= > listener());
  }
Copy the code

The complete code

Change the redux in the counter above to refer to the handwritten redux, and you’ll see that the page has no initial value

So add dispatch({type: “KKK”}) to the createStore; If state is assigned an initial value, make sure that the type is entered into the reducer’s default condition

The complete code is as follows:

export default function createStore(reducer) {
  let currentState;
  let curerntListeners = [];
  // Obtain the state of the store
  function getState() {
    return currentState;
  }
  / / change the store
  function dispatch(action) {
    // Pass the current state and action to the Reducer function
    // Return the new state stored in currentState
    currentState = reducer(currentState, action);
    // Execute the subscription event
    curerntListeners.forEach((listener) = > listener());
  }
  // Subscribe to the state change
  function subscribe(listener) {
    curerntListeners.push(listener);
    return () = > {
      const index = curerntListeners.indexOf(listener);
      curerntListeners.splice(index, 1);
    };
  }
  dispatch({ type: "kkk" });
  return {
    getState,
    dispatch,
    subscribe,
  };
}

Copy the code

You can also check the Redux createStore source code

Redux middleware

When it comes to Redux, middleware is the only thing Redux can do, such as redux-Thunk for asynchronous calls, redux-Logger for logging, etc. The middleware is a function. Adding other functions between the Action and Reducer steps is equivalent to enhancing dispatch.

Develop Redux middleware

There is template code for developing middleware

export default store => next= > action= > {}
Copy the code
  1. A middleware receives the store as an argument and returns a function
  2. The returned function takes next (the old Dispatch function) as an argument and returns a new function
  3. The new function returned is the enhanced Dispatch function, which takes the store and next passed in from the top two levels

For example, simulate writing a Logger middleware

function logger(store) {
  return (next) = > {
    return (action) = > {
      console.log("= = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = =");
      console.log(action.type + "Done!);
      console.log("prev state", store.getState());
      next(action);
      console.log("next state", store.getState());
      console.log("= = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = =");
    };
  };
}
export default logger;
Copy the code

Registration middleware

// Pass applyMiddleware as the second parameter on the createStore
const store = createStore(
  reducer,
  applyMiddleware(logger)
)
Copy the code

As you can see, this is done through the createStore’s second parameter, officially called enhancer. Enhancer is a function that takes the createStore parameter and returns a new createStore function

function enhancer (createStore) {
    return function (reducer) {
      var store = createStore(reducer);
      var dispatch = store.dispatch;
      function _dispatch (action) {
        if (typeof action === 'function') {
          return action(dispatch)
        }
        dispatch(action);
      }
      return {
        ...store,
        dispatch: _dispatch
      }
    }
}
Copy the code

The createStore implementation has three parameters, the first reducer parameter is mandatory, the second state initial value, and the third enhancer is optional

Let’s add the enhancer parameter to the handwriting createStore and the handwriting applyMiddleware function

CreateStore plus enhancer

function createStore(reducer,enhancer) {
	// Determine whether enhancer exists
	// If it exists and is a function, pass createStore to it; if not, throw an error
	// It returns a new createStore
	// Enter the Reducer, execute the new createStore, and return the Store
	// Return the store
	if (typeofenhancer ! = ='undefined') {
		if (typeofenhancer ! = ='function') {
			throw new Error('Enhancer must be a function.')}return enhancer(createStore)(reducer)
	}
	// No enhancer goes through the original logic
	/ / to omit
}
Copy the code

Handwritten applyMiddleware

The applyMiddleware function receives the middleware function and returns an enhancer, so the basic structure is

export default function applyMiddleware(. middlewares) {
  // applyMiddleware should return an enhancer
  // Enhancer is a function that takes createStore as an argument
  return function (createStore) {
    // Enhancer to return a new createStore
    return function newCreateStore(reducer) {};
  };
}

Copy the code

Looking at the structure of the Logger middleware,

function logger(store) {
  return (next) = > {
    return (action) = > {
      console.log("= = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = =");
      console.log(action.type + "Done!);
      console.log("prev state", store.getState());
      next(action);
      console.log("next state", store.getState());
      console.log("= = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = =");
    };
  };
}
export default logger;
Copy the code

Under the perfect applyMiddleware

export default function applyMiddleware(middleware) {
  // applyMiddleware should return an enhancer
  // Enhancer is a function that takes createStore as an argument
  return function (createStore) {
    // Enhancer to return a new createStore
    return function newCreateStore(reducer) {
      / / create a store
      let store = createStore(reducer);
      let dispatch = store.dispatch;

      // The dispatch property must be written in this form. You cannot pass in store.dispatch directly
      // When there are multiple middleware, the value of dispatch is to get the enhanced dispatch of the previous middleware
      // This is valid because Dispatch is a reference type
      const midApi = {
        getState: store.getState,
        dispatch: (action, ... args) = >dispatch(action, ... args), };// Pass store to execute layer 1 functions of the middleware
      const chain = middleware(midApi);

      // Pass the original dispatch function to the chain as the next argument, calling the middleware layer 2 function
      // Return the enhanced dispatch to override the original dispatch
      dispatch = chain(dispatch);

      return {
        ...store,
        dispatch,
      };
    };
  };
}

Copy the code

Testing:Logs can be printed normally

Support for multiple middleware

The applyMiddleware function above handles only one middleware. What about multiple middleware scenarios?

First, let’s simulate writing a redux-Thunk middleware

By default, Redux only supports synchronization, and the arguments can only be objects. What Redux-Thunk implements is that when you pass a function, I execute that function directly, and the asynchronous operation code is written in the passed function, and if an object is passed, the next middleware is called

function thunk({ getState, dispatch }) {
  return next= > {
    return action= > {
      // If it is a function, execute it directly, passing in Dispatch and getState
      if (typeof action == 'function') {
        return action(dispatch, getState)
      }
      next(action)
    }
  }
}
Copy the code

Now it’s time to execute each piece of middleware in sequence. How? I’m going to curryize it, and I’m going to write the compose function

function compose(. funs) {
    // If there is no pass-through function, return the pass-through function
    if (funs.length === 0) {
        return (arg) = > arg
    }
    // When a function is passed, the function is returned, eliminating the need for traversal
    if (funs.length === 1) {
        return funs[0]}// When multiple items are passed, reduce is used to merge them
    For example, compose(f1,f2,f3) will return (... args) => f1(f2(f3(... args)))
    return funs.reduce((a, b) = > {
        return (. args) = > {
            returna(b(... args)) } }) }Copy the code

The applyMiddleware function supports multiple middleware:

export default function applyMiddleware(. middlewares) {
  // applyMiddleware should return an enhancer
  // Enhancer is a function that takes createStore as an argument
  return function (createStore) {
    // Enhancer to return a new createStore
    return function newCreateStore(reducer) {
      / / create a store
      let store = createStore(reducer);
      let dispatch = store.dispatch;

      // The dispatch property must be written in this form. You cannot pass in store.dispatch directly
      // When there are multiple middleware, the value of dispatch is to get the enhanced dispatch of the previous middleware
      // This is valid because Dispatch is a reference type
      const midApi = {
        getState: store.getState,
        dispatch: (action, ... args) = >dispatch(action, ... args), };// Pass a neutered version of the Store object by calling the middleware layer 1 function
      const middlewareChain = middlewares.map((middle) = > middle(midApi));
      
      // Use compose to get a function that combines all the middleware components
      constmiddleCompose = compose(... middlewareChain);// Call the middleware layer 2 functions one by one with the original dispatch function as an argument
      // Return the enhanced dispatch to override the original dispatch
      dispatch = middleCompose(dispatch);

      return {
        ...store,
        dispatch,
      };
    };
  };
}
Copy the code

Validation:

import { createStore } from ".. /kredux";
import logger from ".. /kredux/middlewares/logger";
import thunk from ".. /kredux/middlewares/thunk";
import applyMiddleware from ".. /kredux/applyMiddleware";

const counterReducer = (state = 0, { type, payload = 1 }) = > {
  switch (type) {
    case "ADD":
      return state + payload;
    case "MINUS":
      return state - payload;
    default:
      returnstate; }};export default createStore(counterReducer, applyMiddleware(thunk, logger));

Copy the code

Change the add function to asynchronously trigger Dispatch

  add = () = > {
    // store.dispatch({ type: "ADD", payload: 1 });
    store.dispatch(function (dispatch) {
      setTimeout(() = > {
        dispatch({ type: "ADD".payload: 2 });
      }, 1000);
    });
  };
Copy the code

Five, handwritten combineReducers

When the business logic is complex and it is not possible to write all the reducers in one reducer, use combineReducers to combine several reducers.

Add another userReducer:

const userReducer = (state = { ... initialUser }, { type, payload }) = > {
  switch (type) {
    case "SET":
      return{... state, ... payload };default:
      returnstate; }};Copy the code

Introduce the combineReducers function. This function accepts several objects. The key is the identifier and the value is each Reducer

export default createStore(
  combineReducers({ count: counterReducer, user: userReducer }),
  applyMiddleware(thunk, logger)
);
Copy the code

Write combineReducers, which return reducer functions. Naturally, the reducer function should receive state and action and return the new state

export default function combineReducers(reducers) {
  return function reducer(state = {}, action) {
    let nextState = {};
    // Iterate over all reducers, triggering the return of new states in turn
    for (let key in reducers) {
      nextState[key] = reducers[key](state[key], action);
    }
    return nextState;
  };
}

Copy the code

Six, summarized

  1. The Redux itself is just a predictable state container. All state changes are made by sending the Action command to the Reducer. The Action command is then distributed to the Reducer, and the Reducer returns the new state value.
  2. Redux is a typical observer pattern, registering a callback event when subscribe and executing a callback when dispatch action
  3. Redux focuses on the createStore function, which returns three methods, getState to return the current state, subscribe to subscribe to changes in state, Dispatch to update the store, and execute a callback event
  4. The default Redux supports only incoming objects and can only perform synchronization, so middleware is needed for more scenarios
  5. Middleware is a template forexport default store => next => action => {}The function of
  6. To register the middleware, use the createStore enhancer
  7. Redux middleware is a enhancement of Dispatch, which is a typical decorator pattern, and you can see that Redux is full of design patterns