preface

In the previous Redux technology share section, we introduced the basic use of Redux and the underlying source code implementation. How would the Redux data be used in the React component in a real project? This requires the use of React-Redux as a bridge.

Since most components are written using Hooks in real development, this section will focus on using and learning about React-Redux around the Hooks related API.

Basic usage

Installation:

yarn add redux react-redux
Copy the code

Add and subtract Demo:

// index.js
import React from 'react';
import ReactDom from 'react-dom';
import { Provider, useDispatch, useSelector } from 'react-redux';
import store from './store';

function App() {
  const count = useSelector(state= > state.count);
  const dispatch = useDispatch();

  return (
    <div>
      <div>{count}</div>
      <button onClick={()= >Dispatch ({type: 'INCREMENT'})}> Click + 1</button>
      <button onClick={()= >Dispatch ({type: 'DECREMENT'})}> click - 1</button>
    </div>
  )
}

ReactDom.render(
  <Provider store={store}>
    <App />
  </Provider>.document.getElementById('root'));// store.js
import { createStore } from 'redux';

const iniState = {
  count: 0
}
function reducer(state = iniState, action) {
  switch (action.type) {
    case "INCREMENT":
      return { ...state, count: state.count + 1 };
    case "DECREMENT":
      return { ...state, count: state.count - 1 };
    default:
      returnstate; }}let store = createStore(reducer);

export default store;
Copy the code

Analysis of the Demo:

From the Demo above we used the three property methods provided by React-Redux: Provider, useDispatch, and useSelector. These three apis are sufficient for use by Hooks.

  • Provider: the Provider, similar toReact ContextUnder theProviderObject, which can accept onestoreAs prop, easy to consume in sub-components;
  • UseDispatch:Redux dispatchAction to update the state in the store;
  • UseSelector: getRedux storeIn thestateAs a function componentstateTo use, andstateAfter the update, all components that consume it are updated(Core key).

I believe that the above simple Demo you can understand. Let’s start with the source code to understand the react-Redux mechanism.

Source code analysis

Core Method Overview:

import React, { useMemo, useContext, useEffect, useLayoutEffect } from 'react';

// React-redux implements an internal subscription mechanism to subscribe to the redux store when state changes.
// Update the React consumption component corresponding to state
import Subscription from './Subscription';

React Context object
const ReactReduxContext = React.createContext(null);

// Provider: Internally uses a Provider React component returned by the Context object
function Provider({ store, context, children }) {}

// Get the React Context object
function useReduxContext() {}

// Get the Redux store object provided by the Provider Value
function useStore() {}

// Get the Redux dispatch method
function useDispatch() {}

// Used by useSelector to create a subscriber for the state returned by useSelector to listen for state changes to update the view
function useSelectorWithStoreAndSubscription(. args) {}

// A default state function that compares the old and new state functions and determines the rules for updating views
const refEquality = (a, b) = > a === b;

// a selector that allows function components to retrieve state data from the Redux store
function useSelector(selector, equalityFn = refEquality) {}

export {
  Provider,
  useStore,
  useDispatch,
  useSelector,
  ReactReduxContext,
}
Copy the code

Provider

The React-redux Provider is not a Provider in the React Context object. It is a custom encapsulated component (with the same name as context.provider). But inside it returns context.provider as the wrapped component-provider.

It can accept a store as props (context. Provider can only be value as props), and that store is the familiar Redux store object.

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

Let’s look at the internal implementation of the Provider:

import React, { useMemo, useEffect } from 'react';

React Context object
const ReactReduxContext = React.createContext(null);

function Provider({ store, context, children }) {
  // 1. ContextValue is delivered as context. Provider value
  const contextValue = useMemo(() = > {
    // create a subscriber. Create a subscriber under Provider
    const subscription = new Subscription(store);
    // The onStateChange of the root subscriber points to the notifyNestedSubs method, which notifies all subscribers of data changes
    subscription.onStateChange = subscription.notifyNestedSubs;
    return {
      store,
      subscription,
    }
  }, [store]);

  // start the subscriber
  useEffect(() = > {
    const { subscription } = contextValue;
    // The core of the root subscriber: start the subscription. The essence is to call redux store.subscribe to a listener that listens for state
    // React-Redux implements its own set of Subscription, but for the subscriber to work properly, it needs to subscribe to register listening here
    subscription.trySubscribe();

    return () = > {
      subscription.tryUnsubscribe(); // Destroy the listener store. Unsubscribe
      subscription.onStateChange = null; }} []);const Context = context || ReactReduxContext;

  return <Context.Provider value={contextValue}>{children}</Context.Provider>
}
Copy the code
  • Start by creating a root subscribernew Subscription(store), hand in the subscriber and store as contextValueContext.ProviderThe provider is distributed;
  • Then performsubscription.trySubscribe()To start a subscription, the object of the subscription isRedux storeThe subscription mode isRedux store.subscribe(listener). That is, when Redux data changes, this will be notifiedSubscriptionSubscriber, which then does something (such as updating components that need to be updated).

