Redux started

ReduxIs a state manager

Redux workflow

  1. Create a Store to Store data

Use createStore to create a Store to Store state in the app. There is only one Store in the app

import { createStore } from "redux";

Create a Reducer to define the change rules in state
const countReducer = function(count = 0, action) {
  switch(action.type) {
    case 'ADD':
      return count + 1;
    case 'MINUS':
      return count - 1;
    default:
      returncount; }}// Create a store to store state
const store = createStore(countReducer);

export default store;
Copy the code
  1. The Reducer in the Store initializes State and defines State modification rules

    Reducer is a pure function that takes Action and the current state as arguments and returns a new state. When creating a Store, pass Reducer as a parameter to createStore. If the state structure is complex, it can be considered to split multiple Reducer and synthesize using combineReducer.

import { combineReducers, createStore } from "redux";

// Reducer pure function
const countReducer = function (count = 0, action) {
  console.log("action", action);
  switch (action.type) {
    case "ADD":
      return count + 1;
    case "MINUS":
      return count - 1;
    default:
      returncount; }};const userReducer = function (name = "Caramelized melon seeds", action) {
  switch (action.type) {
    case "ENGLISH":
      return ` ENGLISH:${name}`;
    case "CHINESE":
      return ` English:${name}`;
    default:
      return Anonymous; }};// You can rename reducer in combineReducers
const store = createStore(combineReducers({userReducer, countReducer}));

export default store;

Copy the code
  1. The user submits changes to the data by dispatching an Action

State data changes are triggered using a dispatch submission Action alone, and view updates are not triggered.

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

export default class ReduxPage extends Component {
  componentDidMount() {
    // Store changes cannot trigger render in real time, we need to subscribe to listen to state changes, forcing trigger update
    store.subscribe(() = > {
      this.forceUpdate();
    })
  }

  handleAdd = () = > {
    // Submit action via dispatch: {type: 'ADD'}
    store.dispatch({ type: 'ADD' });
  }

  render() {
    return (
      <div>{store.getState()}<br />
        <button onClick={this.handleAdd}>increase</button>
        <button onClick={this.handleMinus}>To reduce</button>
      </div>); }}Copy the code
  1. The Store automatically calls the Reducer and sends the current State and Action to the Reducer. The Reducer returns the new State based on the input Action type
Reducer returns a new state based on the type submitted by the action
const countReducer = function (count = 0, action) {
  console.log("action", action);
  switch (action.type) {
    case "ADD":
      return count + 1;
    case "MINUS":
      return count - 1;
    default:
      returncount; }};Copy the code

Write a story

createStore

createStore(reducer, enhancer)

Creates a store for storing all data in an application.

Parameters:

  • reducer(Function) : pure functions
  • enhancer(Function):Store enhancer is a higher order Function that combines Store Creator and returns a new enhanced Store Creator

Return value: (store) Holds the state object for all applications. Store has several methods: getState(), Dispatch (action),subscribe(listener), and replaceReducer(nextReducer)

/** * Create a store to store all data in the application *@param {Function} Reducer is a pure function *@returns Return objects that hold all states: getState, dispatch, subscribe methods */
export default function createStore(reducer) {
  let currentState;
  let currentListeners =[]; // To save all listeners
  
  /** * This is the only way to trigger a state change *@returns Returns the current state */
  const getState = () = > {
    return currentState;
  }

  /** * This is the only way to trigger a state change * to call the Store's reduce function synchronously with the result of the current getState() and the incoming action *@param {Object} Action A generic function that describes application changes *@returns Action */ to dispatch
  const dispatch = (action) = > {
    currentState = reducer(getState(), action);
    currentListeners.map(listener= > listener());
    return action;
  }

  // Perform a dispatch to initialize data on the Reducer and return the default data on the Reducer
  dispatch({ type: 'redux/source' });

  const subscribe = (listener) = > {
    if (typeoflistener ! = ='function') {
      throw new Error('Listener must be a function! ');
    }
    // Store all listener functions in currentListener
    currentListeners.push(listener);
    return () = >{ currentListeners = []; }}return {
    getState,
    dispatch,
    subscribe
  }
}
Copy the code

combineReducer

As the application becomes more and more complex, the Reducer function can be split into separate functions (that is, multiple reducer) that are part of each Reducer management state.

CombineReducer: Receives objects combined by multiple reducers and returns a new Reducer. Each Reducer has its own part in the reducer management state

/** * Merge an object made up of different reducer functions as values into a final Reducer function * and then call the createStore method * on this reducer@param {Object} Reducers receive objects * that are combined from multiple reducers@returns Return a Reducer */ from the Reducer combination
export default function combineReducers(reducers) {
  if (Object.prototype.toString.call(reducers) ! = ='[object Object]') {
    throw new Error('combineReducers request to pass in an object composed by reducer ');
  }

  // Return a reducer: parameters state and action
  return (state = {}, action) = > {
    Reducer returns a newState. The latest newState was obtained by executing the reducer in the reducers
    let newState = {};
    
    // Execute all reducer to obtain the latest data and update state
    for (let key in reducers) {
      let reducer = reducers[key];
      newState[key] = reducer(state[key], action);
    }
    returnnewState; }}Copy the code

