Redux is a state machine with no UI, so it is usually used with a UI library. For example, Redux in React is used with the react-Redux library. This library binds Redux’s state machine to React’s UI rendering and automatically updates the page when you dispatch an action to change state. In this article, I’ll write my own react-Redux to start with the basics, then replace the official NPM library and keep it functional.

All the code for this article has been uploaded to GitHub, you can take it down to play:Github.com/dennis-jian…

Basic usage

Here is a simple example of a counter that runs like this:

To implement this functionality, we need to add the React-Redux library to our project and wrap the entire ReactApp root component with its Provider:

import React from 'react';
import ReactDOM from 'react-dom';
import { Provider } from 'react-redux'
import store from './store'
import App from './App';

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

As you can see from the above code, we also provide a store parameter to the Provider. This parameter is the store generated by Redux’s createStore. We need to call this method and pass in the returned store:

import { createStore } from 'redux';
import reducer from './reducer';

let store = createStore(reducer);

export default store;
Copy the code

The createStore parameter is a reducer, so we need a reducer:

const initState = {
  count: 0
};

function reducer(state = initState, action) {
  switch (action.type) {
    case 'INCREMENT':
      return{... state,count: state.count + 1};
    case 'DECREMENT':
      return{... state,count: state.count - 1};
    case 'RESET':
      return{... state,count: 0};
    default:
      returnstate; }}export default reducer;
Copy the code

Reduce here has an initial state with a count of zero, and it can handle three actions that correspond to three buttons on the UI that add, subtract, and reset counts in state. ConnectAPI: react-Redux: connectAPI: connectAPI: connectAPI: connectAPI: connectAPI: connectAPI: connectAPI: connectAPI: connectAPI: connectAPI: connectAPI For example, our counter component needs the count state and the add, subtract, and reset three actions, and we connect them with connect like this:

import React from 'react';
import { connect } from 'react-redux';
import { increment, decrement, reset } from './actions';

function Counter(props) {
  const { 
    count,
    incrementHandler,
    decrementHandler,
    resetHandler
   } = props;

  return (
    <>
      <h3>Count: {count}</h3>
      <button onClick={incrementHandler}>Count + 1</button>
      <button onClick={decrementHandler}>Count - 1</button>
      <button onClick={resetHandler}>reset</button>
    </>
  );
}

const mapStateToProps = (state) = > {
  return {
    count: state.count
  }
}

const mapDispatchToProps = (dispatch) = > {
  return {
    incrementHandler: () = > dispatch(increment()),
    decrementHandler: () = > dispatch(decrement()),
    resetHandler: () = > dispatch(reset()),
  }
};

export default connect(
  mapStateToProps,
  mapDispatchToProps
)(Counter)
Copy the code

As you can see from the above code, connect is a higher-order function. Its first order takes two parameters, mapStateToProps and mapDispatchToProps, which are both functions. MapStateToProps can customize which states need to be connected to the current component. These custom states can be obtained within the component using props. The mapDispatchToProps method passes in the Dispatch function. We can customize methods that call dispatch to the Dispatch action to trigger state updates. These custom methods can also be obtained by the component props. The second order of connect takes a component, and we can assume that this function is used to inject the previously defined state and method into the component, while returning a new component to the external call, so connect is also a higher-order component.

Here we take a look at the apis we use, and these are the goals we’ll be writing later:

Provider: a component that wraps the root component to inject Redux’s store.

CreateStore: The core method Redux uses to create a store, which we wrote in another article.

Connect: Used to inject state and dispatch into the desired component, returning a new component that is actually a higher-order component.

So the core of react-Redux is actually two apis, both of which are components and have similar functions. They both inject parameters into components. Provider injects store into the root component, and connect injects state and Dispatch into required components.

Before writing it by hand, let’s consider why react-Redux is designed with these two apis. If there are no these apis, is Redux ok? Of course it can! The whole point of Redux is to store the state of the entire application, update the state with a Dispatch action, and then the UI updates automatically. Why don’t I just start with the root component and pass the store down at each level? When each child component needs to read the state, just use store.getState() and store. Dispatch when updating the state. However, if you write it this way, if there are many nested levels of sub-components, each level needs to be passed in manually, which can be ugly and tedious to develop, and if a new student forgets to pass in the store, there will be a series of errors. So it’s nice to have something that can inject a global store into the component tree without having to pass layers as props, and that thing is Provider! Also, if each component relied on Redux independently, React’s data flow would be broken, which we’ll talk about later.

The React of Context API

