preface

πŸ’‘ Why is data flow management important? React UI=render(data) UI=render(data) UI=render(data) UI=render(data) UI=render(data) UI=render(data) UI=render(data) UI=render(data) UI=render(data) UI=render(data) UI=render(data) UI=render(data) UI

In this article, we will briefly introduce the data flow management in React, from its own context to the concepts related to the third-party library Redux, and the implementation of redux dependencies.

Before the main article, a brief introduction to the concepts of data and state. React uses reusable components to build interfaces. Components are essentially finite state machines that can remember the current state of components and perform operations based on state changes. In React, this state is defined as state. React manages components by managing state. When state changes, React automatically performs the corresponding operation.

Data not only refers to the data returned by the server layer to the front end, but also the state in React. When the data changes, we need to change the state to cause the interface to change.

React’s own data flow scheme

Unidirectional data flow based on Props

React is a top-down one-way data flow. Container components and presentation components are the most common React component designs. The container component handles the complex business logic and data, and the presentation component handles the UI layer. Usually, we pull out the presentation component for reuse or encapsulation of the component library. The container component itself manages the state through state, setState updates the state to update the UI, and passes its state to the presentation component through props for communication

For simple communication, it is flexible to concatenate parent and sibling components based on props.

However, for the nested deep data flow component, A→B→C→D→E, the data of A needs to be passed to E, so we need to add this data to all the props of B/C/D, resulting in the introduction of some attributes that do not belong to the B/C/D of the intermediate component

Use the Context API to maintain global state

Context API is a component tree global communication method provided by React

Context is based on the producer-consumer pattern, corresponding to the three concepts in React: React. CreateContext, Provider, and Consumer. Create a set of providers by calling createContext. As the Provider of data, the Provider can send data to consumers at any level in its component tree, and consumers can not only read the data sent by the Provider but also read the subsequent updated value of the data

const defaultValue = {
  count: 0.increment: () = >{}};const ValueContext = React.createContext(defaultValue);

<ValueContext.Provider value={this.state.contextState}>
  <div className="App">
    <div>Count: {count}</div>
    <ButtonContainer />
    <ValueContainer />
  </div>
</ValueContext.Provider>

<ValueContext.Consumer>
  {({ increment }) => (
    <button onClick={increment} className="button">increment</button>
  )}
</ValueContext.Consumer>
Copy the code

Usage before 16.3, createContext usage after 16.3, useContext usage

A simple illustration of Context workflow:

Not recommended prior to V16.3 due to various limitations

  • The code is not simple and elegant enough: producers need to be definedchildContextTypesandgetChildContextConsumers need to defineChildTypesTo be able to accessthis.contextAccess the data provided by the producer
  • Data cannot be synchronized in time: available in class componentshouldComponentUpdateReturn false orPureComponentNone of the subsequent components will be updated, which violates the Context mode setting and causes the producer and consumer to fail to synchronize in time

Since v16.3, shouldComponentUpdate can still propagate through components even if it returns false, changing the declaration to be more semantic, making Context a viable communication solution

Context is also managed by a container component, but Consumer and Provider are one-to-one. When the project complexity is high, there may be multiple providers and consumers, or even one Consumer needs to correspond to multiple providers

When the business logic of a component becomes very complex, more and more code is written, because we can only control the data flow within the component. This results in both Model and View in the View layer, and the business logic and UI implementation are in the same layer, which is difficult to maintain

So you need a real data flow management tool that takes all the data out of the UI layer and lets React focus on the View layer

Redux

Redux is a state container for JS applications, providing predictable state management

Redux’s three principles

  • Single data source: The state of the entire application is stored in a single tree that exists only in a single store
  • State is read-only: changes to state only trigger actions
  • Make changes using pure functions: Reducer generates a new state based on the old state and incoming actions (similar to reduce, accept the previous state and the current action and calculate a new value)

Redux workflow

Immutability

To be mutable means to be able to change, immutability means to use unalterable

By default, JS objects and arrays are mutable, and it is possible to create an object or array by changing its contents

const obj = { name: 'FBB'.age: 20 };
obj.name = 'shuangxu';

const arr = [1.2.3];
arr[1] = 6;
arr.push('change');
Copy the code

To change an object or array, the address of the reference in memory has not changed, but the content has changed

To update immutably, the code must copy the original object/array, updating its copy body

const obj = { info: { name: 'FBB'.age: 20 }, phone: '177xxx' }
constcloneObj = { ... obj,info: { name: 'shuangxu'}}Shallow copy, deep copy
Copy the code

Redux expects all states to be immutable.

react-redux

React-redux is the React binding provided by Redux to help use Redux in React projects

Its API is simple, including a component Provider and a higher-order function connect

Provider

❓ Why does a Provider only pass a store when all the components it wraps can access the store’s data?

What does Provider do?

  • To create acontextValuecontainsreduxThe incomingstoreAnd according to thestoreTo create thesubscription, publish and subscribe aresubscriptiondo
  • throughcontextThe contextcontextValueTransitive subcomponent

Connect

What does ❓ Connect do?

Use the store provided by the context by the container component, and pass the state and dispatch returned by mapStateToProps and mapDispatchToProps to the UI component

The component depends on the state of redux, which maps to the props of the container component. A change in state triggers a change in the props of the container component, which triggers the container component component to update its view

const enhancer = connect(mapStateToProps, mapDispatchToProps)
enhancer(Component)
Copy the code

React-redux beggar version implementation

mini-react-redux

Provider

export const Provider = (props) = > {
  const { store, children, context } = props;
  const contextValue = { store };
  const Context = context || ReactReduxContext;
  return <Context.Provider value={contextValue}>{children}</Context.Provider>
};
Copy the code

connect