applyMiddleware

The purpose of applyMiddleware is to enhance dispatch and make it easy for external custom middleware to expand

The first version implements logging

1. Require a log to be generated when the user submits an action

// While enforcing dispatch, the basic functions of Dispatch are performed inside the function
const store = createStore(reducer);
const next = store.dispatch;

store.dispatch = action= > {
    console.log('Start execution${action.type}`);
    next(action);
    console.log('Completion of execution${action.type}`);
}

Copy the code

The second edition records exceptions

There is another requirement to record the cause of every data error, so we extend dispatch

const store = createStore(reducer);
const next = store.dispatch;

// Record exceptions on the basis of logging
store.dispatch = action= > {
    try{
        console.log('Store state tree'Store. GetState ()); next(action); }catch(e) {
        console.log(e); }}Copy the code

Collaboration with multi-functional middleware

Output the current state tree if the latest requirement requires that exceptions be logged as well as logged. You need to merge the two functions

const store = createStore(reducer);
const next = store.dispatch;

store.dispatch = action= > {
  try {
    console.log('Store state tree'Store. GetState ());// Log
    console.log('Start execution${action.type}`);
    next(action);
    console.log('Completion of execution${action.type}`);
  } catch(e){
    console.log('e', e); }}Copy the code

If new requirements continue to come in, manually change the dispatch extension each time. Therefore, we need to consider the implementation of strong scalability of multi-middleware cooperation mode

1. Extract loggerMiddleware for logging

const store = createStore(reducer);
const next = store.dispatch;

const loggerMiddleware = action= > {
   console.log('Start execution${action.type}`);
   next(action);
   console.log('Completion of execution${action.type}`);
}

store.dispatch = loggerMiddleware;
Copy the code

2. The errorMiddleware that records exceptions is extracted and the data in store is recorded in the normal process

const store = createStore(reducer);
const next = store.dispatch;

const errorMiddleware = action= > {
    try {
       console.log('Store state tree'Store. GetState ()); loggerMiddleware(action); }console.log('e', e);
}
store.dispatch = errorMiddleware;
Copy the code

3. Existing problems: ErrorMiddleware’s executors ** Next died as loggerMiddleware, and loggerMiddleware’s executors died as Next ** as Store. dispatch. Both are strongly correlated and over-coupled. Our requirement is that any middleware should be able to be used together

const store = createStore(reducer);
const next = store.dispatch

// loggerMiddleware needs to receive a next to level up
const loggerMiddleware = next= > action= > {
   console.log('Start execution${action.type}`);
   next(action);
   console.log('Completion of execution${action.type}`);
}

const errorMiddleware = next= > action= > {
    try {
       console.log('Store state tree'Store. GetState ()); next(action); }catch(e) {
       console.log('e', e); }}const store = createStore(reducer);
store.dispatch = errorMiddleware(loggerMiddleware(next))
Copy the code

4. Modular division of middleware

Different middleware is placed in different files. The current loggerMiddleware and errorMiddleware middleware are still strongly associated with store and continue to scale up, passing store as a parameter

// loggerMiddleware.js
// loggerMiddleware needs to receive another store to level up again
export default function loggerMiddleware = store= >next= >action= >{
  console.log('Start execution${action.type}`);
  next(action);
  console.log('Completion of execution${action.type}`);
}


// errorMiddleware.js
export default function errorMiddleware = store= >next= >action= >{
    try {
       console.log('Store state tree'Store. GetState ());// 当前顺序 errorMiddleware中的next为loggerMiddleware
       next(action);
    } catch(e) {
       console.log('e', e); }}// store.js
const store = createStore(reducer);
const next = store.dispatch;
const newErrorMiddleWare = errorMiddleWare(store);
const newLoggerMiddleWare = loggerMiddleWare(store);

store.dispatch = newErrorMiddleWare(newLoggerMiddleWare(next));
Copy the code

Optimization of middleware usage

According to the middleware implemented in the previous section, but the use of middleware is not friendly, and the details of middleware need to be encapsulated. In fact, we don’t need to know the specifics of middleware, just that middleware is there. We can encapsulate the details by extending createStore.

Store. Dispatch = errorMiddleware(loggerMiddleware(Next)) middleware merge, which we can implement with a compose function.

// store.js
const store = createStore(reducer);
const next = store.dispatch;

function compose(. funcs) {
  if (funcs.length === 0) {
    return arg= > arg;
  }

  if (funcs.length === 1) {
    return funcs[0];
  }

  return funcs.reduce((a, b) = > (. args) = >a(b(... args))); } store.dispatch = compose(errorMiddleware(store), loggerMiddleware(store))(next);Copy the code

Currently, although compose is used to encapsulate the middleware, details such as store.dispatch are still exposed and we can continue to encapsulate the functionality of the upgraded createStore.

Implement applyMiddleware: Receiving middleware returns a store of data

// applyMiddleware.js
/** *@param  {... any} Middlewares middleware */
export default function applyMiddleware(. middlewares) {
  / * * * *@param  {... any} Funcs Middleware *@returns Combine the middleware and return a new function */
  function compose(. funcs) {
    console.log("funcs", funcs);
    if (funcs.length === 0) {
      return (arg) = > arg;
    }

    if (funcs.length === 1) {
      return funcs[0];
    }

    return funcs.reduce(
      (a, b) = >
        (. args) = >a(b(... args)) ); }return store= > {
    // Enhance dispatch in store
    let dispatch = store.dispatch;
    // Middleware parameters
    let params = {
      getState: store.state,
      dispatch: (. args) = >dispatch(... args), };// Pass parameters to each middleware
    let middlewaresChain = middlewares.map((middleware) = > middleware(params));
    
    / / rewrite dispatchdispatch = compose(... middlewaresChain)(dispatch);return {
      ...store,
      dispatch,
    };
  };
}


// store.js
const store = createStore(reducer);
const stongerStore = applyMiddlewares(errorMiddleware, loggerMiddleware)(store);
Copy the code

Some optimizations for applyMiddleware still have problems, which can cause stores to be created simultaneously. CreateStore can be enhanced by modifying it to use applyMiddleware as a parameter

// createStore.js
/** * Create a store to store all data in the application *@param {Function} Reducer is a pure function *@returns Return objects that hold all states: getState, dispatch, subscribe methods */
export default function createStore(reducer, enhancer) {
  let currentState;
  let currentListeners =[]; // To save all listeners
  
  if (enhancer) {
    return enhancer(createStore)(reducer);
  }

  /** * This is the only way to trigger a state change *@returns Returns the current state */
  const getState = () = > {
    return currentState;
  }

  /** * This is the only way to trigger a state change * to call the Store's reduce function synchronously with the result of the current getState() and the incoming action *@param {Object} Action A generic function that describes application changes *@returns Action */ to dispatch
  const dispatch = (action) = > {
    currentState = reducer(getState(), action);
    currentListeners.map(listener= > listener());
    return action;
  }

  // Perform a dispatch to initialize data on the Reducer and return the default data on the Reducer
  dispatch({ type: 'redux/source' });

  const subscribe = (listener) = > {
    if (typeoflistener ! = ='function') {
      throw new Error('Listener must be a function! ');
    }
    // Store all listener functions in currentListener
    currentListeners.push(listener);
    return () = >{ currentListeners = []; }}return {
    getState,
    dispatch,
    subscribe
  }
}
Copy the code

We also need to modify some of the receive parameters for applyMiddleware

// applyMiddleware.js
/** *@param  {... any} Middlewares middleware */
export default function applyMiddleware(. middlewares) {
  / * * * *@param  {... any} Funcs Middleware *@returns Combine the middleware and return a new function */
  function compose(. funcs) {
    console.log("funcs", funcs);
    if (funcs.length === 0) {
      return (arg) = > arg;
    }

    if (funcs.length === 1) {
      return funcs[0];
    }

    return funcs.reduce(
      (a, b) = >
        (. args) = >a(b(... args)) ); }return (createStore) = > (reducer) = > {
    // Enhance dispatch in store
    const store = createStore(reducer);
    let dispatch = store.dispatch;
    // Middleware parameters
    let params = {
      getState: store.state,
      dispatch: (. args) = >dispatch(... args), };// Pass parameters to each middleware
    let middlewaresChain = middlewares.map((middleware) = > middleware(params));

    / / rewrite dispatchdispatch = compose(... middlewaresChain)(dispatch);return {
      ...store,
      dispatch,
    };
  };
}

Copy the code

After putting applyMiddleWare enhancements into createStore, you can create a store just by running createStore to generate one

// store.js
const store = createStore(reducer, applyMiddleWare(errorMiddleWare, toggerMiddleWare));
Copy the code

bindActionCreator

The main function of bindActionCreator is to hide dispatch and action creator, and bindActionCreator to dispatch for external direct invocation.

Rarely used, use scenarios are relatively few; Used in React-redux

/** * convert an object whose value is a different action creator to an object with the same key. * Wrap each Action Creator with Dispatch at the same time so that they can be called directly * * actionCreators can also be an array, but is not very user-friendly *@param {Function | Object} actionCreators 
 * @param {Function} dispatch 
 */

function bindActionCreator (actionCreator, dispatch) {
  // Returns actionCreator enhanced with Dispatch
  // args: The argument passed in by Action Creator
  return (. args) = >dispatch(actionCreator(... args)); }export default function bindActionCreators(actionCreators, dispatch) {
  if (typeof actionCreators === 'function') {
    return bindActionCreator(actionCreators, dispatch);
  }

  if (typeofactionCreators ! = ='object' || actionCreators === null) {
    throw new Error('actionCreators must be Function or Object');
  }

  let boundActionCreators = {};
  for(let key in actionCreators) {
    let actionCreator = actionCreators[key];
    boundActionCreators[key] = bindActionCreator(actionCreator, dispatch);

    if (typeof actionCreator === 'function') {
      boundActionCreators[key] = bindActionCreator(actionCreator, dispatch)
    }
  }

  return boundActionCreators;
}
Copy the code