preface

Those who have used Redux know that Redux, as a react public state management tool, can manage data, distribute updates and update view rendering well in combination with React-Redux. So how can React-Redux update components according to the state change? Let’s explore the mystery of the React-Redux source code.

Before the formal analysis, let’s think about a few questions:

Why use the Provider package in React-Redux on the root component? How does react-Redux fit redux? How does react-Redux fit redux? How does 3 provide store the current redux and how does it pass to each component that needs to manage state? 4 How does Connect connect our business components and then pass our component update functions? 5 How does connect subscribe to the corresponding state with the first parameter? 6 Connect: props, props, redux

With that in mind, let’s take a look at what the Provider does.

A Provider creates Subscription, and a context holds the context

/* Provider component code */
function Provider({ store, context, children }) {
   /* useMemo creates a contextValue containing a root subscriber and the current store */ 
  const contextValue = useMemo(() = > {
      /* Create a root Subscription */
    const subscription = new Subscription(store)
    /* Subscription of notifyNestedSubs, assigned to onStateChange */
    subscription.onStateChange = subscription.notifyNestedSubs  
    return {
      store,
      subscription
    } /* Store change creates a new contextValue */
  }, [store])
  /* Get the state value before the update. The context in the function component takes precedence over the component update rendering */
  const previousState = useMemo(() = > store.getState(), [store])

  useEffect(() = > {
    const { subscription } = contextValue
    /* Trigger the trySubscribe method to execute, creating listens */
    subscription.trySubscribe() // Initiate a subscription
    if(previousState ! == store.getState()) {/ * component update rendering, if the state changes, then immediately trigger subscription. * / notifyNestedSubs method
      subscription.notifyNestedSubs() 
    }
    /*   */
    return () = > {
      subscription.tryUnsubscribe()  // Uninstall the subscription
      subscription.onStateChange = null
    }
    /* contextValue state change to create effect */
  }, [contextValue, previousState])

  const Context = context || ReactReduxContext
  If createContext does not create a context, then ReactReduxContext is the context created by createContext */
  return <Context.Provider value={contextValue}>{children}</Context.Provider>
}

Copy the code

The provider function from the source code is roughly like this

First, create a contextValue with a created parent Subscription (let’s call it the root subscriber) and a store provided by redux. Use react context context to pass contextValue to descendant components.

Subscription message, publish updates

After we analyze the provider source code that is not very long, a Subscription appears. What does this Subscription do πŸ€”πŸ€”πŸ€”? Let’s first look at the Subscription method that appears in the Provder.

notifyNestedSubs trySubscribe tryUnsubscribe

Subscription is important throughout the React-Redux execution, and is used to collect onStatechange updates for all components wrapped in connect and form a linked list of callbacks. The parent Subscription uniformly distributes updates, and we don’t care how it works. The Subscription source code follows, focusing on the three methods shown above.

/* Publish subscriber mode */
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)}/* Is responsible for detecting whether the component is subscribed, and then adds the subscriber, known as listener */
  addNestedSub(listener) {
    this.trySubscribe()
    return this.listeners.subscribe(listener)
  }
  /* Send notifications to Listeners */
  notifyNestedSubs() {
    this.listeners.notify()
  }
  /* Provide onStateChange is the notifyNestedSubs method, and provide onStateChange is the function responsible for updating components that connect packages to accept updates. * /
  handleChangeWrapper() {
    if (this.onStateChange) {
      this.onStateChange()
    }
  }
   /* Check whether subscription is enabled */
  isSubscribed() {
    return Boolean(this.unsubscribe)
  }
  /* To enable Subscription mode, determine if the current subscriber has a parent subscriber. If it does, add its own handleChangeWrapper to the listener list */
  trySubscribe() {
    /* parentSub is the Subscription in the provide value and is understood as the parent element */
    if (!this.unsubscribe) {
      this.unsubscribe = this.parentSub
        ? this.parentSub.addNestedSub(this.handleChangeWrapper)
        /* Provider Subscription does not include parentSub, so trySubscribe calls store.subscribe */
        : this.store.subscribe(this.handleChangeWrapper)
      this.listeners = createListenerCollection()
    }
  }
  /* Unsubscribe */
  tryUnsubscribe() {
    if (this.unsubscribe) {
      this.unsubscribe()
      this.unsubscribe = null
      this.listeners.clear()

      this.listeners = nullListeners
    }
  }
}


Copy the code