If Subscription is not easy to understand here, it will be easier to understand when the Subscription source is analyzed below.

useReduxContext

Use the React Hooks API useContext to get the contextValue, which is the object with the Store and Subscription attributes provided in the Provider above.

import React, { useContext } from 'react';

const ReactReduxContext = React.createContext(null);

function useReduxContext() {
  const contextValue = useContext(ReactReduxContext);
  return contextValue;
}
Copy the code

useStore

Redux store (contextValue); Redux store (contextValue);

function useStore() {
  const { store } = useReduxContext();
  return store;
}
Copy the code

useDispatch

Since useStore can get the Redux store, we can get the store.dispatch method from it:

function useDispatch() {
  const store = useStore();
  return store.dispatch;
}
Copy the code

useSelector

UseSelector allows you to pass a function as an argument and store state as an argument. The function can access state through the argument, and the return value of the function will be the state to use in the component.

const count = useSelector(state => state.count);
Copy the code

The source code is as follows:

const refEquality = (a, b) = > a === b;

// equalityFn: the comparison function can be customized to compare old and new states and determine the rules for updating views
function useSelector(selector, equalityFn = refEquality) {
  const { store, subscription: contextSub } = useReduxContext();
  // Use the selector for storage and subscription
  const selectedState = useSelectorWithStoreAndSubscription(
    selector,
    equalityFn,
    store,
    contextSub
  );
  return selectedState;
}

function useSelectorWithStoreAndSubscription(
  selector,
  equalityFn,
  store,
  contextSub, // Context subscriber (application root subscriber)
) {
  const [, forceRender] = useReducer((s) = > s + 1.0);

  // create a subscriber for the selector selector with contextSub as parentSub
  const subscription = useMemo(() = > new Subscription(store, contextSub), [
    store,
    contextSub,
  ]);

  // Use useRef to persist variables
  const latestSelector = useRef(); // The latest selector method
  const latestStoreState = useRef(); // The latest warehouse status
  const latestSelectedState = useRef(); // The state returned by the latest selector

  const storeState = store.getState();
  let selectedState;

  // Perform the selector selector to read state each time the component is updated
  if( selector ! == latestSelector.current || storeState ! == latestStoreState.current ) { selectedState = selector(storeState); }else {
    selectedState = latestSelectedState.current
  }

  // The latest data is stored on each execution
  useLayoutEffect(() = > {
    latestSelector.current = selector;
    latestStoreState.current = storeState;
    latestSelectedState.current = selectedState;
  });

  // 3. Bind the component update method to the subsubscriber (onStateChange) so that the root subscriber listens for state updates and executes its component update method
  useLayoutEffect(() = > {
    function checkForUpdates() {
      const newSelectedState = latestSelector.current(store.getState());
      if (equalityFn(newSelectedState, latestSelectedState.current)) {
        return;
      }
      latestSelectedState.current = newSelectedState;
      forceRender();
    }

    // The onStateChange of the subsubscriber points to an update mechanism function that triggers component rerendering
    subscription.onStateChange = checkForUpdates;
    subscription.trySubscribe(); // Enable subscription

    return () = > subscription.tryUnsubscribe();
  }, [store]);

  return selectedState;
}
Copy the code
  • First read store and subscription, enter useSelectorWithStoreAndSubscription treatments;
  • Inside this method one is created for useSelectorThe child subscriberAnd willContextSub (Root subscriber)As aparentSub(This is important when looking at the Subscription source below);
  • callselectorAnd will bestore stateAs arguments, the execution result is what the user wants to usestate;
  • Define a component update method and bind it to a subsubscriber.

This way, when the root subscriber listens for changes in the Redux Store state, these subsubscribers can be notified, triggering component update rendering.

Subscription

Subscription is a set of Subscription update mechanisms implemented internally by React-Redux. There are also linked list data structure related operations that involve Redux Store. subscripe listening for state changes.

The source code is as follows:

const nullListeners = { notify(){}};export default class Subscription {
  constructor(store, parentSub) {
    this.store = store;
    this.parentSub = parentSub;
    this.unsubscribe = null;
    this.listeners = nullListeners;

    this.handleChangeWrapper = this.handleChangeWrapper.bind(this);
  }

  // Add a listener function for the parent to subscribe to the child instance
  addNestedSub(listener) {
    this.trySubscribe();
    return this.listeners.subscribe(listener);
  }

  // Notice to the listeners
  notifyNestedSubs() {
    this.listeners.notify();
  }

  // The listener to be added to trigger component updates
  handleChangeWrapper() {
    if (this.onStateChange) {
      this.onStateChange(); }}// Check whether subscription is enabled
  isSubscribed() {
    return Boolean(this.unsubscribe);
  }

  // Enable subscription
  trySubscribe() {
    if (!this.unsubscribe) {
      // store listens for state destructors
      this.unsubscribe = this.parentSub
        ? this.parentSub.addNestedSub(this.handleChangeWrapper)
        : this.store.subscribe(this.handleChangeWrapper); // store listens for state changes

      this.listeners = createListenerCollection(); }}// Unsubscribe
  tryUnsubscribe() {
    if (this.unsubscribe) {
      this.unsubscribe();
      this.unsubscribe = null;
      this.listeners.clear();
      this.listeners = nullListeners; }}}Copy the code

First we look at the trySubscribe method, which is executed by both root and child subscribers after they create a subscriber:

this.unsubscribe = this.parentSub
    ? this.parentSub.addNestedSub(this.handleChangeWrapper)
    : this.store.subscribe(this.handleChangeWrapper); // store listens for state changes

this.listeners = createListenerCollection();
Copy the code
  • For the root subscriber: it callsstore.subscribeTo register listening callbacks for state changeshandleChangeWrapper;
  • For subsubscribers: itsparentSubIt’s the root subscriber, which we talked about above, and it’s going to call the root subscriberaddNestedSubMethod, and subsubscriber’shandleChangeWrapperAs a parameter.

What exactly does the handleChangeWrapper method do?

handleChangeWrapper() {
  if (this.onStateChange) {
    this.onStateChange(); }}Copy the code

Does the onStateChange method ring a bell?

  • For subsubscribers, onStateChange corresponds to the component update functioncheckForUpdates;
  • For the parent subscriber, onStateChange for thenotifyNestedSubs.

Let’s take a look at notifyNestedSubs and the internal implementation of addNestedSub, which we just missed:

addNestedSub(listener) {
    this.trySubscribe(); // It can be ignored here, since the subscriber work will only be started once
    return this.listeners.subscribe(listener); / / to subscribe to the listener
}

notifyNestedSubs() {
    this.listeners.notify(); / / notify the listener
}
Copy the code

Notice that both methods are involved in this. Listeners have yet to notice another important method in the trySubscribe method: CreateListenerCollection, which internally blocks each listener function with a linked list, executes each listener in list order.

function createListenerCollection() {
  // List pointer to manage the execution of the listener function
  let first = null;
  let last = null;

  return {
    clear() {
      first = null
      last = null
    },
    
    notify() {
      let listener = first;
      while(listener) { listener.callback(); listener = listener.next; }},get() {
      let listeners = []
      let listener = first
      while (listener) {
        listeners.push(listener)
        listener = listener.next
      }
      return listeners
    },

    subscribe(callback) {
      let isSubscribed = true;

      let listener = (last = {
        callback,
        next: null.prev: last,
      })

      // Join the list queue
      if (listener.prev) {
        listener.prev.next = listener;
      } else {
        first = listener;
      }

      return function unsubscribe() {
        if(! isSubscribed || first ===null) return;
        isSubscribed = false;

        // Remove the listener from the list
        if (listener.next) {
          listener.next.prev = listener.prevl;
        } else {
          last = listener.prev;
        }
        // Re-establish the list relationship
        if (listener.prev) {
          listener.prev.next = listener.next
        } else {
          first = listener.next
        }
      }
    },
  }
}
Copy the code

To sort it out:

  1. In the Provider, create the root subscriber and calltrySubscribeEnable root subscription and passstore.subscripeTo listen for state changes and execute if state changesonStateChange(handler function);
  2. When useSelector is used, a subsubscriber is created and calledtrySubscribeOpen subsubscription, while subsubscriptiononStateChangeA handler is a method used to update a component:checkForUpdates; The update component method provided by the child subscriber is then used as a listener functionlistenerAdd to root subscriber:

addNestedSub(listener) {
    this.trySubscribe();
    return this.listeners.subscribe(listener);
}
Copy the code
  1. whenRedux store stateAfter the update, the root subscriber is triggered instore.subscribeRegistered on thehandleChangeWrapperMethod, which ultimately performs:
notifyNestedSubs() {
    this.listeners.notify();
}
Copy the code
  1. Finally, inthis.listeners.notifyIs executed on each child subscribercheckForUpdatesMethod to update the view:
notify() {
  let listener = first;
  while (listener) {
    listener.callback(); // checkForUpdates()listener = listener.next; }},Copy the code
  1. After the above treatment, whenRudex store stateWhen the change occurs, it is executeduseSelectorProvided by the neutron subscribercheckForUpdatesMethod, which updates the component if state changes before and after.

At the end of the article

If there are inadequacies in the compilation of this article, 👏 welcomes readers to put forward valuable suggestions, the author to correct.