React actually provides an API for injecting variables globally. This is called the Context API. Suppose I now have a requirement to pass a text color configuration to all of our components. Our color configuration is on the top component. When this color changes, all of the components below will automatically apply this color. We can inject this configuration using the Context API:

Use the firstReact.createContextCreate a context

// We use a separate file to call createContext
// Because the return value is referenced in different places by the Provider and Consumer
import React from 'react';

const TestContext = React.createContext();

export default TestContext;
Copy the code

useContext.ProviderWrap root component

Having created the context, if we want to pass variables to some components, we need to add testContext. Provider to their root component and pass the variable as a value parameter to TestContext.provider:

import TestContext from './TestContext';

const setting = {
  color: '#d89151'
}

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

useContext.ConsumerReceive parameters

We passed the argument in with context. Provider, so that any child component wrapped by context. Provider can get the variable, but we need to use the context. Consumer package to get the variable. For example, our previous Counter component can get this color by wrapping the JSX it returns with context.consumer:

// Be careful to introduce the same Context
import TestContext from './TestContext';

/ /... Omit n lines of code...
// The returned JSX is wrapped around context.consumer
// Notice that context. Consumer is a method that accesses the Context parameter
// The context is the setting passed by the Provider, and we can get the color variable
return (
    <TestContext.Consumer>
      {context => 
        <>
          <h3 style={{color:context.color}}>Count: {count}</h3>
          <button onClick={incrementHandler}>Count + 1</button>&nbsp;&nbsp;
          <button onClick={decrementHandler}>Count - 1</button>&nbsp;&nbsp;
          <button onClick={resetHandler}>reset</button>
        </>
      }
    </TestContext.Consumer>
  );
Copy the code

In the above code we passed a global configuration through the context, and we can see that the text color has changed:

useuseContextReceive parameters

In addition to the context. Consumer function, the React function also uses the useContext hook to accept Context parameters.

const context = useContext(TestContext);

return (
    <>
      <h3 style={{color:context.color}}>Count: {count}</h3>
      <button onClick={incrementHandler}>Count + 1</button>&nbsp;&nbsp;
      <button onClick={decrementHandler}>Count - 1</button>&nbsp;&nbsp;
      <button onClick={resetHandler}>reset</button>
    </>
);
Copy the code

So we can use the Context API to pass the Redux store. Now we can also guess that the React-Redux Provider actually wraps the context.Provider and passes the redux store argument. The connectHOC of React-Redux is essentially a wrapped Context.Consumer or useContext. We can implement react-Redux ourselves in this way.

handwrittenProvider

The Provider uses the context API, so we need to create a context file and export the required context:

// Context.js
import React from 'react';

const ReactReduxContext = React.createContext();

export default ReactReduxContext;
Copy the code

This file is very simple, just create a new context and export it, see the corresponding source code here.

Then apply this context to our Provider component:

import React from 'react';
import ReactReduxContext from './Context';

function Provider(props) {
  const {store, children} = props;

  // This is the context to pass
  const contextValue = { store };

  // Returns the ReactReduxContext wrapped component, passing in contextValue
  // We will leave children alone
  return (
    <ReactReduxContext.Provider value={contextValue}>
      {children}
    </ReactReduxContext.Provider>)}Copy the code

The Provider component code is not difficult, just pass the store into the context, and render the children directly.

handwrittenconnect

The basic function

In fact, connect is the most difficult part of React-Redux. It has complex functions and many factors to consider. To understand it, we need to look at it layer by layer.

import React, { useContext } from 'react';
import ReactReduxContext from './Context';

// The first function accepts mapStateToProps and mapDispatchToProps
function connect(mapStateToProps, mapDispatchToProps) {
  // The second level function is a higher-order component that gets the context
  // Then execute mapStateToProps and mapDispatchToProps
  // Render the WrappedComponent with this result combined with the user's parameters as the final parameters
  // WrappedComponent is our own component wrapped with ConNext
  return function connectHOC(WrappedComponent) {

    function ConnectFunction(props) {
      // Make a copy of props to wrapperProps
      const { ...wrapperProps } = props;

      // Get the value of context
      const context = useContext(ReactReduxContext);

      const { store } = context;  // Deconstruct store
      const state = store.getState();   / / to the state

      // Execute mapStateToProps and mapDispatchToProps
      const stateProps = mapStateToProps(state);
      const dispatchProps = mapDispatchToProps(store.dispatch);

      // Assemble the final props
      const actualChildProps = Object.assign({}, stateProps, dispatchProps, wrapperProps);

      / / render WrappedComponent
      return <WrappedComponent {. actualChildProps} ></WrappedComponent>
    }

    returnConnectFunction; }}export default connect;