Provider creates Subscription without a second parameter. Provider Subscription does not contain parentSub. When trySubscribe is called in the Provider’s useEffect hook, it triggers this.store. Subscribe. The subscribe is a redux subscribe.

subscription.onStateChange = subscription.notifyNestedSubs 
Copy the code

In this case, the final state change triggers notifyNestedSubs. Let’s look at the notifyNestedSubs again.

/* Send notifications to Listeners */
notifyNestedSubs() {
  this.listeners.notify()
}
Copy the code

The notify update is eventually released to subscribers of the current Subscription.

Subscription summary – Publish an implementation of the Subscription pattern

To sum up, let’s summarize. Subscription, if there is a parent, passes its own update function handleChangeWrapper to the parent. The parent then adds the callback function (the update function) to the current listeners by the addNestedSub method. If there is no parent element (in the case of a Provider), place this callback in store.subscribe, onStateChange in the handleChangeWrapper function, This is the notifyNestedSubs method of the Provider Subscription, and notifyNestedSubs tells the notify method of listens to trigger the update. As a reminder, the child Subscription passes the updated handleChangeWrapper to parentSub to uniformly notify connect of updates.

Here we have a problem

react-reduxThe update component is also usedstore.subscribeandstore.subscribeOnly in theProvider ηš„ Subscription(there is noparentsub )

The rough model is

