preface

Before learning redux, redux-Thunk source code, next work hard, to understand the react-Redux source code.

P.S. The source code version number studied here is 7.2.4

role

Redux is a separate library that can be used in multiple libraries or frameworks. When we use redux in conjunction with a UI library (framework), we often use a UI binding library as a bridge between them.

Redux itself is a standalone library that can be used with any UI layer or framework, including React, Angular, Vue, Ember, and vanilla JS. Although Redux and React are commonly used together, they are independent of each other.

If you are using Redux with any kind of UI framework, you will normally use a “UI binding” library to tie Redux together with your UI framework, rather than directly interacting with the store from your UI code.

Its main functions are:

  • react-reduxorreduxThe official team maintains whenreduxWhen updated, the library is updated;
  • Improved performance, as is usually the case, when acomponentThe whole tree is rerendered when it changes, butreact-reduxInternally help us do optimization, improve performance;
  • The community is strong.

Source code analysis

The source SRC /index.js file is exposed as:

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

ReactReduxContext

CreateContext creates a global context using react.createcontext. So react-Redux is essentially using context, you need to use context to pass stores to nested components, and when the state in the store is updated, you also need to update the state of the nested child components, So the store and description are saved in the context later, simplifying the use of redux.

// src/components/Context.js
import React from 'react'

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

if(process.env.NODE_ENV ! = ='production') {
  ReactReduxContext.displayName = 'ReactRedux'
}

export default ReactReduxContext
Copy the code

batch

This is a variable related to batch updates in React. Batch update, according to my understanding at this stage: If the update is called N times in a row, React will not trigger view rerendering every time, but will update the view when the final result is obtained directly after the update.

// src/utils/reactBatchedUpdates
export { unstable_batchedUpdates } from 'react-dom'

// src/index.js
import { unstable_batchedUpdates as batch } from './utils/reactBatchedUpdates'
Copy the code

Provider

The purpose of the Provider is that you can inject the state of a store into a parent component, and then any component wrapped by the parent component can use the state of the Store without having to manually reference the Store in every file.

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

A Provider is a component, so it has the same basic structure as a normal component. According to the documentation, it takes three parameters: Store, Chidren (which is usually our App component), Context, and (if so, provide the same context for all connected components).

As we know above, React-Redux mainly uses context. Here, it is not difficult to analyze the basic architecture of Provider:

import { ReactReduxContext } from './Context'

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

We need to do something with the Context and store in the Provider. Before we do that, let’s analyze a few small methods:

createListenerCollection

Each node has a callback function and two Pointers: a pointer to the previous node (prev) and a pointer to the next node (next).

CreateListenerCollection specifies a first to the head of the list and a last to the tail of the list. It also defines the following methods:

  • Clear: clear the first and last Pointers, that is, clear the list;

  • Notify: Iterates over each node in the listener and calls their callback. The batch mentioned above is also used here. Guess is traversal node call function, batch can ensure that the function is called after the completion of a one-time update, to achieve the role of batch processing, rather than every callback function, update once;

  • Get: Iterates through the entire list, saving each node in sequence in the Listeners array, and returns it;

  • Subscribed = true to indicate that the new nodes had been listened to, and then added them as if they had been added to the list:

    • Create a new node withCallback, Prev, and nextProperty, whereprevThe assignment forlast(because the new node is placed after the last node);
    • If the current list is empty, then this node is the head node, otherwise, the previous node of the current nodenextIt points to this node.

    Finally, it returns a function to cancel listening on the node:

    • ifisSubscribedforfalseorfirstIf (list) is empty, return directly;
    • Set up theisSubscribedforfalse;
    • Delete logic can be a bit convoluted, let’s use an example where the current node is B:
      • ifBThere’s the next nodeCSo we’re going toCtheprevPoint to theBThe last node of;
      • Otherwise, that is,BThere is no next node, so at this pointlastPoints to theB, then it should belastPoint to theBThe last node of;
      • ifBWe have the last nodeA, thenAthenextShould be directedBThe next node of;
      • Otherwise, sayBThat’s at the head of the listfirstIt points toB, should be modified toBThe next node of.
import { getBatch } from './batch'

