I wrote an article about the source code of Redux. After a few months, I looked at the source code of React Redux again. This time I also got a lot of results, so I wrote this blog to record what I learned.

React Redux is different from Redux. Redux is designed to provide a UI-independent data center that allows you to easily share data between any number of components in a component tree. Redux is independent of React and can be used without React. React Redux allows us to use Redux with React, so that we can easily obtain the data in the Store and subscribe to the change of the data in the Store. When the data in the Store changes, it can cause our corresponding components to re-render according to certain conditions.

So the core of React Redux is:

  • The component that provides data to us.
  • Subscribe to Store updates, timely re-render related components.
  • Components provide apis to us so that we can initiate changes to Store data within the component.

This article can be interpreted around these two points.

Before reading the source code, you should be familiar with how to use React-Redux. If you are not familiar with the API, you can read the documentation on the official website.

Also open a copy of the same version of the source code project on your computer to follow along.

First, download the React Redux source code on GitHub. The source version I read is 7.1.3, so it’s a good idea to download the source code to avoid confusion.

git clone https://github.com/reduxjs/react-redux.git
Copy the code

After downloading, we can see the source file structure, SRC directory specific file structure is as follows:

The SRC ├ ─ ─ alternate - renderers. Js ├ ─ ─ components │ ├ ─ ─ the Context, js │ ├ ─ ─ the Provider. The js │ └ ─ ─ connectAdvanced. Js ├ ─ ─ the connect │ │ ├─ Heavy Metal Exercises - Heavy metal Exercises - Heavy metal Exercises - Heavy metal Exercises - Heavy metal Exercises - Heavy metal Exercises - Heavy metal Exercises - Heavy metal Exercises ├── ├─ ├─ class exercises, class exercises, class Exercises, class Exercises, class Exercises UseSelector. Js │ └ ─ ─ useStore. Js ├ ─ ─ index. The js └ ─ ─ utils ├ ─ ─ Subscription. Js ├ ─ ─ batch. Js ├ ─ ─ isPlainObject. Js ├ ─ ─ ReactBatchedUpdates. Js ├ ─ ─ reactBatchedUpdates. Native. Js ├ ─ ─ shallowEqual. Js ├ ─ ─ useIsomorphicLayoutEffect. Js ├ ─ ─ UseIsomorphicLayoutEffect. Native. Js ├ ─ ─ verifyPlainObject. Js ├ ─ ─ warning. Js └ ─ ─ wrapActionCreators. JsCopy the code

Let’s start with the entry file index.js.

The document is simple:

import Provider from './components/Provider'
import connectAdvanced from './components/connectAdvanced'
import { ReactReduxContext } from './components/Context'
import connect from './connect/connect'

import { useDispatch, createDispatchHook } from './hooks/useDispatch'
import { useSelector, createSelectorHook } from './hooks/useSelector'
import { useStore, createStoreHook } from './hooks/useStore'

import { setBatch } from './utils/batch'
import { unstable_batchedUpdates as batch } from './utils/reactBatchedUpdates'
import shallowEqual from './utils/shallowEqual'

setBatch(batch)

export {
  Provider,
  connectAdvanced,
  ReactReduxContext,
  connect,
  batch,
  useDispatch,
  createDispatchHook,
  useSelector,
  createSelectorHook,
  useStore,
  createStoreHook,
  shallowEqual
}
Copy the code

The main thing is to export the API we need to use from the various files.

The most commonly used apis are Provider and Connect, and we’ll start with those and leave the rest behind.

A, the Provider

In the react-Redux example, we need to import the Provider component, wrap it around our root component, and pass in store data:

It makes it easy for our applications to get store objects, so how does it work?

React has a concept called Context, which also provides a Provider component and enables consumers to access data provided by the Provider from any internal location, so they have very similar functions and features. React-redux is implemented based on the Context API so that store objects provided by the top-level component can be retrieved from an internal location.

The source code of the React-Redux Provider is as follows:

function Provider({ store, context, children }) {
  const contextValue = useMemo((a)= > {
    const subscription = new Subscription(store)
    subscription.onStateChange = subscription.notifyNestedSubs
    return {
      store,
      subscription
    }
  }, [store])

  const previousState = useMemo((a)= > store.getState(), [store])

  useEffect((a)= > {
    const { subscription } = contextValue
    subscription.trySubscribe()

    if(previousState ! == store.getState()) { subscription.notifyNestedSubs() }return (a)= > {
      subscription.tryUnsubscribe()
      subscription.onStateChange = null
    }
  }, [contextValue, previousState])

  const Context = context || ReactReduxContext

  return <Context.Provider value={contextValue}>{children}</Context.Provider>
}
Copy the code

As you can see, this is a simple function component that starts with a contextValue object, which contains a store and a subscription. So this contextValue is passed to the Context API as a value at the end, the last line of code.

What is worth learning here is the calculation method of contextValue. Based on useMemo Hook, the cache of contextValue is realized. Only when the store changes, the value will be recalculated, reducing the cost of calculation.

This store object is required by our internal components. What is this subscription object? This subscription object is the key to implementing the React-Redux data subscription, and we’ll focus on this later, just know that it’s important for now.

The source of the Context is either the Context props received by the Provider or the internal default ReactReduxContext. So we know that we can provide a default Context component to the Provider for further encapsulation. Here we use ReactReduxContext in most cases:

export const ReactReduxContext = /*#__PURE__*/ React.createContext(null)

if(process.env.NODE_ENV ! = ='production') {
  // In non-production environments, you can see the component name in DevTools
  ReactReduxContext.displayName = 'ReactRedux'
}
Copy the code

As you can see, this is a context object created using the React. CreateContext API.

So we provide our contextValue to the underlying component through context.provider. How does our underlying component get our contextValue?

That’s when we think about our connect function, which must be doing the work internally, so let’s look at what connect does.

Second, the connect

The connect function comes from the connect. Js createConnect function call, which is a higher-order function that returns the connect API we’re actually using:

// createConnect with default args builds the 'official' connect behavior. Calling it with
// different options opens up some testing and extensibility scenarios
export function createConnect({ connectHOC = connectAdvanced, mapStateToPropsFactories = defaultMapStateToPropsFactories, mapDispatchToPropsFactories = defaultMapDispatchToPropsFactories, mergePropsFactories = defaultMergePropsFactories, selectorFactory = defaultSelectorFactory } = {}) {
  // This is the connect function we actually use
  return function connect(mapStateToProps, mapDispatchToProps, mergeProps, { pure = true, areStatesEqual = strictEqual, areOwnPropsEqual = shallowEqual, areStatePropsEqual = shallowEqual, areMergedPropsEqual = shallowEqual, ... extraOptions } = {}) {
    // The second argument to the match function is an array of functions, and the result is obtained by calling the functions in the second argument in sequence, taking the first argument as the call argument
    // The function returned is the corresponding wrapped function
    const initMapStateToProps = match(
      mapStateToProps,
      mapStateToPropsFactories,
      'mapStateToProps'
    )
    const initMapDispatchToProps = match(
      mapDispatchToProps,
      mapDispatchToPropsFactories,
      'mapDispatchToProps'
    )
    const initMergeProps = match(mergeProps, mergePropsFactories, 'mergeProps')

    return connectHOC(selectorFactory, {
      // used in error messages
      methodName: 'connect'.// used to compute Connect's displayName from the wrapped component's displayName.
      getDisplayName: name= > `Connect(${name}) `.// if mapStateToProps is falsy, the Connect component doesn't subscribe to store state changes
      shouldHandleStateChanges: Boolean(mapStateToProps),

      // passed through to selectorFactory
      initMapStateToProps,
      initMapDispatchToProps,
      initMergeProps,
      pure,
      areStatesEqual,
      areOwnPropsEqual,
      areStatePropsEqual,
      areMergedPropsEqual,

      // any extra options args can override defaults of connect or connectAdvanced. extraOptions }) } }Copy the code

Why is there a higher-order function to generate the connect function we use? The first comment makes it clear that since passing different parameters to the createConnect function can generate different connect functions for our tests and other scenarios, all the default parameters are used when calculating the actual connect function we use.

The createConnect function returns the actual connect function, which takes parameters that we should be familiar with. If you’re not familiar with it, check out the react-Redux documentation.

The connect function, upon receiving our passed parameters, executes the match function three times to calculate the functions that initialized the mapDispatchToProps, mapStateToProps, and mergeProps. Let’s look at how the match function is defined:

/* connect is a facade over connectAdvanced. It turns its args into a compatible selectorFactory, which has the signature: (dispatch, options) => (nextState, nextOwnProps) => nextFinalProps connect passes its args to connectAdvanced as options, which will in turn pass them to selectorFactory each time a Connect component instance is instantiated or hot reloaded. selectorFactory returns a final props selector from its mapStateToProps, mapStateToPropsFactories, mapDispatchToProps, mapDispatchToPropsFactories, mergeProps, mergePropsFactories, and pure args. The resulting final props selector is called by the Connect component instance whenever it receives new props or store state. */

function match(arg, factories, name) {
  for (let i = factories.length - 1; i >= 0; i--) {
    const result = factories[i](arg)
    if (result) return result
  }

  return (dispatch, options) = > {
    throw new Error(
      `Invalid value of type The ${typeof arg} for ${name} argument when connecting component ${ options.wrappedComponentName }. `)}}Copy the code

The first argument to the match function is the original mapStateToProps, mapDispatchToProps, or mergeProps function we passed in. The second argument is an array passed in by the connect function, and the third argument is the name of the specified function.

Inside the match function, we go through the second array parameters, execute the functions in turn, and pass in our original mapxxxToProps or mergeProps function as an argument. If it returns a non-false value, we return it as our init.. Function.

If the array still does not return a value that is not False, an error of the __standard __ format (note the format of the return function) is returned indicating that the user passed the map.. The match function does not meet the requirements. It looks like the match function will do a check on the parameters we passed.

What is the standard format mentioned in the previous paragraph? We can take a look at the long comments above the function definition. This comment illustrates that connect is just a proxy for connectAdvanced, and all it does is convert the parameters we pass into parameters that can be used by selectorFactory. One standard that can be used by selectorFactory is that the structure of the arguments is defined as follows:

(dispatch, options) => (nextState, nextOwnProps) = > nextFinalProps
Copy the code

This structure is our standard format.

The init family of functions converted to this format are passed as arguments to the connectAdvanced function, which computes the props needed by the final component based on the arguments and the store object.

This standard format is very important, because a lot of the rest of the code is related to this, so we need to pay attention to it.

We already know what the match function does, so let’s look at how match calculates our normalized mapStateToProps with the second argument… Theta is equal to theta.

2.1 initMapStateToProps

This function is calculated from the following code:

const initMapStateToProps = match(
  mapStateToProps,
  mapStateToPropsFactories,
  'mapStateToProps'
)
Copy the code

We already know how match works, so the key to normalization lies in the mapStateToPropsFactories array.

Let’s now look at the array:

import { wrapMapToPropsConstant, wrapMapToPropsFunc } from './wrapMapToProps'

export function whenMapStateToPropsIsFunction(mapStateToProps) {
  return typeof mapStateToProps === 'function'
    ? wrapMapToPropsFunc(mapStateToProps, 'mapStateToProps')
    : undefined
}

export function whenMapStateToPropsIsMissing(mapStateToProps) {
  return! mapStateToProps ? wrapMapToPropsConstant((a)= > ({})) : undefined
}

export default [whenMapStateToPropsIsFunction, whenMapStateToPropsIsMissing]
Copy the code

As you can see, there are two functions in this array that will pass in our mapStateToProps parameter to evaluate and return the result.

The first function whenMapStateToPropsIsFunction is calculated when we mapStateToProps parameters for the function of the results, The second function evaluates the default when we pass in mapStateToProps with a False value (that is, the function that the selectorFactory function should use if our mapStateToProps is null).

Let’s take a closer look at the wrapMapToPropsFunc and wrapMapToPropsConstant functions, where the wrapMapToPropsFunc function is the focus.

The wrapMapToPropsFunc function code is as follows:

// Used by whenMapStateToPropsIsFunction and whenMapDispatchToPropsIsFunction,
// this function wraps mapToProps in a proxy function which does several things:
//
// * Detects whether the mapToProps function being called depends on props, which
// is used by selectorFactory to decide if it should reinvoke on props changes.
//
// * On first call, handles mapToProps if returns another function, and treats that
// new function as the true mapToProps for subsequent calls.
//
// * On first call, verifies the first result is a plain object, in order to warn
// the developer that their mapToProps function is not returning a valid result.
//
export function wrapMapToPropsFunc(mapToProps, methodName) {
  return function initProxySelector(dispatch, { displayName }) {
    const proxy = function mapToPropsProxy(stateOrDispatch, ownProps) {
      return proxy.dependsOnOwnProps
        ? proxy.mapToProps(stateOrDispatch, ownProps)
        : proxy.mapToProps(stateOrDispatch)
    }

    // allow detectFactoryAndVerify to get ownProps
    // Set dependsOnOwnProps to true on the first run
    DetectFactoryAndVerify allows the detectFactoryAndVerify to get a second parameter at run time, and so on at run time
    // proxy.mapToProps and denpendsOnOwnProps are both computed
    proxy.dependsOnOwnProps = true

    proxy.mapToProps = function detectFactoryAndVerify(stateOrDispatch, ownProps) {
      proxy.mapToProps = mapToProps
      proxy.dependsOnOwnProps = getDependsOnOwnProps(mapToProps)
      let props = proxy(stateOrDispatch, ownProps)

      if (typeof props === 'function') {
        proxy.mapToProps = props
        proxy.dependsOnOwnProps = getDependsOnOwnProps(props)
        props = proxy(stateOrDispatch, ownProps)
      }

      if(process.env.NODE_ENV ! = ='production')
        verifyPlainObject(props, displayName, methodName)

      return props
    }

    return proxy
  }
}
Copy the code

You can see that the result returned by this function does conform to our standard format, and that proxy.maptoprops returns the final result in the standard format: nextFinalProps.

As you can see, the returned initProxySelector function can take the dispatch function and the options parameters. It returns a proxy function object, which takes the stateOrDispatch function and the ownProps parameters. Finally, a calculation result is returned.

The key here is the proxy.dependsonownprops attribute, which determines whether the second function is passed when we call the proxy.maptoprops function. How is this dependsOnOwnProps property obtained?

DependsOnOwnProps is true when initProxySelector is executed for the first time. This is to allow the detectFactoryAndVerify function to get the ownProps parameter. DetectFactoryAndVerify exists to do some extra work when the mapToProps function is run the first time, such as evaluating the dependsOnOwnProps property and verifying the props result returned. The detectFactoryAndVerify function is re-assigned to proxy.maptoprops, which means that the parameters are not re-evaluated when the proxy.maptoprops function is run a second time.

In addition, if the result returned by mapStateToProps is a function, the function will be used as the real mapToProps function in the subsequent calculation. That’s why the official document includes the following:

You may define mapStateToProps and mapDispatchToProps as a factory function, i.e., you return a function instead of an object. In this case your returned function will be treated as the real mapStateToProps or mapDispatchToProps, and be called in subsequent calls. You may see notes on Factory Functions or our guide on performance optimizations.

That is, you can return a function as well as a pure object.

The getDependsOnOwnProps function evaluates the dependsOnOwnProps property:

// This function is used to calculate whether the mapToProps needs to use props
If function.length === 1, stateOrDispatch is required
// If function.length! If == 1, this means that props is needed for calculation.
export function getDependsOnOwnProps(mapToProps) {
  returnmapToProps.dependsOnOwnProps ! = =null&& mapToProps.dependsOnOwnProps ! = =undefined
    ? Boolean(mapToProps.dependsOnOwnProps) : mapToProps.length ! = =1
}
Copy the code

When mapToProps. DependsOnOwnProps has value when they use this value directly as a result, no longer recount; If there is still no value, we need to perform a calculation. The logic of the calculation is the parameter format of the mapToProps function, the number of parameters we passed to the mapStateToProps function, The ownProps parameter is not passed only when the number of parameters is 1.

The wrapMapToPropsConstant function is the function that evaluates the result when the mapStateToProps function we passed is null. The code is as follows:

export function wrapMapToPropsConstant(getConstant) {
  return function initConstantSelector(dispatch, options) {
    const constant = getConstant(dispatch, options)

    function constantSelector() {
      return constant
    }
    constantSelector.dependsOnOwnProps = false
    return constantSelector
  }
}
Copy the code

DependsOnOwnProps = fasle (const selector) (const selector) (const selector) (const selector) There is no dependency on ownProps. The final result is a undefined object, because in this case getConstant is an empty function: () => {}.

2.2 initMapDispatchToProps

The array of functions used to calculate this function is:

import { bindActionCreators } from 'redux'
import { wrapMapToPropsConstant, wrapMapToPropsFunc } from './wrapMapToProps'

export function whenMapDispatchToPropsIsFunction(mapDispatchToProps) {
  return typeof mapDispatchToProps === 'function'
    ? wrapMapToPropsFunc(mapDispatchToProps, 'mapDispatchToProps')
    : undefined
}

export function whenMapDispatchToPropsIsMissing(mapDispatchToProps) {
  return! mapDispatchToProps ? wrapMapToPropsConstant(dispatch= > ({ dispatch }))
    : undefined
}

export function whenMapDispatchToPropsIsObject(mapDispatchToProps) {
  return mapDispatchToProps && typeof mapDispatchToProps === 'object'
    ? wrapMapToPropsConstant(dispatch= >
        bindActionCreators(mapDispatchToProps, dispatch)
      )
    : undefined
}

export default [
  whenMapDispatchToPropsIsFunction,
  whenMapDispatchToPropsIsMissing,
  whenMapDispatchToPropsIsObject
]
Copy the code

There are three functions that handle when the mapDispatchToProps passed in is a function, null, and an object.

When mapDispatchToProps is passed as a function, wrapMapToPropsFunc is also called to calculate the result, which is consistent with initMapStateToProps.

When mapDispatchToProps is null, the logic is the same as initMapStateToProps, except that the argument is not an empty function. Instead, it is a function that returns an object, which by default contains the dispatch function. This allows us to internally access the Store dispatch API using react-redux via this.props. Dispatch:

Once you have connected your component in this way, your component receives props.dispatch. You may use it to dispatch actions to the store.

When mapDispatchToProps is passed as an object, this is an ActionCreator object, You can convert this ActionCreator into an object containing many functions and merge it into props by using redux’s bindActionCreator API.

2.3 initMergeProps

The calculation of this function is relatively simple, the code is as follows:

import verifyPlainObject from '.. /utils/verifyPlainObject'

export function defaultMergeProps(stateProps, dispatchProps, ownProps) {
  return{... ownProps, ... stateProps, ... dispatchProps } }export function wrapMergePropsFunc(mergeProps) {
  return function initMergePropsProxy(dispatch, { displayName, pure, areMergedPropsEqual }) {
    let hasRunOnce = false
    let mergedProps

    return function mergePropsProxy(stateProps, dispatchProps, ownProps) {
      const nextMergedProps = mergeProps(stateProps, dispatchProps, ownProps)

      if (hasRunOnce) {
        // here if pure === true and the content of the new and old props is unchanged
        // Do not assign mergedProps, so that references to the original contents remain unchanged,
        // Let useMemo or react. memo work.
        if(! pure || ! areMergedPropsEqual(nextMergedProps, mergedProps)) mergedProps = nextMergedProps }else {
        hasRunOnce = true
        mergedProps = nextMergedProps

        if(process.env.NODE_ENV ! = ='production')
          verifyPlainObject(mergedProps, displayName, 'mergeProps')}return mergedProps
    }
  }
}

export function whenMergePropsIsFunction(mergeProps) {
  return typeof mergeProps === 'function'
    ? wrapMergePropsFunc(mergeProps)
    : undefined
}

export function whenMergePropsIsOmitted(mergeProps) {
  return! mergeProps ?(a)= > defaultMergeProps : undefined
}

export default [whenMergePropsIsFunction, whenMergePropsIsOmitted]
Copy the code

It’s basically a straightforward amalgamation of stateProps, dispatchProps, and ownProps with some basic checks.

We now have three main functions: initMapStateToProps, initMapDispatchToProps, and initMergeProps. We saw how react-Redux calculates the props of the connected component using the parameters we passed in with store.

Let’s take a closer look at how the selectorFactory function is based on our init… Functions to calculate the final props.

2.4 selectorFactory

Find the file selectorFactory. Js file, you can see finalPropsSelectorFactory function, this is our selectorFactory function.

The code is as follows:

export default function finalPropsSelectorFactory(dispatch, { initMapStateToProps, initMapDispatchToProps, initMergeProps, ... options }) {
  const mapStateToProps = initMapStateToProps(dispatch, options)
  const mapDispatchToProps = initMapDispatchToProps(dispatch, options)
  const mergeProps = initMergeProps(dispatch, options)

  if(process.env.NODE_ENV ! = ='production') {
    verifySubselectors(
      mapStateToProps,
      mapDispatchToProps,
      mergeProps,
      options.displayName
    )
  }

  const selectorFactory = options.pure
    ? pureFinalPropsSelectorFactory
    : impureFinalPropsSelectorFactory

  return selectorFactory(
    mapStateToProps,
    mapDispatchToProps,
    mergeProps,
    dispatch,
    options
  )
}
Copy the code

You can see that init… The mapStateToProps, mapDispatchToProps, and mergeProps functions are calculated.

Then select a different function based on the options.pure value for the next calculation.

When options.pure === true, it means that our component is “pure”.

One of the things the React Redux source code has to mention is the pure parameter in the configuration item. We can pass this configuration in createStore, which defaults to true.

When Pure is true, React Redux has several internal optimizations, such as the selectFactory we see here. When pure is different, different functions are selected for the calculation of props. If our pure is false, we evaluate each time to generate new props, which we pass to our internal component, triggering rerender.

When pure is true, React Redux caches the result of the last calculation and compares the result with the next one to see if it is the same. If it is, it returns the result of the last calculation. Once our component is pure, Passing in the same props will not cause the component rerender, achieving the purpose of performance optimization.

When the pure is false, call impureFinalPropsSelectorFactory computing props:

export function impureFinalPropsSelectorFactory(mapStateToProps, mapDispatchToProps, mergeProps, dispatch) {
  return function impureFinalPropsSelector(state, ownProps) {
    return mergeProps(
      mapStateToProps(state, ownProps),
      mapDispatchToProps(dispatch, ownProps),
      ownProps
    )
  }
}
Copy the code

This returns new props for each evaluation, causing the component to rerender all the time.

When pure true, call pureFinalPropsSelectorFactory computing props:

export function pureFinalPropsSelectorFactory(mapStateToProps, mapDispatchToProps, mergeProps, dispatch, { areStatesEqual, areOwnPropsEqual, areStatePropsEqual }) {
  let hasRunAtLeastOnce = false
  let state
  let ownProps
  let stateProps
  let dispatchProps
  let mergedProps

  function handleFirstCall(firstState, firstOwnProps) {
    state = firstState
    ownProps = firstOwnProps
    stateProps = mapStateToProps(state, ownProps)
    dispatchProps = mapDispatchToProps(dispatch, ownProps)
    mergedProps = mergeProps(stateProps, dispatchProps, ownProps)
    hasRunAtLeastOnce = true
    return mergedProps
  }

  function handleNewPropsAndNewState() {
    stateProps = mapStateToProps(state, ownProps)

    if (mapDispatchToProps.dependsOnOwnProps)
      dispatchProps = mapDispatchToProps(dispatch, ownProps)

    mergedProps = mergeProps(stateProps, dispatchProps, ownProps)
    return mergedProps
  }

  function handleNewProps() {
    if (mapStateToProps.dependsOnOwnProps)
      stateProps = mapStateToProps(state, ownProps)

    if (mapDispatchToProps.dependsOnOwnProps)
      dispatchProps = mapDispatchToProps(dispatch, ownProps)

    mergedProps = mergeProps(stateProps, dispatchProps, ownProps)
    return mergedProps
  }

  function handleNewState() {
    const nextStateProps = mapStateToProps(state, ownProps)
    conststatePropsChanged = ! areStatePropsEqual(nextStateProps, stateProps) stateProps = nextStatePropsif (statePropsChanged)
      mergedProps = mergeProps(stateProps, dispatchProps, ownProps)

    return mergedProps
  }

  function handleSubsequentCalls(nextState, nextOwnProps) {
    constpropsChanged = ! areOwnPropsEqual(nextOwnProps, ownProps)conststateChanged = ! areStatesEqual(nextState, state) state = nextState ownProps = nextOwnPropsif (propsChanged && stateChanged) return handleNewPropsAndNewState()
    if (propsChanged) return handleNewProps()
    if (stateChanged) return handleNewState()
    return mergedProps
  }

  return function pureFinalPropsSelector(nextState, nextOwnProps) {
    return hasRunAtLeastOnce
      ? handleSubsequentCalls(nextState, nextOwnProps)
      : handleFirstCall(nextState, nextOwnProps)
  }
}
Copy the code

Can see the process of calculation for the first time with impureFinalPropsSelectorFactory is consistent, but more a process of closures in the cache, in the subsequent calculation of props, according to the change of the state and props choose different function to calculate, This is done to minimize computation and optimize performance. If neither the state nor the props have changed, the props of the cache is returned.

You can see that there are several functions in this code to compare variables: areOwnPropsEqual, areStatesEqual, and areStatePropsEqual. We also saw the areMergedPropsEqual function, which was assigned when the connect function was defined:

areStatesEqual = strictEqual,
areOwnPropsEqual = shallowEqual,
areStatePropsEqual = shallowEqual,
areMergedPropsEqual = shallowEqual
Copy the code

StrictEqual definition:

function strictEqual(a, b) {
  return a === b
}
Copy the code

And shallowEqual is defined as follows:

// Eagle-eyed people may notice that this code comes from the React source code
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

You can see the contrast of the former is simple and rough, and the contrast of the latter is more delicate.

Why is the comparison of state different from the other three?

This is because state is special. Check out the source code of Redux: Combinereducers.ts. It is not difficult to find that when all the contents returned by our reducers remain unchanged (the original reference is maintained), the state we finally obtained (the object returned by store.getState ()) will also maintain the original reference, making oldState === newState established. So we’ll compare state in React Redux much easier than the other three.

Why can Redux ensure that the reducer returns the original state when the reducer does not modify the state, but the reference relationship of the reducer modified state must have changed? This is because REdux requires users to follow these requirements when defining reducer. When reducer produces new data, new objects must be created (by extending the syntax… Or Object.assisgn), which must return the old Object if no action. Type is matched. This can be seen in the previously mentioned combineReducers source code can still see many targeted checks.

After looking at section 2.4, we can actually see that selectorFactory does conform to the standard format mentioned at the beginning of section 2:

(dispatch, options) => (nextState, nextOwnProps) = > nextFinalProps
Copy the code

I want you to keep this conclusion in mind, because it will help us understand the relevant code later.

Now that you know how selectorFactory works, let’s look at how it is used inside connectAdvanced.

2.5 connectAdvanced

We’ve seen how React Redux calculates the props to use for the next render using the state and props. This section goes back to the connectAdvenced function to see when our props are evaluated.

The connectAdvanced function is actually the connect function we used, which takes the configuration and associated components and returns us a higher-order component.

Open the SRC/components/connectAdvanced js file, you can see the function after a few to do their part in front of the check, direct return a function has a large section of the code: WrapWithConnect, this function is about 300 lines long, and it’s what we call connect(…). The function that then returns, as you can imagine, takes a __ from our UI component and returns us a container component __.

Let’s go through the code and pick out the most important ones.

 constselectorFactoryOptions = { ... connectOptions, getDisplayName, methodName, renderCountProp, shouldHandleStateChanges, storeKey, displayName, wrappedComponentName, WrappedComponent }const { pure } = connectOptions

 function createChildSelector(store) {
   return selectorFactory(store.dispatch, selectorFactoryOptions)
 }
Copy the code

This code builds a selectorFactoryOptions object based on the parameters, and then creates a new createChildSelector function that calls the selectorFactory function we analyzed earlier. We know that the selectorFactory function conforms to our standard format, so calling selectorFactory gives us a new function that takes the props and the state and returns the props that we computed for the next rendering. So the result of createChildSelector is a props function. The reason for doing this is to calculate the props function for the current store so that it does not need to be called again when it needs to be evaluated later. This function is called again when our store object changes:

// The childPropsSelector function is configured according to mapStateToProps, mapDispatchToProps, etc
// Calculate the component props after the store update
const childPropsSelector = useMemo((a)= > {
  // The child props selector needs the store reference as an input.
  // Re-create this selector whenever the store changes.
  return createChildSelector(store)
}, [store])
Copy the code

You can see that this is also optimized using useMemo.

After some pre-work last night, we internally defined a ConnectFunction, which is what we’ll actually use for rendering and will be returned by Connect ()(). When we pass props to a container component, we pass ConnectFunction.

2.6 ConnectFunction

At the beginning, ConnectFunction prepares some data to be used:

const [propsContext, forwardedRef, wrapperProps] = useMemo((a)= > {
  // Distinguish between actual "data" props that were passed to the wrapper component,
  // and values needed to control behavior (forwarded refs, alternate context instances).
  // To maintain the wrapperProps object reference, memoize this destructuring.
  const{ forwardedRef, ... wrapperProps } = propsreturn [props.context, forwardedRef, wrapperProps]
}, [props])

// Calculate the Context to be used according to propsContext and Context, where propsContext is passed from outside and Context is passed from inside
// If ContainerComponent receives the context property, it uses the context it passed in, otherwise it uses the context.js definition.
// That's also the context that provides the store object.
const ContextToUse = useMemo((a)= > {
  // Users may optionally pass in a custom context instance to use instead of our ReactReduxContext.
  // Memoize the check that determines which context instance we should use.
  return propsContext &&
    propsContext.Consumer &&
    isContextConsumer(<propsContext.Consumer />)? PropsContext: Context}, [propsContext, Context]) // Use the Context via useContext and subscribe to its changes, re-render when the Context changes. // Retrieve the store and ancestor subscription via context, if available const contextValue = useContext(ContextToUse) // The store _must_ exist as either a prop or in context. // We'll check to see if it _looks_ like a Redux store first. // This allows us to pass through a `store` prop that is just // Check if there is a store in the props and context. If there is no store in the context, it cannot play. // So we can also pass ContainerComponent a store props as our connect store const didStoreComeFromProps = Boolean(props.store) && Boolean(props.store.getState) && Boolean(props.store.dispatch) const didStoreComeFromContext = Boolean(contextValue) && Boolean(contextValue.store) // Based on the previous check, One of these must be true // Use the store passed in. If ContainerComponent receives a store, use it as a store. // It is possible to pass the props of the DisplayComponent into the store, and if it does, the component will use the store of the props instead of the context. const store = didStoreComeFromProps ? props.store : contextValue.storeCopy the code

The props in this code is the props we pass to the container component, from which we parse out our forwardedRef, context, and other props properties.

The forwardedRef is used to transfer the ref property we set on the container component to the internal UI component via the React.

The context property is used to calculate the context that we’re going to use. Other props Calculates the props used by the UI component.

When deciding which context to use, use the values passed by it through the useContext API, so we use React Hooks. We use the useContext API to access the contents of the Provider. Without using the Consumer component.

React Redux will use the store as the data source if we pass the store property to our container component. It’s not a store object in the top-level Context.

Instead of thinking about how the component subscribes to store updates, we can look at how the PROPS needed by the UI component are calculated and applied.

// The childPropsSelector function is configured according to mapStateToProps, mapDispatchToProps, etc
// Calculate the component props after the store update
const childPropsSelector = useMemo((a)= > {
  // The child props selector needs the store reference as an input.
  // Re-create this selector whenever the store changes.
  return createChildSelector(store)
}, [store])
Copy the code

The createChildSelector function is used to call selectorFactory and returns the result of a single selectorFactory call. Since selectorFactory conforms to our design specification: selectorFactory (selectorFactory)

(dispatch, options) => (nextState, nextOwnProps) = > nextFinalProps
Copy the code

So the function returned by childPropsSelector conforms to the following specification:

(nextState, nextOwnProps) => nextFinalProps
Copy the code

In the second half of the function, we have the following code:

// If we aren't running in "pure" mode, we don't want to memoize values.
// To avoid conditionally calling hooks, we fall back to a tiny wrapper
// that just executes the given callback immediately.
const usePureOnlyMemo = pure ? useMemo : callback= > callback()

// Finally used props
const actualChildProps = usePureOnlyMemo((a)= > {
  / /... Ignore some code
  return childPropsSelector(store.getState(), wrapperProps)
}, [store, previousStateUpdateResult, wrapperProps])
Copy the code

As you can see, this is also based on the options.pure option, If not true will update each store, previousStateUpdateResult or wrapperProps leads to actualChildProps recount.

So the actualChildProps here is the nextFinalProps in the specification above us.

After calculating the final props, we can render our UI component:

// Now that all that's done, we can finally try to actually render the child component.
// We memoize the elements for the rendered child component as an optimization.
const renderedWrappedComponent = useMemo(
  (a)= ><WrappedComponent {... actualChildProps} ref={forwardedRef} />, [forwardedRef, WrappedComponent, actualChildProps] )Copy the code

The WrappedComponent in the code above is the UI component we passed in, and you can see that the forwardedRef ends up pointing to the internal UI component.

The renderedWrappedComponent is the result of our rendering of the UI component, but we can’t just take that and give it back to the user for rendering. We need to consider other scenarios. Let’s look at how the UI component subscris to store updates.

Third, Subscriptions

The Subscription object, which allows components to subscribe to store updates, is key to the React Redux implementation. Let’s dive into the implementation and functionality of the API.

Open our SRC/utils/Subscription. Js file, the file is a total of two functions: createListenerCollection and Subscription. The former is the auxiliary tool, the latter is the API we actually use.

Let’s start with the utility function:

const CLEARED = null
const nullListeners = { notify() {} }

function createListenerCollection() {
  const batch = getBatch()
  // the current/next pattern is copied from redux's createStore code.
  // TODO: refactor+expose that code to be reusable here?
  // We use two queues to avoid the edge behavior caused by the notify
  // Reference: https://github.com/reduxjs/react-redux/pull/1450#issuecomment-550382242
  let current = []
  let next = []

  return {
    clear() {
      next = CLEARED
      current = CLEARED
    },

    notify() {
      const listeners = (current = next)
      batch((a)= > {
        for (let i = 0; i < listeners.length; i++) {
          listeners[i]()
        }
      })
    },

    get() {
      return next
    },

    subscribe(listener) {
      let isSubscribed = true
      if (next === current) next = current.slice()
      next.push(listener)

      return function unsubscribe() {
        if(! isSubscribed || current === CLEARED)return
        isSubscribed = false

        if (next === current) next = current.slice()
        next.splice(next.indexOf(listener), 1)}}}}Copy the code

You can see that there are two queues defined inside the object returned by this function, one next and one current, which are used to hold the listener to subscribe to the update of the current object, and one queue for the next update. This is to prevent edge problems caused by the call to SUBSCRIBE or unsubscribe after notify is executed. Each notify is preceded by synchronization of current to Next, and subsequent subscribe executions are executed on top of next.

So this is a function that returns a pure object, and this object is an event publication and subscription center. This is an application of the observer mode. All of our Listeners are going to listen to the current object, and when the current object calls notify, all of them are going to be executed. The time to subscribe or notify the object depends on the user of the object.

The subscription object is provided below:

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

    // Bind this, because it will be executed elsewhere later
    this.handleChangeWrapper = this.handleChangeWrapper.bind(this)
  }

  addNestedSub(listener) {
    // Execute the trySubscribe function to determine the current instance's subscription target (parentSub or store).
    this.trySubscribe()
    // Sublisteners are managed on this.listeners
    return this.listeners.subscribe(listener)
  }

  notifyNestedSubs() {
    this.listeners.notify()
  }

  handleChangeWrapper() {
    // this.onStateChange is provided externally
    if (this.onStateChange) {
      this.onStateChange()
    }
  }

  isSubscribed() {
    return Boolean(this.unsubscribe)
  }

  trySubscribe() {
    if (!this.unsubscribe) {
      this.unsubscribe = this.parentSub
        ? this.parentSub.addNestedSub(this.handleChangeWrapper)
        : this.store.subscribe(this.handleChangeWrapper)

      this.listeners = createListenerCollection()
    }
  }

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

Let’s start with its constructor, which takes two arguments: Store and parentSub. The store is what you think of as store, which is Redux’s data center. It is used in another function of the class, trySubscribe:

trySubscribe() {
  if (!this.unsubscribe) {
    this.unsubscribe = this.parentSub
      ? this.parentSub.addNestedSub(this.handleChangeWrapper)
    	: this.store.subscribe(this.handleChangeWrapper)

    this.listeners = createListenerCollection()
  }
}
Copy the code

If there is no unsubscribe attribute, the next step will be calculated based on whether we have a parentSub attribute. This indicates that our parentSub is an optional parameter. If parentSub is not present, store. Subscribe is used directly to subscribe to store updates, and when data is updated, the handleChangeWrapper function is executed. If there is a parentSub attribute, the addNestedSub function of parentSub is executed, and since this function exists on the Subscription class, you can assume that parentSub is an instance of Subscription.

After the unsubscribe initialization, the listeners are initialized, using the tool function we mentioned earlier.

We see to subscribe to store update function is the Subscription. The prototype. HandleChangeWrapper:

handleChangeWrapper() {
  // this.onStateChange is provided externally
  if (this.onStateChange) {
    this.onStateChange()
  }
}
Copy the code

The onStateChange function is not defined on Subscription, which means that the function is defined when it is used. We’ll see that later when we read the code that uses this class.

Let’s look again at how Subscription uses our listener:

addNestedSub(listener) {
  // Execute the trySubscribe function to determine the current instance's subscription target (parentSub or store).
  this.trySubscribe()
  // Sublisteners are managed on this.listeners
  return this.listeners.subscribe(listener)
}

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

AddNestedSub is the function that is called when an instance of Subscription is called as a parentSub property of another Subscription instance, The child Subscription instance’s handleChangeWrapper function is registered with the parent Subscription instance’s listeners. When the parent Subscription instance calls notifyNestedSub, All subsubscription handleChangeWrapper functions are executed.

React Redux provides a tree of subscriptions and listeners. The Subscription instance at the top subscribes to store changes. Store changes execute handleChangeWrapper, and if our handleChangeWrapper function (which internally executes onStateChange) calls notifyNestedSub, All low-level Subscription instances will be updated. The handleChangeWrapper function of the child Subscription instance is executed. This is a top-down event passing mechanism, ensuring that events at the top are passed down the hierarchy.

Schematic diagram:

It is clear from this diagram how Subscription implements the top-down distribution of events.

Let’s go back and see how we implemented component subscription Store data updates.

const [subscription, notifyNestedSubs] = useMemo((a)= > {
  if(! shouldHandleStateChanges)return NO_SUBSCRIPTION_ARRAY

  // This Subscription's source should match where store came from: props vs. context. A component
  // connected to the store via props shouldn't use subscription from context, or vice versa.
  // If store comes from the lowest Provider, parentSub should also come from the Provider
  const subscription = new Subscription(
    store,
    didStoreComeFromProps ? null : contextValue.subscription
  )

  // `notifyNestedSubs` is duplicated to handle the case where the component is unmounted in
  // the middle of the notification loop, where `subscription` will then be null. This can
  // probably be avoided if Subscription's listeners logic is changed to not call listeners
  // that have been unsubscribed in the middle of the notification loop.
  const notifyNestedSubs = subscription.notifyNestedSubs.bind(
    subscription
  )

  return [subscription, notifyNestedSubs]
}, [store, didStoreComeFromProps, contextValue])
Copy the code

This is the subscription instance and notifyNestedSub function used by the current component.

The first line of code identifies that subscription is not required if the user is configured not to handle store changes.

The subscription object for the current component is initialized. If the store comes from props, the current component is the topmost component in the subscription tree. It has no parentSub and subscribes directly to changes in the store.

If the current store is from context, it may not be a top-level Subscription instance, and the Subscription attribute is included in contextValue. If so, you need to instantiate it as parentSub.

Finally, the notifyNestedSub function is computed, which is bound because, as I drew earlier in the Subscription tree, it is called as a handleChangeWrapper function for the Subscription instance, So make sure that this doesn’t change.

It is easy to ask why there is a Subscription instance passed in contextValue. We did not see this property when we looked at the source code of the Provider component. The following code overrides the contextValue passed to the lower component to ensure that the lower component is able to retrieve the subscription instance from the upper component to build the tree:

// Determine what {store, subscription} value should be put into nested context, if necessary,
// and memoize that value to avoid unnecessary context updates.
const overriddenContextValue = useMemo((a)= > {
  if (didStoreComeFromProps) {
    // This component is directly subscribed to a store from props.
    // We don't want descendants reading from this store - pass down whatever
    // the existing context value is from the nearest connected ancestor.
    return contextValue
  }

  // Otherwise, put this component's subscription instance into context, so that
  // connected descendants won't update until after this component is done
  // If the store comes from the underlying Provider, pass the subscription level down.
  return {
    ...contextValue,
    subscription
  }
}, [didStoreComeFromProps, contextValue, subscription])
Copy the code

The overriddenContextValue property is the overwritten contextValue, and you can see that it passes the subscription of the current component to the next layer, which goes back to the part left off in section 2.6. This is the last piece of code for ConnectFunction:

// If React sees the exact same element reference as last time, it bails out of re-rendering
// that child, same as if it was wrapped in React.memo() or returned false from shouldComponentUpdate.
const renderedChild = useMemo((a)= > {
  if (shouldHandleStateChanges) {
    // If this component is subscribed to store updates, we need to pass its own
    // subscription instance down to our descendants. That means rendering the same
    // Context instance, and putting a different value into the context.
    return (
      <ContextToUse.Provider value={overriddenContextValue}>
        {renderedWrappedComponent}
    	</ContextToUse.Provider>
    )
  }

  return renderedWrappedComponent
}, [ContextToUse, renderedWrappedComponent, overriddenContextValue])

return renderedChild
Copy the code

If we are configured to handle store updates, we will re-wrap the next layer of component (our UI component) with the Provider, and the component will receive the contextValue we rewrote: OverriddenContextValue.

So when the next layer of the component is connected, its ConnectFunction can retrieve the subscription object from the previous layer in contextValue, associating the component tree. This is an important step!

Once the subscription tree between component trees is built, we need to look at how events are passed between them.

Inside ConnectFunction, a series of refs are defined:

// Set up refs to coordinate values between the subscription effect and the render logic
const lastChildProps = useRef()
const lastWrapperProps = useRef(wrapperProps)
const childPropsFromStoreUpdate = useRef()
const renderIsScheduled = useRef(false)
Copy the code

We know that the React function component has a separate context each time it renders. Close reading useEffect complete guide), in case each ConnectFunction render does not get the parameters of the last render, we need ref for state preservation.

The four refs here retain the props from the last UI component rendering, the wrapperProps data from the last one, and the contents of the two flag variables. Including childPropsFromStoreUpdate representative due to Store update new props, whether needs a rerender renderIsScheduled representative.

// Finally used props
const actualChildProps = usePureOnlyMemo((a)= > {
  // Tricky logic here:
  // - This render may have been triggered by a Redux store update that produced new child props
  // - However, we may have gotten new wrapper props after that
  // If we have new child props, and the same wrapper props, we know we should use the new child props as-is.
  // But, if we have new wrapper props, those might change the child props, so we have to recalculate things.
  // So, we'll use the child props from store update only if the wrapper props are the same as last time.
  if (
    childPropsFromStoreUpdate.current &&
    wrapperProps === lastWrapperProps.current
  ) {
    return childPropsFromStoreUpdate.current
  }

  // TODO We're reading the store directly in render() here. Bad idea?
  // This will likely cause Bad Things (TM) to happen in Concurrent Mode.
  // Note that we do this because on renders _not_ caused by store updates, we need the latest store state
  // to determine what the child props should be.
  return childPropsSelector(store.getState(), wrapperProps)
}, [store, previousStateUpdateResult, wrapperProps])
Copy the code

The calculation process, this is a complete actualChildProps above is not difficult to found about childPropsFromStoreUpdate and lastWrapperProps contrast and its annotation.

If a new props is generated due to an update of the store and the current wrapperProps is not changed, the new props can be used directly. If the current wrapperProps is changed, the new props cannot be used directly. Because changes in wrapperProps may cause changes in the calculation results.

We continue to find the subscription code for the Subscription instance:

// Actually subscribe to the nearest connected ancestor (or store)
// Subscribe to upper-layer subscription notifications. Store state changes when notifications are received
// Check whether re-render is required, at which point checkForUpdates will be executed
subscription.onStateChange = checkForUpdates
subscription.trySubscribe()
Copy the code

The onStateChange function for the current subscription is set to checkForUpdates. If the current subscription receives a store update message, The checkForUpdates function is executed to update the relevant state and rerender.

So let’s go ahead and find the implementation code for the checkForUpdates function:

// We'll run this callback every time a store subscription update propagates to this component
// Execute this function every time you receive notification of a store state update
const checkForUpdates = (a)= > {
  if (didUnsubscribe) {
    // Don't run stale listeners.
    // Redux doesn't guarantee unsubscriptions happen until next dispatch.
    return
  }

  const latestStoreState = store.getState()

  let newChildProps, error
  try {
    // Actually run the selector with the most recent store state and wrapper props
    // to determine what the child props should be
    newChildProps = childPropsSelector(
      latestStoreState,
      lastWrapperProps.current
    )
  } catch (e) {
    error = e
    lastThrownError = e
  }

  if(! error) { lastThrownError =null
  }

  // If the child props haven't changed, nothing to do here - cascade the subscription update
  if (newChildProps === lastChildProps.current) {
    if(! renderIsScheduled.current) { notifyNestedSubs() } }else {
    // Save references to the new child props. Note that we track the "child props from store update"
    // as a ref instead of a useState/useReducer because we need a way to determine if that value has
    // been processed. If this went into useState/useReducer, we couldn't clear out the value without
    // forcing another re-render, which we don't want.
    lastChildProps.current = newChildProps
    childPropsFromStoreUpdate.current = newChildProps
    renderIsScheduled.current = true

    // If the child props _did_ change (or we caught an error), this wrapper component needs to re-render
    / / if you need to update, compulsory execution forceComponentUpdateDispatch function to update the current component, it will be finished by the subscription
    // Update state and re-render component
    forceComponentUpdateDispatch({
      type: 'STORE_UPDATED'.payload: {
        error
      }
    })
  }
}
Copy the code

When the store update event arrives, a newChildProps (our new props) will be computed. Any calculation errors will be saved.

If the calculated props is the same as the result referenced by lastChildProps, the data has not changed. If the component does not have an update plan, the notifyNestedSubs function needs to be manually triggered to notify the child component of the update.

If the calculated props is not equal to the previous props, it indicates that the store update causes the props to change, and the related references need to be updated and the current component is triggered to update. After the current component is updated, the calculation of ConnectFunction starts a new round. So back to the calculation of the actualChildProps data. This is why updates to the props and wrapperProps are considered in the calculation of actualChildProps.

We see checkForUpdates update current component is called the forceComponentUpdateDispatch function, we see its implementation:

function storeStateUpdatesReducer(state, action) {
  const [, updateCount] = state
  return [action.payload, updateCount + 1]}// We need to force this wrapper component to re-render whenever a Redux store update
// causes a change to the calculated child component props (or we caught an error in mapState)
// Force an update of the current component through the useReducer, since the state changes after each dispatch
The second parameter of the array returned by storeStateUpdatesReducer will always be increased
const [
  [previousStateUpdateResult],
  forceComponentUpdateDispatch
] = useReducer(storeStateUpdatesReducer, EMPTY_ARRAY, initStateUpdates)

// If the underlying component catches an error while using it, it is thrown at the current layer
// Propagate any mapState/mapDispatch errors upwards
if (previousStateUpdateResult && previousStateUpdateResult.error) {
  throw previousStateUpdateResult.error
}
Copy the code

Here we construct a data and an update function using useReducer Hooks.

We see the reducer is a function of a fixed update every time (updateCount ever since the increase), so each call forceComponentUpdateDispatch will lead to the current components to rendering. The source of the error in the data is the error captured when the checkForUpdates calculation is performed for the next props:

forceComponentUpdateDispatch({
  type: 'STORE_UPDATED'.payload: {
    error
  }
})
Copy the code

If errors are made during the calculation, they will be thrown out during the next rendering.

We need to update the previous reference after the component is rendered, so we have this code:

 // We need this to execute synchronously every time we re-render. However, React warns
// about useLayoutEffect in SSR, so we try to detect environment and fall back to
// just useEffect instead to avoid the warning, since neither will run anyway.
// Use useLayoutEffect to do something after the DOM update. Here, update some internal refs after the DOM update
// Make sure the ref used for the next judgment is up to date
useIsomorphicLayoutEffect((a)= > {
  // We want to capture the wrapper props and child props we used for later comparisons
  lastWrapperProps.current = wrapperProps
  lastChildProps.current = actualChildProps
  renderIsScheduled.current = false

  // If the render was from a store update, clear out that reference and cascade the subscriber update
  if (childPropsFromStoreUpdate.current) {
    childPropsFromStoreUpdate.current = null
    notifyNestedSubs()
  }
})
Copy the code

You can also see that after rendering is complete, all child components are notified that the store data has been updated.

It is worth noting that the useIsomorphicLayoutEffect this custom Hook:

// React currently throws a warning when using useLayoutEffect on the server.
// To get around it, we can conditionally useEffect on the server (no-op) and
// useLayoutEffect in the browser. We need useLayoutEffect to ensure the store
// subscription callback always has the selector from the latest render commit
// available, otherwise a store update may happen between render and the effect,
// which may cause missed updates; we also must ensure the store subscription
// is created synchronously, otherwise a store update may occur before the
// subscription is created and an inconsistent state may be observed

Reference: / / https://reactjs.org/docs/hooks-reference.html#uselayouteffect

export const useIsomorphicLayoutEffect =
  typeof window! = ='undefined' &&
  typeof window.document ! = ='undefined' &&
  typeof window.document.createElement ! = ='undefined'
    ? useLayoutEffect
    : useEffect
Copy the code

Since useLayoutEffect is not suitable for SSR scenarios, useEffect is used as fallback.

Four, conclusion

React Redux does not need to be pasted as much as possible to avoid large chunks of code, so it may be difficult to read. I suggest you go to GitHub to see the source code, including the relevant notes, what questions can also be put forward in issues.