stateChange – >store.subscribe– > the triggerprovider ηš„ Subscription ηš„ handleChangeWrapperThat isnotifyNestedSubs– > notificationlisteners.notify()-> Notify each byconnectContainer Component Updates ->callbackExecute -> trigger subcomponentsSubscriptionHandleChangeWrapper -> triggersonstatechange(I can tell you in advance,onstatechangeSaves the function that updates the component.

In front of the content of the mentioned createListenerCollection * *, * *, listeners but he specifically what role we together to have a look at the next.

function createListenerCollection() {
   Unstable_batchedUpdates method obtained from getBatch */
  const batch = getBatch()
  let first = null
  let last = null

  return {
    /* Clear all listeners */
    clear() {
      first = null
      last = null
    },
    /* Distribute updates */
    notify() {
      batch(() = > {
        let listener = first
        while (listener) {
          listener.callback()
          listener = listener.next
        }
      })
    },
    /* θŽ·ε–listenersηš„ζ‰€ζœ‰listener */
    get() {
      let listeners = []
      let listener = first
      while (listener) {
        listeners.push(listener)
        listener = listener.next
      }
      return listeners
    },
     /* Receive the subscription and store the current callback (handleChangeWrapper) in the current linked list */
    subscribe(callback) {
      let isSubscribed = true

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

      if (listener.prev) {
        listener.prev.next = listener
      } else {
        first = listener
      }
      Unsubscribe from the current handleChangeWrapper */
      return function unsubscribe() {
        if(! isSubscribed || first ===null) return
        isSubscribed = false

        if (listener.next) {
          listener.next.prev = listener.prev
        } else {
          last = listener.prev
        }
        if (listener.prev) {
          listener.prev.next = listener.next
        } else {
          first = listener.next
        }
      }
    }
  }
}
Copy the code

batch

import { unstable_batchedUpdates as batch } from './utils/reactBatchedUpdates'
setBatch(batch)
Copy the code

We can conclude that createListenerCollection can produce a listeners. The role of Listeners.

Collect subscriptions: A linked list of handleChangeWrapper functions is collected for each listener. 2. Dispatch updates: Batch updates are carried out through the batch method (unstable_batchedUpdates in React-DOM).

Note: React’s unstable_batchedUpdate() API allows all React updates in an event loop to be batch processed into one render.

conclusion

πŸ€” Here we understand:

Provider in React-Redux passes subscription and store in Redux through the React context, and creates a topmost root subscription.

Subscribe to connect component updates and distribute updates via store.subscribe

If there is such a parent, it passes its own update function to the parent to unify the Subscription.

What exactly does Connect do?

1 review connect usage

If you want to do a good job, you must first sharpen its tools. Before you want to understand the source code, you must be deeply familiar with its usage. To know what it is and why it is. Let’s first look at the use of the high-level component Connect.

function connect(mapStateToProps? , mapDispatchToProps? , mergeProps? , options?)
Copy the code

mapStateToProps

const mapStateToProps = state= > ({ todos: state.todos })
Copy the code

The function is very simple, the component depends on the state of redux, mapped to the props of the business component, the state change triggers, the business component props change, triggers the business component to update the view. When this parameter is not present, the current component does not subscribe to store changes.

mapDispatchToProps

const mapDispatchToProps = dispatch= > {
  return {
    increment: () = > dispatch({ type: 'INCREMENT' }),
    decrement: () = > dispatch({ type: 'DECREMENT' }),
    reset: () = > dispatch({ type: 'RESET'}}})Copy the code

Map the dispatch method in the redux to the props of the business component.

mergeProps

/* * stateProps, contents mapped to props by state * dispatchProps, contents mapped to props by dispatch. * ownProps props */ for the ownProps component
(stateProps, dispatchProps, ownProps) => Object
Copy the code

Normally, if you don’t have this argument, the merge will be done as follows, and the object returned can be our own merge rule. We can also attach some attributes.

{... ownProps, ... stateProps, ... dispatchProps }Copy the code

options

{ context? :Object.// Customize the contextpure? : boolean,// The default is true. When this is true, the component will not be updated if any changes are made to the input or state except for mapStateToProps and props.areStatesEqual? :Function.// When pure true, compare whether the state value in the imported store is the same as before. (next: Object, prev: Object) => booleanareOwnPropsEqual? :Function.// Props = props (next: Object, prev: Object) => booleanareStatePropsEqual? :Function.// When pure true, compare mapStateToProps to props. (next: Object, prev: Object) => booleanareMergedPropsEqual? :Function.// If pure is true, compare mergeProps with the previous values (next: Object, prev: Object) => BooleanforwardRef? : boolean,// When true, you can get the component instance wrapped by connect through ref.
}
Copy the code

Options can be the properties described above. The functions of each of these properties are marked above, so I won’t go into details here.

2 connect a preliminary

For the Connect component, let’s take a look at the source code first

/src/connect/connect.js

export function createConnect({ connectHOC = connectAdvanced, mapStateToPropsFactories = defaultMapStateToPropsFactories, mapDispatchToPropsFactories = defaultMapDispatchToPropsFactories, mergePropsFactories = defaultMergePropsFactories, selectorFactory = defaultSelectorFactory } = {}) {
  return function connect(
    mapStateToProps,
    mapDispatchToProps,
    mergeProps,
    {
      pure = true, areStatesEqual = strictEqual, areOwnPropsEqual = shallowEqual, areStatePropsEqual = shallowEqual, areMergedPropsEqual = shallowEqual, ... extraOptions } = {}) {
   
     /* mapStateToProps */
    const initMapStateToProps = match( mapStateToProps, mapStateToPropsFactories,'mapStateToProps' )
    /* mapDispatchToProps */
    const initMapDispatchToProps = match(  mapDispatchToProps, mapDispatchToPropsFactories,'mapDispatchToProps')
     /* mergeProps */
    const initMergeProps = match(mergeProps, mergePropsFactories, 'mergeProps')

    return connectHOC(selectorFactory, {
     
      methodName: 'connect'.getDisplayName: name= > `Connect(${name}) `.shouldHandleStateChanges: Boolean(mapStateToProps), initMapStateToProps, initMapDispatchToProps, initMergeProps, pure, areStatesEqual, areOwnPropsEqual, areStatePropsEqual, areMergedPropsEqual, ... extraOptions }) } }export default /*#__PURE__*/ createConnect()
Copy the code

So let’s analyze what the function does first.

Create a createConnect method first. A few default parameters are passed in, two of which are very important, connectHOC as the higher-order component of the entire connect. SelectorFactory is used as the main function to form new props during the connect update process. The default mode is Pure.

2 Then execute the createConnect method, returning the real connect function itself. Connect takes a few parameters and then integrates them with the default function, wrapping, proxying, and finally forming three real initialization functions, which we’ll skip over here. We will describe the purpose of each of the three functions.

InitMapStateToProps is used to create a real MapStateToProps function, mapping the state in the store to the props

InitMapDispatchToProps, used to form true MapDispatchToProps, injecting dispatches and custom dispatches into props.

InitMergeProps, used to form real mergeProps, props for merging business components, props for state mapping, and props for dispatch mapping.

This is mergeProps. Remember this function because it is the key to determining whether the entire connect component is updated. When we do not pass mergeProps to connect, the default is the following

/src/connect/mergeProps.js

export function defaultMergeProps(stateProps, dispatchProps, ownProps) {
  return{... ownProps, ... stateProps, ... dispatchProps } }Copy the code

This function returns a new object, the new props. In addition, the business component props, state in store, and Dispatch are combined to form a new object and passed to the business component as the new props.

3 selectorFactory forms new props

SelectorFactory selectorFactory selectorFactory selectorFactory

/src/connect/selectorFactory.js

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

  const selectorFactory = options.pure ? pureFinalPropsSelectorFactory : impureFinalPropsSelectorFactory
   // Return a function to generate new props
  return selectorFactory(
    mapStateToProps,
    mapDispatchToProps,
    mergeProps,
    dispatch,
    options
  )
}
Copy the code