Copy the code

Triggered update

Replacing the official React-Redux with the Provider and connect above actually rendered the page, but clicking the button didn’t react because we changed the state in the store via dispatch, but the change didn’t trigger an update to our component. As Redux mentioned earlier, we can use store. Subscribe to listen for changes in state and execute callbacks. The callback we need to register here is to check if the props we gave to the WrappedComponent have changed. Re-render ConnectFunction if it changes, so here we need to solve two problems:

  1. When westateChange when the check is finally givenConnectFunctionIs there any change in the parameters of
  2. If this parameter changes, we need to re-renderConnectFunction

Checking parameter Changes

To check for changes in parameters, we need to know the last render parameters and the local render parameters and compare them. To find out the parameters of the last render, we can use useRef directly in ConnectFunction to record the parameters of the last render:

// Record last render parameters
const lastChildProps = useRef();
useLayoutEffect(() = >{ lastChildProps.current = actualChildProps; } []);Copy the code

Note that lastChildProps. Current is assigned after the first render, and useLayoutEffect is required to ensure immediate synchronization after rendering.

Because we need to recalculate the actualChildProps to detect parameter changes, the logic of the calculation is the same, so we pull this calculation logic out into a separate method childPropsSelector:

function childPropsSelector(store, wrapperProps) {
  const state = store.getState();   / / to the state

  // Execute mapStateToProps and mapDispatchToProps
  const stateProps = mapStateToProps(state);
  const dispatchProps = mapDispatchToProps(store.dispatch);

  return Object.assign({}, stateProps, dispatchProps, wrapperProps);
}
Copy the code

The react-redux uses shallowEqual, which is a shallow comparison, to compare only one layer. If you have several layers of structures returned by mapStateToProps, for example:

{
  stateA: {
    value: 1}}Copy the code

React-redux is designed for performance reasons. Deep comparisons, such as recursive comparisons, can waste performance, and circular references can cause dead loops. Using shallow comparisons requires users to follow this paradigm and not pass in layers, which is also stated in the official documentation. Let’s just copy a shallow comparison here:

// shallowEqual.js 
function is(x, y) {
  if (x === y) {
    returnx ! = =0|| y ! = =0 || 1 / x === 1 / y
  } else {
    returnx ! == x && y ! == y } }export default function shallowEqual(objA, objB) {
  if (is(objA, objB)) return true

  if (
    typeofobjA ! = ='object' ||
    objA === null ||
    typeofobjB ! = ='object' ||
    objB === null
  ) {
    return false
  }

  const keysA = Object.keys(objA)
  const keysB = Object.keys(objB)

  if(keysA.length ! == keysB.length)return false

  for (let i = 0; i < keysA.length; i++) {
    if(!Object.prototype.hasOwnProperty.call(objB, keysA[i]) || ! is(objA[keysA[i]], objB[keysA[i]]) ) {return false}}return true
}
Copy the code

Detect parameter changes in callback:

// Register a callback
store.subscribe(() = > {
  const newChildProps = childPropsSelector(store, wrapperProps);
  // If the parameter changes, record the new value to lastChildProps
  // And forces the current component to update
  if(! shallowEqual(newChildProps, lastChildProps.current)) { lastChildProps.current = newChildProps;// An API is required to force updates to the current component}});Copy the code

Forced to update

There’s more than one way to force an update to the current component. If you’re using a Class component, you can just go this.setstate ({}), which is what the old react-Redux did. React-redux uses useReducer or useStatehook as the main useReducer provided by React.

function storeStateUpdatesReducer(count) {
  return count + 1;
}

/ / ConnectFunction inside
function ConnectFunction(props) {
  / /... N lines of code left out...
  
  // Use useReducer to force updates
  const [
    ,
    forceComponentUpdateDispatch
  ] = useReducer(storeStateUpdatesReducer, 0);
  // Register a callback
  store.subscribe(() = > {
    const newChildProps = childPropsSelector(store, wrapperProps);
    if(!shallowEqual(newChildProps, lastChildProps.current)) {
      lastChildProps.current = newChildProps;
      forceComponentUpdateDispatch();
    }
  });
  
  / /... N lines of code omitted...
}
Copy the code

Connect this code is mainly corresponding to the source code of the connectAdvanced class, the basic principle and structure are the same as ours, but he wrote more flexible, Supports user passing custom childPropsSelector and merging methods of stateProps, dispatchProps, and wrapperProps. Interested friends can go to see his source: github.com/reduxjs/rea…

React-redux is now available to replace the official react-redux with the counter function. But here’s how React-Redux ensures that components are updated in order, because a lot of the source code deals with this.

Ensure that components are updated in sequence

Our Counter component connects to the Redux store using connect. If there are other children connected to the Redux store, we need to consider the order in which their callbacks are executed. React is a one-way data flow, and its parameters are passed from parent to child. Redux is now introduced. Even though both parent and child components reference the same variable count, the child component can take the parameter directly from Redux instead of the parent component. In a parent -> child one-way data stream, if one of their common variables changes, the parent must update first, and then the parameter is passed to the child to update, but in Redux, the data becomes Redux -> parent, Redux -> child, and the parent and child can be updated independently based on the data in Redux. However, it cannot guarantee the process that the parent level updates first and the child level updates again. React-redux ensures this update order manually by creating a listener class outside the Redux store Subscription:

  1. SubscriptionTake care of all of themstateThe change of the callback
  2. If the current connectionreduxThe component is the first connectionreduxThe component, that is, it is connectedreduxThe root component of hisstateCallbacks are registered directly toredux store; Create a new one at the same timeSubscriptionThe instancesubscriptionthroughcontextPass to the children.
  3. If the current connectionreduxThe component is not a connectionreduxThat is, it has a component registered to itredux storeSo he can get it throughcontexthand-me-downsubscriptionIn the source code, this variable is calledparentSubThe update callback for the current component is registered toparentSubOn. And create a new oneSubscriptionInstance, substitutecontextOn thesubscription“, which means that its child component’s callback is registered with the current onesubscriptionOn.
  4. whenstateThe root component is registered toredux storeThe root component needs to manually execute the child component’s callback, which triggers the child component’s update, and then the child component executes itselfsubscriptionOn the registered callback, the grandson component is triggered to update, and the grandson component is then called to register itselfsubscriptionThe callback on… This ensures that the child components are updated layer by layer, starting with the root componentFather - > the sonThis is the update order.

Subscriptionclass

So let’s create a new Subscription class:

export default class Subscription {
  constructor(store, parentSub) {
    this.store = store
    this.parentSub = parentSub
    this.listeners = [];        // Source listeners are often linked to lists

    this.handleChangeWrapper = this.handleChangeWrapper.bind(this)}// Child component registration callback to Subscription
  addNestedSub(listener) {
    this.listeners.push(listener)
  }

  // Perform the child component's callback
  notifyNestedSubs() {
    const length = this.listeners.length;
    for(let i = 0; i < length; i++) {
      const callback = this.listeners[i]; callback(); }}// Wrap the callback function
  handleChangeWrapper() {
    if (this.onStateChange) {
      this.onStateChange()
    }
  }

  // Register the callback function
  // If parentSub has a value, the callback is registered with parentSub
  // If parentSub has no value, the current component is the root component, and the callback is registered with the Redux store
  trySubscribe() {
      this.parentSub
        ? this.parentSub.addNestedSub(this.handleChangeWrapper)
        : this.store.subscribe(this.handleChangeWrapper)
  }
}
Copy the code

Subscription is available here.

transformProvider

In our own react-redux implementation, the root component is always the Provider, so the Provider needs to instantiate a Subscription and place it into the context, and manually invoke the subcomponent callback every time the state is updated.

import React, { useMemo, useEffect } from 'react';
import ReactReduxContext from './Context';
import Subscription from './Subscription';

function Provider(props) {
  const {store, children} = props;

  // This is the context to pass
  // Add store and subscription instances
  const contextValue = useMemo(() = > {
    const subscription = new Subscription(store)
    // Register the callback as the notification child, so that hierarchical notification can begin
    subscription.onStateChange = subscription.notifyNestedSubs
    return {
      store,
      subscription
    }
  }, [store])

  // Get the previous state value
  const previousState = useMemo(() = > store.getState(), [store])

  // Every time the contextValue or previousState changes
  // notifyNestedSubs is used to notify child components
  useEffect(() = > {
    const { subscription } = contextValue;
    subscription.trySubscribe()

    if(previousState ! == store.getState()) { subscription.notifyNestedSubs() } }, [contextValue, previousState])// Returns the ReactReduxContext wrapped component, passing in contextValue
  // We will leave children alone
  return (
    <ReactReduxContext.Provider value={contextValue}>
      {children}
    </ReactReduxContext.Provider>)}export default Provider;
Copy the code

transformconnect

With the Subscription class, connect cannot register directly with the Store, but should register with parent Subscription, notifying child components of updates in addition to updating itself. Instead of rendering the wrapped component directly, you should pass in the modified contextValue again using the context.provider wrapper, which should replace the subscription with its own. The code after transformation is as follows:

import React, { useContext, useRef, useLayoutEffect, useReducer } from 'react';
import ReactReduxContext from './Context';
import shallowEqual from './shallowEqual';
import Subscription from './Subscription';

function storeStateUpdatesReducer(count) {
  return count + 1;
}

function connect(mapStateToProps = () => {}, mapDispatchToProps = () => {}) {
  function childPropsSelector(store, wrapperProps) {
    const state = store.getState();   / / to the state

    // Execute mapStateToProps and mapDispatchToProps
    const stateProps = mapStateToProps(state);
    const dispatchProps = mapDispatchToProps(store.dispatch);

    return Object.assign({}, stateProps, dispatchProps, wrapperProps);
  }

  return function connectHOC(WrappedComponent) {
    function ConnectFunction(props) {
      const { ...wrapperProps } = props;

      const contextValue = useContext(ReactReduxContext);

      const { store, subscription: parentSub } = contextValue;  // Deconstruct store and parentSub
      
      const actualChildProps = childPropsSelector(store, wrapperProps);

      const lastChildProps = useRef();
      useLayoutEffect(() = > {
        lastChildProps.current = actualChildProps;
      }, [actualChildProps]);

      const [
        ,
        forceComponentUpdateDispatch
      ] = useReducer(storeStateUpdatesReducer, 0)

      // Create a new subscription instance
      const subscription = new Subscription(store, parentSub);

      // The state callback is extracted as a method
      const checkForUpdates = () = > {
        const newChildProps = childPropsSelector(store, wrapperProps);
        // If the parameter changes, record the new value to lastChildProps
        // And forces the current component to update
        if(! shallowEqual(newChildProps, lastChildProps.current)) { lastChildProps.current = newChildProps;// An API is required to force updates to the current component
          forceComponentUpdateDispatch();

          // Then notify the child of the updatesubscription.notifyNestedSubs(); }};// Register callbacks using subscription
      subscription.onStateChange = checkForUpdates;
      subscription.trySubscribe();

      // Modify the context passed to the child
      // Replace subscription with your own
      constoverriddenContextValue = { ... contextValue, subscription }/ / render WrappedComponent
      // Use the ReactReduxContext package again, passing in the modified context
      return (
        <ReactReduxContext.Provider value={overriddenContextValue}>
          <WrappedComponent {. actualChildProps} / >
        </ReactReduxContext.Provider>)}returnConnectFunction; }}export default connect;
Copy the code

React-redux is now available on GitHub: github.com/dennis-jian…

Let’s summarize the core principles of React-Redux.

conclusion

  1. React-ReduxIs the connectionReactandReduxLibrary, while usingReactandReduxThe API.
  2. React-ReduxMainly usedReactthecontext apiTo passReduxthestore.
  3. ProviderThe function of is to receiveRedux storeAnd put it incontextPass it on.
  4. connectThe role ofRedux storeSelect the required properties to pass to the wrapped component.
  5. connectWill make their own judgment whether to update, the basis of judgment is necessarystateWhether it has changed.
  6. connectShallow comparison is used when judging whether there is a change, that is, only one layer is compared, so inmapStateToPropsandmapDispatchToPropsDo not revert to multiple nested objects in.
  7. In order to resolve the parent component and child component independent dependencyRedux, destroyedReacttheParent -> ChildUpdate process,React-ReduxuseSubscriptionThe class manages its own notification process.
  8. Only connected toReduxThe top-level components are registered directlyRedux storeAll other child components are registered with the nearest parentsubscriptionInstance.
  9. When notifying, the root component notifies its children, and when the child component receives the notification, it updates itself and then notifies its children.

The resources

Official documentation: react-redux.js.org/

GitHub source: github.com/reduxjs/rea…

At the end of this article, thank you for your precious time to read this article. If this article gives you a little help or inspiration, please do not spare your thumbs up and GitHub stars. Your support is the motivation of the author’s continuous creation.

Welcome to follow my public numberThe big front end of the attackThe first time to obtain high quality original ~

“Front-end Advanced Knowledge” series:Juejin. Cn/post / 684490…

“Front-end advanced knowledge” series article source code GitHub address:Github.com/dennis-jian…