import { useContext, useReducer } from "react";
import { ReactReduxContext } from "./ReactReduxContext";


export const connect = (mapStateToProps, mapDispatchToProps) = > (
  WrappedComponent
) = > (props) = > {
  const { ...wrapperProps } = props;
  const context = useContext(ReactReduxContext);
  const { store } = context; // Deconstruct store
  const state = store.getState(); / / to the state
  // Use useReducer to get a force update function
  const [, forceComponentUpdateDispatch] = useReducer((count) = > count + 1.0);
  // Subscribe to state changes and execute callbacks when state changes
  store.subscribe(() = > {
    forceComponentUpdateDispatch();
  });
  // Execute mapStateToProps and mapDispatchToProps
  conststateProps = mapStateToProps? .(state);constdispatchProps = mapDispatchToProps? .(store.dispatch);// Assemble the final props
  const actualChildProps = Object.assign(
    {},
    stateProps,
    dispatchProps,
    wrapperProps
  );
  return <WrappedComponent {. actualChildProps} / >;
};

Copy the code

redux Middleware

“It provides a third-party extension point between Dispatching an action, And the moment it reaches the reducer. “– Dan Abramov

The Middleware middleware provides the opportunity to sort through actions, examining each one and picking out specific types of actions

Middleware example

Print log

store.dispatch = (action) = > {
  console.log("this state", store.getState());
  console.log(action);
  next(action);
  console.log("next state", store.getState());
};
Copy the code

Monitoring error

store.dispatch = (action) = > {
  try {
    next(action);
  } catch (err) {
    console.log("catch---", err); }};Copy the code

The two become one

store.dispatch = (action) = > {
  try {
    console.log("this state", store.getState());
    console.log(action);
    next(action);
    console.log("next state", store.getState());
  } catch (err) {
    console.log("catch---", err); }};Copy the code

Extracting loggerMiddleware/catchMiddleware

const loggerMiddleware = (action) = > {
  console.log("this state", store.getState());
  console.log("action", action);
  next(action);
  console.log("next state", store.getState());
};
const catchMiddleware = (action) = > {
  try {
    loggerMiddleware(action);
  } catch (err) {
    console.error("Error report:", err); }}; store.dispatch = catchMiddlewareCopy the code

LoggerMiddleware is dead in catchMiddleware, and next(Store.dispatch) is dead in loggerMiddleware, so it needs to be flexible enough to accept the dispatch parameter

const loggerMiddleware = (next) = > (action) = > {
  console.log("this state", store.getState());
  console.log("action", action);
  next(action);
  console.log("next state", store.getState());
};
const catchMiddleware = (next) = > (action) = > {
  try {
    /*loggerMiddleware(action); * /
    next(action);
  } catch (err) {
    console.error("Error report:", err); }};/*loggerMiddleware becomes a parameter */
store.dispatch = catchMiddleware(loggerMiddleware(next));
Copy the code

Accepting a store in Middleware lets you extract the above methods into a separate function file

export const catchMiddleware = (store) = > (next) = > (action) = > {
  try {
    next(action);
  } catch (err) {
    console.error("Error report:", err); }};export const loggerMiddleware = (store) = > (next) = > (action) = > {
  console.log("this state", store.getState());
  console.log("action", action);
  next(action);
  console.log("next state", store.getState());
};

const logger = loggerMiddleware(store);
const exception = catchMiddleware(store);
store.dispatch = exception(logger(next));
Copy the code

Each Middleware needs to take the Store argument to optimize the call function

export const applyMiddleware = (middlewares) = > {
  return (oldCreateStore) = > {
    return (reducer, initState) = > {
      // Get the old store
      const store = oldCreateStore(reducer, initState);
      //[catch, logger]
      const chain = middlewares.map((middleware) = > middleware(store));
      let oldDispatch = store.dispatch;
      chain
        .reverse()
        .forEach((middleware) = > (oldDispatch = middleware(oldDispatch)));
      store.dispatch = oldDispatch;
      return store;
    };
  };
};

const newStore = applyMiddleware([catchMiddleware, loggerMiddleware])(
  createStore
)(rootReducer);
Copy the code

Redux offers applyMiddleware to load the middleware, applyMiddleware accepts three parameters, middlewares array/Redux createStore/reducer

export default function applyMiddleware(. middlewares) {
  return createStore= > (reducer, ... args) = > {
    Create a store by createStore and Reducer
    conststore = createStore(reducer, ... args)let dispatch = store.dispatch
    var middlewareAPI = {
      getState: store.getState,
      dispatch: (action, ... args) = >dispatch(action, ... args) }// Pass getState/dispatch to middleware,
    // Map lets each middleware get the middlewareAPI parameter
    // Form an array of chain anonymous functions [f1,f2,f3...fn]
    const chain = middlewares.map(middleware= > middleware(middlewareAPI))
    //dispatch=f1(f2(f3(store.dispatch)))dispatch = compose(... chain)(store.dispatch)return {
      ...store,
      dispatch
    }
  }
}
Copy the code

ApplyMiddleware fits the onion model

conclusion

The purpose of this article is to explain react data flow management. Start with the data flow mode provided by React

  1. Based on thepropsThe concatenation of parent and sibling components is very flexible, but for deeply nested components, the intermediate components will add unnecessary componentspropsdata
  2. Use Context to maintain global state, which covers pre-v16.3 / post-v16.3 /hookscontextAs well as versions prior to V16.3contextThe disadvantages of.
  3. Redux, a third-party state container, and the React-Redux API(Provider/ Connect) analysis and hack implementation are introduced. Finally, how redux’s powerful middleware can rewrite the Dispatch method

Refer to the connection

  • React state management and solution comparison
  • React Context
  • Redux middleware,
  • Handwritten react – story