FinalPropsSelectorFactory code is very simple, first of all get really connect through a proxy function layer mapStateToProps, mapDispatchToProps, mergeProps. Then call selectorFactory (in pure mode, selectorFactory pureFinalPropsSelectorFactory).

You can use closures over and over again, so it’s a little bit confusing at first, but it’s not that hard to look at. Because the default is pure, so we basically see pureFinalPropsSelectorFactory function what did you do next.

/** This is the props */. This is the props */
export function pureFinalPropsSelectorFactory(
  mapStateToProps,
  mapDispatchToProps,
  mergeProps,
  dispatch,
  { areStatesEqual, areOwnPropsEqual, areStatePropsEqual } // Determine whether state prop is equal
) {
  let hasRunAtLeastOnce = false
  let state
  let ownProps
  let stateProps
  let dispatchProps
  let mergedProps
 
  /* ownProps stateProps dispatchProps are merged to form new props */
  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() {
    // both props and state change mergeProps
  }

  function handleNewProps() {
    // props change mergeProps
  }

  function handleNewState() {
     // state changes mergeProps
  }

  /* This is not the case for the first time (props or store.state). * /
  function handleSubsequentCalls(nextState, nextOwnProps) {
      /* Check whether the two props are the same */
    constpropsChanged = ! areOwnPropsEqual(nextOwnProps, ownProps)/* Determine whether store.state is equal to store.state */
    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

The logic of this function is pretty clear. Basically, we did these things. Returns a function pureFinalPropsSelector in the form of a closure. PureFinalPropsSelector determines if the component is initialized for the first time.

If it is the first time, then direct call mergeProps merger ownProps, stateProps, dispatchProps form the props. If it is not the first time, then determine whether the props or store.state changed, and then recreate the corresponding props based on the change, and finally merge it into the real props.

The wholeselectorFactoryLogic is the formation of newpropsThe business components passed to us.

4 connectAdvanced forms a Hoc that really wraps the business component

Let’s take a look at what the connectAdvanced() return from connect does. To help you understand connect, let’s take a look at connect.

In normal mode:

const mapStateToProp = (store) = > ({ userInfo: store.root.userInfo })

function Index(){
    / *... * /
    return <div>{/ *... * /}</div>
}
export default connect(mapStateToProp)(Index)
Copy the code

Decorator mode:

const mapStateToProp = (store) = > ({ userInfo: store.root.userInfo })

@connect(mapStateToProp)
class Index extends React.Component{
    /* .... */
    render(){
        return <div>{/ *... * /}</div>}}Copy the code

As we mentioned above, the connect execution accepts parameters like mapStateToProp and returns connectAdvanced(), So in the example above connect performs the first step connect(mapStateToProp)===connectAdvanced(), that is, connectAdvanced() executes to return the real Hoc used to wrap our business component.

Next let’s look at the connectAdvanced code

/src/components/connectAdvanced.js

export default function connectAdvanced(
  selectorFactory, // Each time the props,state is changed, the new props are generated.
  {
    getDisplayName = name => `ConnectAdvanced(${name}) `.//MethodName = may be overridden by a wrapped function such as connect ()'connectAdvanced'.//If so, the name of the attribute passed to the wrapping element, indicating the call to render. Used to monitor unnecessary rerenders in React DevTools. renderCountProp =undefined,
    shouldHandleStateChanges = true.//Determines whether this HOC subscribs to store change storeKey ='store',
    withRef = false,
    forwardRef = false.//ForwarRef mode context = ReactReduxContext//The context saved by the Provider... connectOptions } = {}) {
  /* ReactReduxContext is an existing context */
  const Context = context
   /* WrappedComponent is the component of connect */   
  return  function wrapWithConnect(WrappedComponent){
      // WrappedComponent Connects to the business component itself}}Copy the code

ConnectAdvanced accepts the configuration parameters and then returns the true HOC wrapWithConnect.

// We can talk about the following decomposition of the expression
connect(mapStateToProp)(Index)

/ / connect execution
connect(mapStateToProp) 
/ / return
connectAdvanced()
/ / returns the HOC
wrapWithConnect

Copy the code

Let’s look at what wrapWithConnect does.

5 wrapWithConnect Advanced component

Let’s take a look at wrapWithConnect, and let’s focus on wrapWithConnect as a high-level component that returns a component that does a series of enhancements to the existing business component.

function wrapWithConnect(WrappedComponent) {
    const wrappedComponentName =
      WrappedComponent.displayName || WrappedComponent.name || 'Component'
  
    const displayName = getDisplayName(wrappedComponentName)
  
    constselectorFactoryOptions = { ... connectOptions, getDisplayName, methodName, renderCountProp, shouldHandleStateChanges, storeKey, displayName, wrappedComponentName, WrappedComponent }const { pure } = connectOptions
    function createChildSelector(store) {
      // Merge the mergeprops function to get the latest props
      return selectorFactory(store.dispatch, selectorFactoryOptions)
    }
    // If yes, use useMemo to improve performance
    const usePureOnlyMemo = pure ? useMemo : callback= > callback()
    // The container child component responsible for the update
    function ConnectFunction (props){
        // props is the real props for the business component
    }
    const Connect = pure ? React.memo(ConnectFunction) : ConnectFunction
  
    Connect.WrappedComponent = WrappedComponent
    Connect.displayName = displayName
    /* forwardRef */
    if (forwardRef) {
      const forwarded = React.forwardRef(function forwardConnectRef(props, ref) {
        return <Connect {. props} reactReduxForwardedRef={ref} />
      })
  
      forwarded.displayName = displayName
      forwarded.WrappedComponent = WrappedComponent
      return hoistStatics(forwarded, WrappedComponent)
    }
  
    return hoistStatics(Connect, WrappedComponent)
  }
}
Copy the code

What wrapWithConnect does is roughly divided into the following:

The first step

1 Claim to be responsible for updatingConnectFunctionStateless components. And responsible for mergingprops ηš„createChildSelectormethods

The second step

2 Check whetherpurePure component mode, if usedreact.memoThe advantage of doing this is that the package will go topureComponentAs forpropsMake shallow comparisons.

The third step

3 ifconnect ζœ‰forwardRefConfiguration items, useReact.forwardRefThe benefits of doing this are as follows.

Normally, because our WrappedComponent is wrapped by Connect, we cannot access the instance of the business component WrappedComponent through ref.

Child components

const mapStateToProp = (store) = > ({ userInfo: store.root.userInfo })

class Child extends React.Component{
    render(){
        / *... * /}}export default connect(mapStateToProp)(Child)
Copy the code

The parent component


class Father extends React.Compoent{
    child = null 
    render(){
        return <Child ref={(cur)= >This. child = cur} {/* not 'child' itself */} />}}Copy the code

We cannot access the Child component through the REF.

Therefore, we can set the forwardRef attribute of options to true, which will solve the problem fundamentally.

connect(mapStateToProp,mapDispatchToProps,mergeProps,{ forwardRef:true  })(Child)
Copy the code

The fourth step

hoistStatics(Connect, WrappedComponent)
Copy the code

The last thing to do is to inherit the static methods/properties of the child WrappedComponent to the parent component Connect via the hoistStatics library. Because high-level components wrap business components, static properties or methods are not accessed by the wrapped components without additional processing, a library such as hoistStatics is needed to do this.

Now, this is the core of connect. Let’s take a look at what the container responsible for updates, ConnectFunction, does.

6 ConnectFunction Controls updates

The ConnectFunction code is very complex and needs to be digested step by step.

  function ConnectFunction(props) {
      / *TODO:Step 1: Retrieve the context ForwardedRef props */
      const [
        reactReduxForwardedRef,
        wrapperProps // props for delivering props
      ] = useMemo(() = > {
       
        const{ reactReduxForwardedRef, ... wrapperProps } = propsreturn [reactReduxForwardedRef, wrapperProps]
      }, [props])
   
  
      // Retrieve the context content with Redux stores and subscription
      const contextValue = useContext(Context)

      //TODO:Prop: props didStoreComeFromProps = false
      const didStoreComeFromProps =
        Boolean(props.store) &&
        Boolean(props.store.getState) &&
        Boolean(props.store.dispatch)
      const didStoreComeFromContext =
        Boolean(contextValue) && Boolean(contextValue.store)
  
      // Obtain the store in redux
      const store = didStoreComeFromProps ? props.store : contextValue.store
       // Return the merge function to generate props that are actually passed to the child component
      const childPropsSelector = useMemo(() = > {
        return createChildSelector(store)
      }, [store])


      // TODO:Step 2 Subscription listener instance
      const [subscription, notifyNestedSubs] = useMemo(() = > {
          // If the update is not subscribed, return it directly.
        if(! shouldHandleStateChanges)return NO_SUBSCRIPTION_ARRAY
  
        const subscription = new Subscription(
          store,
          didStoreComeFromProps ? null : contextValue.subscription // Establish a relationship with the parent 'subscription'. this.parentSub = contextValue.subscription
        )
        // notifyNestedSubs triggers all the child listeners of Noticy -> Triggers the batch method, which triggers the batchUpdate method, and updates in batches
        const notifyNestedSubs = subscription.notifyNestedSubs.bind(
          subscription
        )
  
        return [subscription, notifyNestedSubs]
      }, [store, didStoreComeFromProps, contextValue])

      /* Create a new contextValue and replace parent subscription with its own subscription */
      const overriddenContextValue = useMemo(() = > {   
        if (didStoreComeFromProps) { 
          return contextValue
        }
        return {
          ...contextValue,
          subscription
        }
      }, [didStoreComeFromProps, contextValue, subscription])
      const [
        [previousStateUpdateResult],
        forceComponentUpdateDispatch  / * * /
      ] = useReducer(storeStateUpdatesReducer, EMPTY_ARRAY, initStateUpdates)
  

      // TODO:The third step
      const lastChildProps = useRef() // Save props that were merged last time (ownprops,stateProps, dispatchProps)
      const lastWrapperProps = useRef(wrapperProps) // Save the props of this context execution business component
      const childPropsFromStoreUpdate = useRef()
      const renderIsScheduled = useRef(false) // Whether the current component is in the render phase
      // actualChildProps = props that have been merged
      const actualChildProps = usePureOnlyMemo(() = > {
          // Call mergeProps for the merge and return the latest merged porps
        return childPropsSelector(store.getState(), wrapperProps)
      }, [store, previousStateUpdateResult, wrapperProps])

     /* Is responsible for updating the cache variables so that the next update can compare */
      useEffect(() = >{ captureWrapperProps(... [ lastWrapperProps, lastChildProps, renderIsScheduled, wrapperProps, actualChildProps, childPropsFromStoreUpdate, notifyNestedSubs ]) }) useEffect(() = >{ subscribeUpdates(... [ shouldHandleStateChanges, store, subscription, childPropsSelector, lastWrapperProps, lastChildProps, renderIsScheduled, childPropsFromStoreUpdate, notifyNestedSubs, forceComponentUpdateDispatch ]) },[store, subscription, childPropsSelector])// TODO:ReactReduxForwardedRef specifies whether or not the parent element has a forwardRef.
      const renderedWrappedComponent = useMemo(
        () = > (
          <WrappedComponent
            {. actualChildProps}
            ref={reactReduxForwardedRef}
          />
        ),
        [reactReduxForwardedRef, WrappedComponent, actualChildProps]
      )
      const renderedChild = useMemo(() = > {
        / / shouldHandleStateChanges source connect to see if there is the first parameter
        if (shouldHandleStateChanges) {
          return (
            // ContextToUse passes context
            <ContextToUse.Provider value={overriddenContextValue}>
              {renderedWrappedComponent}
            </ContextToUse.Provider>)}return renderedWrappedComponent
      }, [ContextToUse, renderedWrappedComponent, overriddenContextValue])
  
      return renderedChild
    }
Copy the code

To make it easier for you to understand, I have kept the core code that affects the process here, and I will analyze the whole core part step by step. To understand this, you need to know something about react-hooks and providers.

The first step

Isolate the reactreduxprops and wrapperProps by the props. ReactReduxForwardedRef is the React. ForwaedRef passed by the parent when the ForwardedRef is open.

Then determine whether the current, redux.store, is from props by storing the constant didStoreComeFromProps. Normally, all our stores come from providers, not props, So we can say didStoreComeFromProps = true. Next we get the store and use the store to determine whether to update the true merged props function childPropsSelector.

Step 2 Create childrensubscription, passing on new layerscontext(Important)

This step is very important to determine if this HOC is subscribed to the store changes by shouldHandleStateChanges. If it has subscribed to the update (in this case connect has the first argument), Create a subscription and associate it with the previous provider subscription. Enclosing parentSub = contextValue. Subscription. The subscription is then separated from the notifyNestedSubs. The notifyNestedSubs notifies current Listeners of updates. .

It then creates a new contextValue using useMemo, replacing the parent subscription with its own subscription. Used to pass the new context through the Provider. Here is a brief introduction, the use of a Provider can be used to correspond to a number of consumer components. Multiple providers can also be nested, with the inner one overwriting the outer data. React-redux uses context in favor of the Provider’s ability to deliver context well.

Next through useReducer create real trigger updates forceComponentUpdateDispatch function. This is the function whose entire state or props change triggers the update of the component. Why do you do that?

The author thinks that the reason of react-Redxx’s design is that connect wants to control its own update, and multiple upper and lower connect will not be affected. So on the one hand, the business component by useMemo unnecessary update, on the other hand to update by forceComponentUpdateDispatch HOC function, Produce actualChildProps actualChildProps change, useMemo execution, and trigger the component rendering.

Step 3: Save the information and execute the side effect hook (here comes the most important part)

This step is very important, and why? First, cache a few variables through useRef:

LastChildProps -> Save props that were merged last time (ownprops,stateProps, dispatchProps). LastWrapperProps -> Props to save this context execution business component. RenderIsScheduled -> Whether the current component is in the render phase. ActualChildProps -> actualChildProps indicates the merged props. The component uses dep -> actualChildProps to determine whether to update the props.

The first useEffect executes captureWrapperProps. CaptureWrapperProps. What does captureWrapperProps do?

// Get props for the wrapper
function captureWrapperProps(lastWrapperProps, lastChildProps, renderIsScheduled, wrapperProps, actualChildProps, childPropsFromStoreUpdate, notifyNestedSubs) {
  lastWrapperProps.current = wrapperProps  / / props
  lastChildProps.current = actualChildProps // Prop formed after megeprops
  renderIsScheduled.current = false  // The current component is rendered
}
Copy the code

The captureWrapperProps function is simple: after a component rendering update, it saves the props from before and after the last merge. The purpose of this is to compare whether props stateProps have changed between the two hoc renderings. This determines whether to update the hoc and further update the component.

Implementing the second useEffect is key. Execute the subscribeUpdates function. SubscribeUpdates is the main function that subscribes to updates. Let’s take a look:

function subscribeUpdates(
  shouldHandleStateChanges,
  store,
  subscription,
  childPropsSelector,
  lastWrapperProps,  / / props
  lastChildProps, // Prop formed after megeprops
  renderIsScheduled,
  childPropsFromStoreUpdate,
  notifyNestedSubs,
  forceComponentUpdateDispatch
) {
  if(! shouldHandleStateChanges)return

   // Capture the value to check if and when the component is unloaded
  let didUnsubscribe = false
  let lastThrownError = null
   // Run this callback when the store update subscription is propagated to this component
  const checkForUpdates = () = >{
      //....
  }
  subscription.onStateChange = checkForUpdates
  // Turn on the subscriber, which will put the current checkForceUpdate in the addNestedSub stored in the parent element.
  subscription.trySubscribe()
  // Fetch the data from the storage after the first rendering, in case the storage has changed since we started.
  checkForUpdates()
  /* Uninstall subscription */
  const unsubscribeWrapper = () = > {
    didUnsubscribe = true
    subscription.tryUnsubscribe()
    subscription.onStateChange = null
  }

  return unsubscribeWrapper
}

Copy the code

This is definitely at the heart of the whole subscription update, so let me start with thatstoreUpdate the callback function when the subscription is propagated to this componentcheckForUpdatesAssign it toonStateChangeIf thestoreIn thestateChanges occur in the component subscriptionstateContent after, associated withstateChange will trigger the current componentonStateChangeTo merge to get the newpropsTo trigger component updates.

thensubscription.trySubscribe()Put the subscription functiononStateChangeBind to the parentsubscription“, carried out layers of subscription.

Finally, in order to prevent after rendering,storeThe content has changed, so it is executed firstcheckForUpdates. thencheckForUpdatesCheck to see if updates to the current component are sent.

React-redux subscribs via subscription. For the layered component structure, the overall model is shown as follows:.

Now let’s look at checkForUpdates

  // Run this callback when the store update subscription is propagated to this component
  const checkForUpdates = () = > {
    if (didUnsubscribe) {
      // If it is written in
      return
    }
     // Obtain the state in the store
    const latestStoreState = store.getState()q
    let newChildProps, error
    try {
      /* Get the latest props */
      newChildProps = childPropsSelector(
        latestStoreState,
        lastWrapperProps.current
      )
    } 
    // If the new merged props have not changed, then do nothing here - cascade subscribe updates
    if (newChildProps === lastChildProps.current) { 
      if(! renderIsScheduled.current) { notifyNestedSubs()/* Notify the child Subscription to trigger checkForUpdates to check whether an update is needed. * /}}else {
      lastChildProps.current = newChildProps
      childPropsFromStoreUpdate.current = newChildProps
      renderIsScheduled.current = true
      // This may be the case because the props update occurred when the code was running here, so trigger a reducer to update the components.
      forceComponentUpdateDispatch({
        type: 'STORE_UPDATED'.payload: {
          error
        }
      })
    }
  }
Copy the code

checkForUpdatesBy calling thechildPropsSelectorTo form newprops“And then judge the previous onepropAnd the current newpropWhether they are equal. If it is, it proves that no change has occurred, no need to update the current component, then through the callnotifyNestedSubsTo notify the child container component to check if it needs to be updated. If not, prove the subscriptionstore.stateChange, then execute immediatelyforceComponentUpdateDispatchTo trigger an update to the component.

For the tiered subscription structure, the whole update model is shown as follows:

conclusion

Let’s summarize the entire connect process. Let’s start with both subscriptions and updates.

Subscribe to the process

The entire process of subscribing is ifconnectPackage, and has the first parameter. First of all bycontextGet the nearestsubscriptionAnd create a new onesubscription, and the parentsubscriptionEstablish a relationship. When the firsthocThe container component hangs at completion inuseEffectIn, to subscribe, will own the subscription functioncheckForUpdates, as a callback function, throughtrySubscribe ε’Œthis.parentSub.addNestedSubIs added to the parent levelsubscriptionthelistenersIn the. This completes the entire subscription process.

Update process

The whole update process is, thatstateChange, will trigger the root subscriberstore.subscribeAnd then it triggerslisteners.notify, that is,checkForUpdatesFunction, and thencheckForUpdatesThe function is first based onmapStoretoprops.mergepropsTo verify that the component initiates a subscription,propsWhether to change, and update, if the change occurs, then triggeruseReducertheforceComponentUpdateDispatchFunction to update the business component or, if no update has occurred, by callingnotifyNestedSubs, to notify the currentsubscriptionthelistenersCheck for updates and then focus on the layerscheckForUpdates, to complete the entire update process.

About fouruseMemoUsage thinking?

The whole react-Redux source code, for useMemo usage is quite a lot, I summarized a few, submitted to 🌹🌹 :

1 Cache properties/methods

The React-Redux source code uses useMemo dependency/cache attributes in several places. The benefit of this is that new cache properties/methods are updated only when dependencies change, such as childPropsSelector, Subscription, actualChildProps and other primary method properties.

2 control component rendering, rendering throttling.

The React-Redux source code uses useMemo to control whether the business component is rendered or not. To determine whether or not to render the component, determine whether or not to render the component by using the variations of actualChildProps to prove whether or not the modification came from the ** itself props ** or the subscribed state.

Example 🌰 :

const renderedWrappedComponent = useMemo(
    () = > (
        <WrappedComponent
        {. actualChildProps}
        ref={reactReduxForwardedRef}
        />
    ),
    [reactReduxForwardedRef, WrappedComponent, actualChildProps]
)
Copy the code

Five summarizes

Hopefully, this post will give you a fresh look at the react-Redux feed and update process. Send roses, hand stay fragrance, reading friends can give the author like, follow a wave, and update the front end of the super core article.

Check out my previous highly praised articles for more exciting content waiting for you!

  • “React Advanced” eight year-end optimisations sent to React developers 800+ likes πŸ‘

  • “Front-end engineering” builds react, TS scaffolding from 0-1 (1.2w word super detailed tutorial) 300 praise πŸ‘

  • Vue communication modes and application Scenarios 250+ Praise πŸ‘

  • H5, applet fly into shopping cart (parabola draw motion track point) 300 likes πŸ‘

Vue3.0 source series

  • Vue3.0 Responsive Principle (super detailed) 200+ praise πŸ‘

  • Comprehensive analysis of vue3.0 Diff algorithm 100+ praise πŸ‘

  • Vue3.0 Watch and computed source parsing 30+ praise πŸ‘

The react – hooks series

  • Play react-hooks, custom hooks design mode and its actual combat 150+ πŸ‘ praise

  • How do you use react-hooks with 70+ likes πŸ‘

Open Source Project Series

  • “React Cache Pages” from requirements to open source (how I impressed the product sister) 230 +Praise πŸ‘