function createListenerCollection() {
  // The React update mechanism is related to batch. When multiple values are updated together, instead of rerendering each time a value is updated, the react update mechanism is related to batch
  const batch = getBatch()
  let first = null  / / the head pointer
  let last = null   // The tail pointer indicates that the list is bidirectional

  return {
    // Empty the pointer
    clear() {
      first = null  
      last = null
    },

    notify() {
      batch(() = > {                  // The callback function is called for batch processing
        let listener = first         // Iterate through the list from the beginning, executing each callback function
        while (listener) {
          listener.callback()
          listener = listener.next
        }
      })
    },

    get() {
      let listeners = []
      let listener = first
      while (listener) {
        listeners.push(listener)
        listener = listener.next
      }
      return listeners                // Return a callback function for all subscriptions
    },

    subscribe(callback) {
      let isSubscribed = true        // Indicates a subscription

      let listener = (last = {       // The listener is a node with a bidirectional pointer
        callback,          
        next: null.// The tail of the new node is null, so it is null
        prev: last,                  // The node before the new node must be the previous node
      }) 

      if (listener.prev) {  
        // If the new node has a previous node, then its previous node points to it with the next pointer
        listener.prev.next = listener
      } else {
        // If not, the new node is considered as the first node
        first = listener
      }

      return function unsubscribe() {
        // If there is no subscription, or the list is empty, then there is no need to unlisten
        if(! isSubscribed || first ===null) return
        // Unsubscribe
        isSubscribed = false
		
        // Since the node is deleted, the pointer to the node before and after it needs to be changed
        // You may also need to modify the first and last Pointers
        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

Subscription

The Provider passes the state in the Store to each component and changes the store in each component when the state is updated. Therefore, you need to listen on the store in real time and support the operation of unbinding the listener. So it encapsulates a function called Subscription, which means to subscribe. This function will be used later in the implementation of other methods, so you need to clear your mind about it.

encapsulates the subscription logic for connecting a component to the redux store, as well as nesting subscriptions of descendant components, so that we can ensure the ancestor components re-render before descendants

Encapsulate the subscription logic used to wire components to the Redux store, as well as nested subscriptions to the descendant components so that we can ensure that the ancestor components re-render before the descendant components

// src/utils/Subscription.js
const nullListeners = { notify(){}}export default class Subscription {
  constructor(store, parentSub) {
    this.store = store              // Store in redux
    this.parentSub = parentSub      // context.store
    this.unsubscribe = null         // Subscribe to the object
    this.listeners = nullListeners  // Subscribe to the callback function

    this.handleChangeWrapper = this.handleChangeWrapper.bind(this)}// Add listeners to nested components
  addNestedSub(listener) {
    this.trySubscribe()
    return this.listeners.subscribe(listener)
  }
    
  // Notify the nested component that state has changed
  notifyNestedSubs() {
    this.listeners.notify()
  }
  
  // The callback function to execute when the store changes
  handleChangeWrapper() {
    if (this.onStateChange) {
      this.onStateChange()
    }
  }
  
  // Check whether a subscription is available
  isSubscribed() {
    return Boolean(this.unsubscribe)
  }
	
  // Add a subscription
  ParentSub has a higher priority than store
  trySubscribe() {
    if (!this.unsubscribe) {
      this.unsubscribe = this.parentSub
        ? this.parentSub.addNestedSub(this.handleChangeWrapper)
        : this.store.subscribe(this.handleChangeWrapper)
	  // Initialize Listeners to create the linked list
      this.listeners = createListenerCollection()
    }
  }
  
  // Unsubscribe
  tryUnsubscribe() {
    if (this.unsubscribe) {
      this.unsubscribe()
      this.unsubscribe = null
      this.listeners.clear()
      this.listeners = nullListeners
    }
  }
}

Copy the code

Subscription creates a listeners object that listens on the store and notifies each component, or parentSub, adds a listener, and iterates over it

useIsomorphicLayoutEffect

Before the study useIsomorphicLayoutEffect, stroke our useEffect and useLayoutEffect difference:

  • useEffect: whenrenderWhen it’s over,callbackThe function is executed, and because it is asynchronous, it does not block browser rendering;
  • useLayoutEffect: If you wereuseEffectNeed to deal withDOMOperation, can be useduseLayoutEffect, the screen will not flicker. It will beDOMImmediately after completion, but before the browser does any drawing, so it blocks the browser’s rendering.
  • No matteruseLayoutEffectoruseEffectCan’t inJavascript Execute before the code has finished loading. Introduced in the server-side rendering componentuseLayoutEffectCode will triggerReactWarning. To solve this problem, you need to move the code logic touseEffectIn the.

The Provider of

import { useEffect, useLayoutEffect } from 'react'

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

To solve the above problems, the browser rendering uses useLayoutEffect and the server rendering uses useEffect. UseLayoutEffect is used to ensure that the store’s subscription callback is executed on the last Render commit, otherwise we might lose the update when the store’s update occurs between Render and Effect.

We also need to ensure that the subscription is created asynchronously, otherwise the states we observe might not be consistent with those in the Store when an update to a store occurs before the subscription is created.

The source code

import React, { useMemo } from 'react'
import PropTypes from 'prop-types'
import { ReactReduxContext } from './Context'
import Subscription from '.. /utils/Subscription'
import { useIsomorphicLayoutEffect } from '.. /utils/useIsomorphicLayoutEffect'

function Provider({ store, context, children }) {
  // useMemo recalls the internal function if the dependency changes
  // When the store changes, the contextValue is recalculated
  const contextValue = useMemo(() = > {
    // Create Subscription object
    const subscription = new Subscription(store)
    // Bind notifyNestedSubs to onStateChange
    subscription.onStateChange = subscription.notifyNestedSubs
      
    // In React-redux, contextValue contains both of these attributes
    // Subscription is a listener and can be passed as the store changes
    // The component that subscribed to the store can also be unlistened
    return {
      store,
      subscription,
    }
  }, [store])
  
  // Recalculate the current value of getState when the store changes
  const previousState = useMemo(() = > store.getState(), [store])
  
  // Triggers the following logic when previousState or contextValue changes
  useIsomorphicLayoutEffect(() = > {
    const { subscription } = contextValue
    // Iterate over each node in the list, triggering a callback function that publishes notifications to the components that subscribe to the Store
    // Then create a new list
    subscription.trySubscribe()
    
    // The value of state may change after the notification is published,
    // Continue traversing the list, notification
    if(previousState ! == store.getState()) { subscription.notifyNestedSubs() }return () = > {
      // Cancel listening to facilitate garbage collection
      subscription.tryUnsubscribe()
      subscription.onStateChange = null
    }
  }, [contextValue, previousState])
  
  // If we don't pass a context, we use ReactReduxContext by default
  const Context = context || ReactReduxContext

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

export default Provider

Copy the code

A Provider is a function that acts as a component. Internally, it performs several steps:

  • tostoreI created asubscriptionObject for conveniencestoreUpdate with helpsubscriptionAnd assign two values toContextValue;
  • useContext.ProviderwillContextValuePassed to theChildren.

If react-Redux uses context to pass stores, what does it do internally if it also supports custom context?

When using a Context in a Provider, create a Conetxt and pass it as a parameter to the Provider, but be sure to manually import the Context in the child components otherwise an error will be reported:

import React from 'react';
export const ColorContext = React.createContext({color: 'red'});
Copy the code
// index.js
import React from 'react';
import ReactDOM from 'react-dom';
import App from './App';
import store from './store';
import { Provider } from 'react-redux';
import { ColorContext } from './store/context';

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

To avoid errors, in the child component,context is passed as props:

// App.js
<Count context={ColorContext} />
Copy the code

To read the ColorContext internally, we can retrieve it from the ownProps:

// Counter.js
function Counter(props){
  const { count, increment, ColorContext } = props;
  const contextValue = useContext(ColorContext);
  console.log('contextValue123', contextValue);
    
  return (
    <>
      <p>{count}</p>
      <button onClick = {() = > increment(1)}>+</button>
    </>)}function mapStateToProps(state, ownProps) {
  return { 
    count: state.count,
    ColorContext:  ownProps.ownProps
  }
}

function mapDispatchToProps(dispatch) {
  return { increment: bindActionCreators(actionCreators.increment, dispatch) }
}

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

In addition, according to the back of the source code available, this will not report an error… , but cannot get the Context on ownProps

 <Count store={store} />
Copy the code

connectAdvanced

This is a never-before-used API! It is the underlying implementation of Connect, but the official documentation says it may be removed from the code in the future. ConnectAdvanced differs from Connect in that it does not specify how state, props, and dispatches are passed into the final props, nor does it cache the resulting props to optimize performance.

ConnectAdvanced accepts two parameters: selectorFactory and factoryOptions. In selectorFactory we can implement what parameters are passed to the component. These parameters can depend on store.getState(), action, and the component’s own props. Official DEMO:

function selectorFactory(dispatch) {
  let ownProps = {}
  let result = {}

  const actions = bindActionCreators(actionCreators, dispatch)
  const addTodo = (text) = > actions.addTodo(ownProps.userId, text)

  return (nextState, nextOwnProps) = > {
    const todos = nextState.todos[nextOwnProps.userId]
    constnextResult = { ... nextOwnProps, todos, addTodo } ownProps = nextOwnPropsif(! shallowEqual(result, nextResult)) result = nextResultreturn result
  }
}
export default connectAdvanced(selectorFactory)(TodoApp)
Copy the code

Let’s just call the function returned by selectorFactory a selector. The selector function takes two arguments, the new state and the props of the component itself. The selector function is called when a component receives a new state or props, and then returns a simple object to the wrapped component.

There are several possible parameters in factoryOptions:

  • getDisplayName: get beconnectAdvancedThe name of the new component to return after the package. Default isConnectAdvanced('+name+')
  • methodName: used to display in error messages. The default isconnectAdvanced;
  • shouldHandleStateChanges: Whether to trackstateThe default value istrue;
  • forwardRef: When neededrefTo save isconnectAdvancedSet to when wrapping new components afterfalse;
  • context:createContextTo create thecontext;
  • The remaining parameters: will be passed toselectorFactorynamelyselectorMethod in the second argument.

Here on the basic structure of the source code, some of the parameters to be removed by me deleted!

export default function connectAdvanced(
	selectorFactory,
    {
    	getDisplayName = (name) => `ConnectAdvanced(${name}) `,
    	methodName = 'connectAdvanced',
        shouldHandleStateChanges = true,
    	forwardRef = false,
        context = ReactReduxContext,
        //The rest of the arguments are passed to selectorFactory... connectOptions }){	
    // ...
    return function wrapWithConnect(WrappedComponent) {
        // ...
        return hoistStatics(Connect, WrappedComponent)
    }
}
Copy the code

hoistStatics comes from the package hoist- non-React-statics that copies static properties from your original component to the new component wrapped with connectAdvanced. Here’s an example:

const Component.staticMethod = () = > {}
const WrapperComponent = enhance(Component);
typeof WrapperComponent.staticMethod === undefined // true

// The solution is as follows
function enhance(Component) {
  class WrapperComponent extends React.Component {/ *... * /}
  WrapperComponent.staticMethod = Component.staticMethod;
  return Enhance;
}

// If we don't know all the static property values of the original component, then the above method is invalid.
// Hoist non-react-statics
import hoistNonReactStatic from 'hoist-non-react-statics';
function enhance(Component) {
  Class WrapperComponent extends React.Component {/ *... * /}
  hoistNonReactStatic(WrapperComponent, Component);
  return WrapperComponent;
}
Copy the code

Continue with the analysis to understand each line of code inside:

export default function connectAdvanced(){
    // The first half of this section reports an error for the attributes to be removed...
    
    const Context = context;   // Save the context to be passed inside the component
    return function wrapWithConnect(WrappedComponent) {
        // ...}}Copy the code

Go to the wrapWithConnect source code:

function wrapWithConnect(WrappedComponent) {
   // Get the name of the component first
    const wrappedComponentName =
      WrappedComponent.displayName || WrappedComponent.name || 'Component'
     // Name of the new component returned after being wrapped => 'ConnectAdvanced(${name})'
     const displayName = getDisplayName(wrappedComponentName)
     
     constselectorFactoryOptions = { ... connectOptions, getDisplayName, methodName, shouldHandleStateChanges, displayName, wrappedComponentName, WrappedComponent, }const { pure } = connectOptions
     
     // Create our selector function
     function createChildSelector(store) {
      return selectorFactory(store.dispatch, selectorFactoryOptions)
    }
     
     // Use useMemo to improve the performance of the new component returned by the package
     const usePureOnlyMemo = pure ? useMemo : (callback) = > callback()
     
     function ConnectFunction(props){}
     
     // Use react. memo to improve the performance of the new component returned by the package
     const Connect = pure ? React.memo(ConnectFunction) : ConnectFunction
     Connect.WrappedComponent = WrappedComponent
     Connect.displayName = ConnectFunction.displayName = displayName
    
    // ...
}
Copy the code

Here is a ConnectFunction function:

function ConnectFunction(props) {
    // From props, we deconstruct the context, the ref, and the remaining props
    const [
        propsContext,
        reactReduxForwardedRef,
        wrapperProps,
      ] = useMemo(() = > {
        const{ reactReduxForwardedRef, ... wrapperProps } = propsreturn [props.context, reactReduxForwardedRef, wrapperProps]
      }, [props])
     
     ReactReduxContext = ReactReduxContext; const Context = context
     // Provider allows users to pass in a custom Context. If not, the default is ReactReduxContext
     // If the user passed the context on the Provider, the user had to manually pass the context in the nested component, so we need to determine the value of the context.
    // Use the appropriate Context
     const ContextToUse = useMemo(() = > {
        return propsContext &&
          propsContext.Consumer &&
          isContextConsumer(<propsContext.Consumer />)? propsContext : Context }, [propsContext, Context])// Get the value in the context
     const contextValue = useContext(ContextToUse)
     
     // The Provider is a Provider
     // Provider encapsulates contextValue with store and subscription
     // If the user passes a custom Context, the nested component must pass the Context or store to avoid an error.
     
     // contextValue. Store: contextValue. Store: contextValue
     // If there is no store, then we need to find the store in contextValue, if there is no store in contextValue
     // An error is reported
     // This is why if a user passes a context, it is necessary to pass a custom context or store in the nested component
     const didStoreComeFromProps =
        Boolean(props.store) &&
        Boolean(props.store.getState) &&
        Boolean(props.store.dispatch)
      const didStoreComeFromContext =
        Boolean(contextValue) && Boolean(contextValue.store)

      if( process.env.NODE_ENV ! = ='production'&&! didStoreComeFromProps && ! didStoreComeFromContext ) {throw new Error(
          `Could not find "store" in the context of ` +
            `"${displayName}". Either wrap the root component in a <Provider>, ` +
            `or pass a custom React context provider to <Provider> and the corresponding ` +
            `React context consumer to ${displayName} in connect options.`)}/ / save the store
    const store = didStoreComeFromProps ? props.store : contextValue.store
    
    // As mentioned earlier, the selector function will be called if the store changes
    const childPropsSelector = useMemo(() = > {
    	return createChildSelector(store)
  	}, [store])
    
    // ...
}
Copy the code
const [subscription, notifyNestedSubs] = useMemo(() = > {
    // Return [null, null] if you don't need to listen for state changes.
    if(! shouldHandleStateChanges)return NO_SUBSCRIPTION_ARRAY

    / / if the store is provided by the context, the need to contextValue. The subscription for listening
    const subscription = new Subscription(
      store,
      didStoreComeFromProps ? null : contextValue.subscription
    )

    / / copy notifyNestedSubs
    const notifyNestedSubs = subscription.notifyNestedSubs.bind(
      subscription
    )

    return [subscription, notifyNestedSubs]
}, [store, didStoreComeFromProps, contextValue])

// Override the current contextValue
const overriddenContextValue = useMemo(() = > {
    // If the store is from Props, then return contextValue directly
    if (didStoreComeFromProps) {
      return contextValue
    }

    // Otherwise, the component subscription is placed in the context
    return {
      ...contextValue,
      subscription,
    }
}, [didStoreComeFromProps, contextValue, subscription])

Copy the code
// When the store is updated, it is necessary to force the component that is being updated to be updated, and therefore the child component to be updated as well
const [
    [previousStateUpdateResult],
    forceComponentUpdateDispatch,
] = useReducer(storeStateUpdatesReducer, EMPTY_ARRAY, initStateUpdates)

/ / will throw an exception when an error occurs, the normal previousStateUpdateResult should be null
if (previousStateUpdateResult && previousStateUpdateResult.error) {
	throw previousStateUpdateResult.error
}

// Here is the source code for each parameter
function storeStateUpdatesReducer(state, action) {
  const [, updateCount] = state
  return [action.payload, updateCount + 1]}const EMPTY_ARRAY = []
const initStateUpdates = () = > [null.0]
Copy the code
/ / mentioned earlier, useIsomorphicLayoutEffect internal judgment,
// Return useEffect if it is a server
// Return useLayoutEffect for the browser
import { useIsomorphicLayoutEffect } from '.. /utils/useIsomorphicLayoutEffect'

/ / here you can understand, call the useEffect/useLayoutEffect, but the parameter is dynamic
function useIsomorphicLayoutEffectWithArgs(effectFunc, effectArgs, dependencies) {
  useIsomorphicLayoutEffect(() = >effectFunc(... effectArgs), dependencies) }// Continue analyzing the source code
const lastChildProps = useRef()   // Pass the props (Component) for the current Component before the update.
const lastWrapperProps = useRef(wrapperProps) // Update the props (ConnectComponent) for the new component
const childPropsFromStoreUpdate = useRef()     // Determine if the component update is due to a store update
const renderIsScheduled = useRef(false)

// usePureOnlyMemo Determines whether to use memo based on pure
const actualChildProps = usePureOnlyMemo(() = > {
// View updates may be due to store updates that cause the Component to get new props
// If the Component has new props and the ConnectComponent has the same props, then we should use the new props so that we can get the new props for ConnectComponent
// But if we have a new ConnectComponent props, which might change the Component props, then the calculation will be recalculated
// To avoid problems, we will update the Component with new props only if ConnectComponent's props are the same
if (
  childPropsFromStoreUpdate.current &&
  wrapperProps === lastWrapperProps.current
) {
  return childPropsFromStoreUpdate.current
}

// Call the selector function
return childPropsSelector(store.getState(), wrapperProps)
}, [store, previousStateUpdateResult, wrapperProps])
      
// We need the following function to perform the synchronization to render, but using useLayoutEffect in SSR will cause an error, we need to check this, if SSR, then use effect instead to avoid this warning
useIsomorphicLayoutEffectWithArgs(captureWrapperProps, [
    lastWrapperProps,
    lastChildProps,
    renderIsScheduled,
    wrapperProps,
    actualChildProps,
    childPropsFromStoreUpdate,
    notifyNestedSubs,
])

function captureWrapperProps(lastWrapperProps, lastChildProps, renderIsScheduled, wrapperProps, actualChildProps, childPropsFromStoreUpdate, notifyNestedSubs) {
  // For the next comparison
  lastWrapperProps.current = wrapperProps
  lastChildProps.current = actualChildProps
  renderIsScheduled.current = false

  // Clear the reference and update it
  if (childPropsFromStoreUpdate.current) {
    childPropsFromStoreUpdate.current = null
    notifyNestedSubs()
  }
}

// When the store or subscription changes, we resubscribe
useIsomorphicLayoutEffectWithArgs(
    subscribeUpdates,
    [
      shouldHandleStateChanges,
      store,
      subscription,
      childPropsSelector,
      lastWrapperProps,
      lastChildProps,
      renderIsScheduled,
      childPropsFromStoreUpdate,
      notifyNestedSubs,
      forceComponentUpdateDispatch,
    ],
[store, subscription, childPropsSelector]
)
Copy the code

function subscribeUpdates(
  shouldHandleStateChanges, // Whether to subscribe to store updates
  store,                    // store
  subscription,             // subscription
  childPropsSelector,       // selector
  lastWrapperProps,         // Props for the last component
  lastChildProps,           // The last props
  renderIsScheduled,        // Whether scheduling is underway
  childPropsFromStoreUpdate // Check whether the subprops are an update from the store
  notifyNestedSubs,         // notifyNestedSubs
  forceComponentUpdateDispatch
) {
  // If there is no subscription, return directly
  if(! shouldHandleStateChanges)return

  // Capture the value and check if and when the component is unloaded
  let didUnsubscribe = false
  let lastThrownError = null

  // This function is called every time a store update is propagated to this component
  const checkForUpdates = () = > {
    if (didUnsubscribe) {
      // If you have unsubscribed, return directly
      return
    }
	// Get the store before the update
    const latestStoreState = store.getState()

    let newChildProps, error
    try {
      // Call the selector to get the latest props
      newChildProps = childPropsSelector(
        latestStoreState,
        lastWrapperProps.current
      )
    } catch (e) {
      error = e
      lastThrownError = e
    }

    if(! error) { lastThrownError =null
    }

    // If the old props are the same as the old props, don't do anything about it
    if (newChildProps === lastChildProps.current) {
      if(! renderIsScheduled.current) {/ / call the subscription. NotifyNestedSubs ()
        notifyNestedSubs()
      }
    } else {
      // use useState/useReducer to trace the latest props.
      // There will be no way to determine whether the value has been processed
      // There is no way to clear this value to trigger a forced render
      lastChildProps.current = newChildProps
      childPropsFromStoreUpdate.current = newChildProps
      renderIsScheduled.current = true

      // If the child props were updated, then it is possible to render again
      forceComponentUpdateDispatch({
        type: 'STORE_UPDATED'.payload: {
          error,
        },
      })
    }
  }

  subscription.onStateChange = checkForUpdates
  subscription.trySubscribe()

  // Update the view to the changed values in the store
  checkForUpdates()

  const unsubscribeWrapper = () = > {
    didUnsubscribe = true
    subscription.tryUnsubscribe()
    subscription.onStateChange = null

    if (lastThrownError) {
      // When the store is updated but a component does not depend on the updated state in the store, there is a problem with mapState
      // An error is reported
      throw lastThrownError
    }
  }

  return unsubscribeWrapper
}
Copy the code
// Back in the main source code, we have processed the parameters that need to be passed, and the next step is to render the component
// useMemo is also used to optimize performance
const renderedWrappedComponent = useMemo(
() = > (
  <WrappedComponent
    {. actualChildProps}
    ref={reactReduxForwardedRef}
  />
),
[reactReduxForwardedRef, WrappedComponent, actualChildProps]
)

// If React sees the exact same element reference as last time, it will exit and re-render the child elements, just as it would if it were wrapped in React.memo() or returned false from shouldComponentUpdate.
const renderedChild = useMemo(() = > {
if (shouldHandleStateChanges) {
  // If you need to update the component based on store changes, use context.provider to wrap the component
  // And pass in the processed value
  return (
    <ContextToUse.Provider value={overriddenContextValue}>
      {renderedWrappedComponent}
    </ContextToUse.Provider>)}// Otherwise, return the original component
return renderedWrappedComponent
}, [ContextToUse, renderedWrappedComponent, overriddenContextValue])

return renderedChild
Copy the code

Here the connectAdvanced source code is analyzed. Let’s get this straight. I’ll write a line of code for the sake of illustration:

function selector = (dispatch, ... args) = >{}
const connectComponent = connectAdvanced(selector, options)(component)
Copy the code
  • ConnectAdvanced is a function that takes a selector and options object:

    • selectorThe method is user defined, receive onedispacthParameter, and finally you need to return one with an incoming parameterNextState and nextOwnPropsThis inner function eventually returns an object whose key-value pair is to be passed tocomponentThe component’sprops;
    • optionsObject, either official or custom, is passed in. If it is custom, it is passed inselectorIn the second argument of. There are several important parameters that are officially specified, among whichpureThe value of determines whether the last returned component should be optimized for performance;
    • connectAdvancedThe result is a higher-order function that takes the component to wrap.
  • The options value is then checked and a warning is given to properties that are about to be removed.

  • Save the context in options;

  • Return the higher-order function wrapWithConnect(WrappedComponent), taking the component to wrap. The next step is to analyze the contents of wrapWithConnect(WrappedComponent).

  • First check the incoming component to determine whether it is a qualified component;

  • Record the name of the wrapped component, and then name the new component returned, connectComponent.

  • Encapsulate the extra arguments in options with the defined arguments as the selector’s arguments.

  • According to the options parameter pure, use the usePureOnlyMemo variable to determine whether to use the useMemo method.

  • Create a Connect component with ConnectFunction and set WrappedComponent and displayName for the component. ConnectFunction is the core of connectAdvanced:

    • ConnectFunctionTake a parameterpropsthepropsYou know, after the packageConnetComponentThe parameters of theprops;
    • willContext, ref, and the rest of the parametersfrompropsIs decoupled from;
    • Get the finalcontextAnd get out of itcontextValue;
    • To obtainstore;
    • To define amemoFunction, whenstoreThe user-defined one is invoked if the value ofselectorFunctions;
    • Get the finalContextValueThe value of the;
    • To eventually return the wrapped componentactualChildProps
    • Used when the value of a dependency changesuseEffectoruseLayoutEffectTo invoke thecaptureWrapperProps, which will be currentconnentComponentthepropsandstorethestateRecord it so you can compare it to the next rendering ifstoreThe value of thenotifyNestedSubsFires each callback function;
    • whenstoreorsubscriptionWhen an update is made, a notification is initiated,subscribeUpdates;
    • The last judgmentshouldHandleStateChangesTo decide whether or not to parcel it outsideContextIf not, return it directlyConnectComponents.
  • After the Connect component is obtained, we need to determine the forwardRef. If it exists, we need to use the React. ForwardRef to forward the Connect; otherwise, we need to use Connect directly.

summary

ConnectAdvanced returns a Context.Comsumer with props for the last component passed in. The main difference between connectAdvanced and connect is that the internal pure property defaults to false. That is, it does not start performance tuning.

If react-Redux uses context to pass stores, what is done internally to support custom context as well?

React-redux uses a Context to store and a subscription to publish the store, and when you want to use a Context with react-Redux, Remember to insert the same name of context into the component as props, so connectAdvanced will incorporate the user-defined context as props into the final component’s props.

So while react-Redux allows you to make a global context, it’s not necessary

shallowEqual

Before exploring the connect source code, learn about shallowEqual:

value = = = is(x,y)
NaN, NaN false true
0, + 0 true true
0、-0 true false
+ 0, 0 true false
. / / the Object is to basic data types: null, and undefined, number, string, Boolean make very precise comparison,
// However, there is no direct comparison for reference data types.
function is(x, y) {
  if (x === y) {
    // Handle 0, +0, and -0 cases
    returnx ! = =0|| y ! = =0 || 1 / x === 1 / y
  } else {
    // Handle NaN cases
    returnx ! == x && y ! == y } }export default function shallowEqual(objA, objB) {
  // Use is to determine whether two variables are equal
  if (is(objA, objB)) return true
    
  if (
    typeofobjA ! = ='object' ||
    objA === null ||
    typeofobjB ! = ='object' ||
    objB === null
  ) {
    return false
  }
  
  // Iterate over the key-value pairs of the two objects, comparing them once
  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. The prototype. The hasOwnProperty and operator is different, in this method will ignore those attributes inherited from the prototype chain.
      !Object.prototype.hasOwnProperty.call(objB, keysA[i]) || ! is(objA[keysA[i]], objB[keysA[i]]) ) {return false}}return true
}
Copy the code

The first comparison is called to determine whether the two values are the same. Some common basic letters === are used to determine if there is a problem; Then check whether it is not an object or null. If so, return false. Otherwise, iterate over the keys of the two objects and compare them. As you can see from the source code, shallow comparisons are not applicable to nested type comparisons.

That’s the idea of a shallow comparison

As you can see from MDN, IS is a Polyfill implementation of object.is ().

connect

Look at the source code by the way to see the official document, the first feeling is my English really progress! The second feeling is that I did not flip before, how to see a lot of negligence? !

Basic architecture

// src/connect/connect.js
import connectAdvanced from '.. /components/connectAdvanced'
import shallowEqual from '.. /utils/shallowEqual'
import defaultMapDispatchToPropsFactories from './mapDispatchToProps'
import defaultMapStateToPropsFactories from './mapStateToProps'
import defaultMergePropsFactories from './mergeProps'
import defaultSelectorFactory from './selectorFactory'

export function createConnect({ onnectHOC = 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 } = {}){
       // ...
       return connectHOC()
   } 
}

export default /*#__PURE__*/ createConnect()
Copy the code

The createConnect method is executed before the export, and it returns a familiar connect function, with the internal HOC function called. It involves a lot of secret parameters with similar names, which makes it a little painful to see.

When createConnect() is called, no parameters are passed, so the parameters are read by default:

connectHOC = connectAdvanced,
mapStateToPropsFactories = defaultMapStateToPropsFactories,
mapDispatchToPropsFactories = defaultMapDispatchToPropsFactories,
mergePropsFactories = defaultMergePropsFactories,
selectorFactory = defaultSelectorFactory,
Copy the code

We then return the connect function of our property, which supports four arguments: mapStateToProps, mapDispatch, mergeProps, and an object. According to the official document:

  • MapStateToProps: Functions that can be passed in store.getState() and the props of the current component itself.

  • MapDispacthToProps: functions that receive props from store.dispacth and the current component itself. Finally, an object needs to be returned.

  • MergeProps: functions that receive the props of store.getState(), store.dispacth, and the component itself.

  • Options: This object has several values:

    { context? :Object.// When we introduce the Provider into the context, we can write it herepure? : boolean,// In order to improve react-Redux performance, pureComponent is enabled by default and will only rerender the component if the result of its props, mapStateToProps, or mapDispatchToProps changesareStatesEqual? :Function.// The comparison function for state updates passed in when pure is enabledareOwnPropsEqual? :Function.// This is the comparison function of the props update when pure is enabledareStatePropsEqual? :Function.// Comparison function when mapStateToProps is updated when pure is enabledareMergedPropsEqual? :Function.// The comparison function when the return value of mergeProps is updated when pure is enabledforwardRef? : boolean,// If the component needs to receive a ref, set this to true
    }
    Copy the code

Finally, it returns a higher-order function. ConnectHOC is just like connectAdvanced, which takes two arguments, a selector function and an options object:

return connectHOC(selectorFactory, {
    // To use when an error is reported
    methodName: 'connect'.// Give the new component a name based on the name of the component wrapped in connect
    getDisplayName: (name) = > `Connect(${name}) `.// If mapStateToProps is Null, then there is no need to listen for updates to state
    shouldHandleStateChanges: Boolean(mapStateToProps),

    // The following values are given to selectorFactory
    // The second parameter of connectAdvanced, options, is an object
    // All properties except the specified key will eventually become the second argument to the selector function
    initMapStateToProps,
    initMapDispatchToProps,
    initMergeProps,
    pure,
    areStatesEqual,
    areOwnPropsEqual,
    areStatePropsEqual,
    areMergedPropsEqual,

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

Step by step to understand the default parameters in createConnect:

connectHoc

Its default value is connectAdvanced, which I’ve already seen above, so I won’t repeat.

defaultMapStateToPropsFactories

To get started, take a look at the file wrapMapToProps introduced in this source code:

import verifyPlainObject from '.. /utils/verifyPlainObject'

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

    function constantSelector() {
      return constant
    }
    // dependsOnOwnProps determines whether the current mapStateToProps is dependent on the ownProps
    constantSelector.dependsOnOwnProps = false
    return constantSelector
  }
}

// Do not rely on ownProps
export function getDependsOnOwnProps(mapToProps) {
  returnmapToProps.dependsOnOwnProps ! = =null&& mapToProps.dependsOnOwnProps ! = =undefined
    ? Boolean(mapToProps.dependsOnOwnProps) : mapToProps.length ! = =1
}

// Encapsulate the mapToProps function
// Check whether selectorFactory is updated according to ownProps.
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 its props
    proxy.dependsOnOwnProps = true

    proxy.mapToProps = function detectFactoryAndVerify(stateOrDispatch, ownProps) {
      proxy.mapToProps = mapToProps
      proxy.dependsOnOwnProps = getDependsOnOwnProps(mapToProps)
      let props = proxy(stateOrDispatch, ownProps)
      
	  // props returns the function, handles the mapToProps, and uses the new function as a true mapToProps for subsequent calls
      if (typeof props === 'function') {
        proxy.mapToProps = props
        proxy.dependsOnOwnProps = getDependsOnOwnProps(props)
        props = proxy(stateOrDispatch, ownProps)
      }
	  // If it is not a normal object, a warning is issued
      if(process.env.NODE_ENV ! = ='production')
        verifyPlainObject(props, displayName, methodName)

      return props
    }

    return proxy
  }
}
Copy the code
// src/connect/mapStateToProps.js
import { wrapMapToPropsConstant, wrapMapToPropsFunc } from './wrapMapToProps'

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

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

export default [whenMapStateToPropsIsFunction, whenMapStateToPropsIsMissing]

Copy the code

MapStateToProps returns an array, respectively is whenMapStateToPropsIsFunction and whenMapStateToPropsIsMissing, from the function name, when mapStateToProps function or empty, The two functions are called separately.

defaultMapDispatchToPropsFactories

// src/connect/mapDispatchToProps.js
import bindActionCreators from '.. /utils/bindActionCreators'
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

MapDispatchToProps returns an array, Is whenMapDispatchToPropsIsFunction, whenMapDispatchToPropsIsMissing and whenMapDispatchToPropsIsObject respectively, from the function name, MapDispatchToProps is called when mapDispatchToProps is a function, empty, or object.

defaultMergePropsFactories

// src/connect/mergeProps.js
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) {
        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 ?() = > defaultMergeProps : undefined
}

export default [whenMergePropsIsFunction, whenMergePropsIsOmitted]

Copy the code

MergeProps returns an array of mergeProps whenMergePropsIsFunction and whenMergePropsIsOmitted, which are called whenMergePropsIsFunction is a function or is omitted.

defaultSelectorFactory

Select the factory function to generate the selector function required for connectAdvanced. The selector’s first argument is Dispatch and its second argument is an object.

// import defaultSelectorFactory from './selectorFactory'
// selectorFactory.js

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

  if(process.env.NODE_ENV ! = ='production') {
    // ...
  }
    
  // Use different functions based on Pure and call them
  const selectorFactory = options.pure
    ? pureFinalPropsSelectorFactory
    : impureFinalPropsSelectorFactory

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

If we haven’t turned on pure, we can return mergeProps without doing anything, and print the props we want to pass to the wrapped component:

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

When this function is executed for the first time, it uses the stored state and props to obtain the combined props passed to the package component.

If so, call handleSubsequentCalls to compare whether this update updates only state, props only, or both, then call different logic, and finally give the wrapped component the final props.

The following logic is related to the performance of react-Redux. MapStateToProps, mapDispatchtoProps, mergeProps, and Options can be passed to connect. The first two are required.

  • mapStateToPropsYou can pass in two arguments,stateandownProps, and finally return a new onestateSo we can know the newstateNot only depends onstoreAnd also depends onownPropsIf you update one of them, it will be recalculatedstate;
  • mapDispatchtoPropsIt also supports passing two arguments,dispatchandownPropsTo return a newdispatchThe newdispatchDepends ondispacthandownProps;
  • mergePropsIt’s a function, and by default we’re going to omit it, so it’s going to be assigned by default, and then the newstate, newdispacthandownPropsInto a newpropsPassed to theConnectComponents.

The react-Redux function has been optimized to improve the performance of new components that are returned by Connect, both by the store and by the component itself props.

Connect has one more pure argument than connectAdvanced. The default is True. Is used to enable performance tuning.

export function pureFinalPropsSelectorFactory(mapStateToProps, mapDispatchToProps, mergeProps, dispatch, { areStatesEqual, areOwnPropsEqual, areStatePropsEqual }) {
  let hasRunAtLeastOnce = false
  let state
  let ownProps
  let stateProps
  let dispatchProps
  let mergedProps
	
  // For the first execution, obtain the final stateProps and dispatchProps based on the current state and props
  // Then retrieve the final component props from the same 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
  }
  
  // Call this function if both its Props and store have changed
  function handleNewPropsAndNewState() {
    // Get the new stateProps directly from mapStateToProps
    stateProps = mapStateToProps(state, ownProps)
    // If the mapDispatchToProps method depends on the ownProps method, then a new dispatchProps method must be obtained
    if (mapDispatchToProps.dependsOnOwnProps)
      dispatchProps = mapDispatchToProps(dispatch, ownProps)
	// Pass the new stateProps, dispatchProps, and mergedProps to get the props of the final component
    mergedProps = mergeProps(stateProps, dispatchProps, ownProps)
    return mergedProps
  }
	
  // If only your props change
  function handleNewProps() {
    // If the ownProps parameter is used when declaring mapStateToProps, a new stateProps will be generated.
    if (mapStateToProps.dependsOnOwnProps)
      stateProps = mapStateToProps(state, ownProps)
	
    // If the second parameter (ownProps) is used when declaring mapDispatchToProps, this parameter will be set to true
    if (mapDispatchToProps.dependsOnOwnProps)
      dispatchProps = mapDispatchToProps(dispatch, ownProps)

    mergedProps = mergeProps(stateProps, dispatchProps, ownProps)
    return mergedProps
  }
 
  // Only store changes
  function handleNewState() {
    // When the store changes, mapStateToProps returns the new value
    const nextStateProps = mapStateToProps(state, ownProps)
    // Determine whether the new stateProps are the same as the last stateProps
    conststatePropsChanged = ! areStatePropsEqual(nextStateProps, stateProps) stateProps = nextStateProps// If there is a change, the final props need to be recalculated
    if (statePropsChanged)
      mergedProps = mergeProps(stateProps, dispatchProps, ownProps)

    return mergedProps
  }
 
  // If it is not the first execution,
  function handleSubsequentCalls(nextState, nextOwnProps) {
    // Determine whether the Props of the component itself have changed
    constpropsChanged = ! areOwnPropsEqual(nextOwnProps, ownProps)// Determine whether the store has changed
    conststateChanged = ! areStatesEqual(nextState, state)// Record the current state and ownProps for comparison in the next update
    state = nextState
    ownProps = nextOwnProps

    if (propsChanged && stateChanged) return handleNewPropsAndNewState()
    if (propsChanged) return handleNewProps()
    if (stateChanged) return handleNewState()
    return mergedProps
  }

  return function pureFinalPropsSelector(nextState, nextOwnProps) {
    // Call handleSubsequentCalls or handleFirstCall to determine if it was executed once
    return hasRunAtLeastOnce
      ? handleSubsequentCalls(nextState, nextOwnProps)
      : handleFirstCall(nextState, nextOwnProps)
  }
}
Copy the code

With selectorFactory in mind, the next step is the execution of connectAdvanced, which I won’t repeat.

Hook

Finally, let’s talk about the newly added hooks. In June, the project was still eating the old money. It was clear that the project used hooks, but about React-Redux, I was completely unaware of the existence of hooks.

useSelector

UseSelector is a bit like mapStateToProps. We can use the props to get the state we want from the store, but the difference is that mapStateToProps uses a shallow comparison, while useSelector uses a deep comparison. Whenever the state changes, UseSelector returns only one value. If you want to return an object, you are advised to use shallowEqual or create a memory selector with reselect that returns multiple values in one object. But only if one of the values changes returns a new object.

import { shallowEqual, useSelector } from 'react-redux'
import { createSelector } from 'reselect'

// Regular usage
export const CounterComponent = () = > {
  const counter = useSelector((state) = > state.counter)
  return <div>{counter}</div>
}

// Use a shallow comparison to get an object
export const ObjComponent = () = > {
  const obj = useSelector((state) = > state.obj, shallowEqual)
  return <div>{obj.counter}</div>
}

/ / reselect
const selectNumCompletedTodos = createSelector(
  (state) = > state.todos,
  (todos) = > todos.filter((todo) = > todo.completed).length
)

export const CompletedTodosCounter = () = > {
  const numCompletedTodos = useSelector(selectNumCompletedTodos)
  return <div>{numCompletedTodos}</div>
}

export const App = () = > {
  return (
    <>
      <span>Number of completed todos:</span>
      <CompletedTodosCounter />
    </>)}Copy the code

Next, start analyzing the source code:

// useSelector is returned by createSelectorHook
export const useSelector = /*#__PURE__*/ createSelectorHook()
Copy the code

So we can infer that

createSelectorHook() = (selector, equalityFn) = > selectedState
Copy the code

Let’s see createSelectorHook:

export function createSelectorHook(context = ReactReduxContext) {
  CreateSelectorHook () does not pass parameters, so context = ReactReduxContext
  const useReduxContext =
    / / get the context
    context === ReactReduxContext
      ? useDefaultReduxContext
      : () = > useContext(context)
  
  // Return a function (selector, equalityFn = refEquality) => selectorState to useSelector
  // const refEquality = (a, b) => a === b useSelector
  return function useSelector(selector, equalityFn = refEquality) {
    // Check the selector, equalityFn
    if(process.env.NODE_ENV ! = ='production') {
      if(! selector) {throw new Error(`You must pass a selector to useSelector`)}if (typeofselector ! = ='function') {
        throw new Error(`You must pass a function as a selector to useSelector`)}if (typeofequalityFn ! = ='function') {
        throw new Error(
          `You must pass a function as an equality function to useSelector`)}}// As we saw earlier in the source code, the context will store stores and subscription
    const { store, subscription: contextSub } = useReduxContext()
	
    // Get selectedState and return it
    const selectedState = useSelectorWithStoreAndSubscription(
      selector,
      equalityFn,
      store,
      contextSub
    )

    useDebugValue(selectedState)

    return selectedState
  }
}
Copy the code

To look at how useSelectorWithStoreAndSubscription selectState return we want:

function useSelectorWithStoreAndSubscription(
  selector,
  equalityFn,
  store,
  contextSub    // subscription
) {
  const [, forceRender] = useReducer((s) = > s + 1.0)
  
  // Create a new subscription object
  const subscription = useMemo(() = > new Subscription(store, contextSub), [
    store,
    contextSub,
  ])
  
  const latestSubscriptionCallbackError = useRef()
  const latestSelector = useRef()
  const latestStoreState = useRef()
  const latestSelectedState = useRef()
  
  // Get the state in the current store
  const storeState = store.getState()
  let selectedState

  try {
    // If the selector changes, or the state in the store changes, or an error occurs
    // Then re-execute selector to get the new selectState
    // Otherwise, return the last selectState
    if( selector ! == latestSelector.current || storeState ! == latestStoreState.current || latestSubscriptionCallbackError.current ) {// Compare new selectState to last selectState
      // If so, return the last selectState
      // Otherwise, return new selectState
      const newSelectedState = selector(storeState)
      if (
        latestSelectedState.current === undefined| |! equalityFn(newSelectedState, latestSelectedState.current) ) { selectedState = newSelectedState }else {
        selectedState = latestSelectedState.current
      }
    } else {
      selectedState = latestSelectedState.current
    }
  } catch (err) {
    if (latestSubscriptionCallbackError.current) {
      err.message += `\nThe error may be correlated with this previous error:\n${latestSubscriptionCallbackError.current.stack}\n\n`
    }

    throw err
  }
  
  // This function saves the selector, store state, selectState, and subscription capture errors each time it is executed
  // For the next comparison
  useIsomorphicLayoutEffect(() = > {
    latestSelector.current = selector
    latestStoreState.current = storeState
    latestSelectedState.current = selectedState
    latestSubscriptionCallbackError.current = undefined
  })
  
  // When the store or subscription changes
  // Call checkForUpdates to get the new storeState and selectState
  // Compare the value of selectState to the value of selectState last time
  / / if, update latestSelectedState and latestStoreState latestSubscriptionCallbackError
  useIsomorphicLayoutEffect(() = > {
    function checkForUpdates() {
      try {
        const newStoreState = store.getState()
        const newSelectedState = latestSelector.current(newStoreState)

        if (equalityFn(newSelectedState, latestSelectedState.current)) {
          return
        }

        latestSelectedState.current = newSelectedState
        latestStoreState.current = newStoreState
      } catch (err) {
        // we ignore all errors here, since when the component
        // is re-rendered, the selectors are called again, and
        // will throw again, if neither props nor store state
        // changed
        latestSubscriptionCallbackError.current = err
      }

      forceRender()
    }

    subscription.onStateChange = checkForUpdates
    subscription.trySubscribe()

    checkForUpdates()

    return () = > subscription.tryUnsubscribe()
  }, [store, subscription])
  
  // Finally return the selectedState we need
  return selectedState
}
Copy the code

Mainly through useSelector internal useSelectorWithStoreAndSubscription selectState to get what we want, UseSelectorWithStoreAndSubscription by comparing the selector and store to judge whether or not to implement the selector to get the latest selectState, and then capturing the new selectState selectState last time To determine which value to return. The current selector, store, selectState, and captured errors are recorded internally for comparison purposes. The purpose is to improve the performance of the useSelector without having to return new content every time the data is the same as before and cause the view to update.

useDispatch

The essence of useDiapatch is to get dispatches in a store. Here’s an official chestnut:

import React, { useCallback } from 'react'
import { useDispatch } from 'react-redux'

export const CounterComponent = ({ value }) = > {
const dispatch = useDispatch()
 const increaseCounter = useCallback(() = > dispatch({ type: 'increase-counter' }), [])
  return (
   <div>
     <span>{value}</span>
      <button onClick={increaseCounter}>Increase counter</button>
   </div>)}Copy the code

Look directly at the source:

export function createDispatchHook(context = ReactReduxContext) {
  const useStore =
    context === ReactReduxContext ? useDefaultStore : createStoreHook(context)

  return function useDispatch() {
    const store = useStore()
    return store.dispatch
  }
}
export const useDispatch = /*#__PURE__*/ createDispatchHook()
Copy the code

It makes sense to first get the Context, then get the store in the Context, and then return to Store.Dispatch.

useStore

In useDispatch, there is a code called createStoreHook(context), which is actually an implementation of useStore:

export function createStoreHook(context = ReactReduxContext) {
  const useReduxContext =
    context === ReactReduxContext
      ? useDefaultReduxContext
      : () = > useContext(context)
  return function useStore() {
    const { store } = useReduxContext()
    return store
  }
}
export const useStore = /*#__PURE__*/ createStoreHook()
Copy the code

First get the Context, then get the store in the Context, and finally return the Store ~

summary

Source intermittently looked at a week, but also looked back and forth several times, a little around. The website says that connectAdvanced will be deprecation in the near future, but we still need to work hard to implement Connect. MapStateToProps () and mapDispacthToProps () can use the ownProps () as the second parameter of the useMemo hook (). I also came into contact with the resELECT library.

Finally, be sure to read more English documents

reference

  • The react – the story’s official website
  • React-redux (1): connectAdvanced
  • Polyfill
  • Do you really understand shallow comparison?

If there are any mistakes, please point them out, thanks for reading ~