One, foreword

1.1 What is React-Redux

As the name suggests, React-Redux is a Redux writing method specifically designed for the React framework. Compared with the traditional Redux framework, react-Redux has more easy-to-use apis to simplify writing and improve performance.

We use react-Redux more in actual React projects than Redux directly.

1.2 Why is React-Redux needed

In the Redux series – Understanding Redux in depth, we learned that Redux is essentially a framework for rerendering pages and modifying the UI by listening for store updates, and we can use it this way.

const Counter = ({ value, onIncrement, onDecrement }) = > (
  <div>
  <h1>{value}</h1>
  <button onClick={onIncrement}>+</button>
  <button onClick={onDecrement}>-</button>
  </div>
);

const reducer = (state = 0, action) = > {
  switch (action.type) {
    case 'INCREMENT': return state + 1;
    case 'DECREMENT': return state - 1;
    default: returnstate; }};const store = createStore(reducer);

const render = () = > {
  ReactDOM.render(
    <Counter
      value={store.getState()}
      onIncrement={()= > store.dispatch({type: 'INCREMENT'})}
      onDecrement={() => store.dispatch({type: 'DECREMENT'})}
    />.document.getElementById('root')); }; render();31 store.subscribe(render);
Copy the code

The above code block is an example of a counter implemented using Redux. In line 31 we register a store listener and re-call the render function every time we receive an action, rendering the entire page with the latest state, which is obviously inefficient.

React-redux was created to solve this problem. React is a Redux framework that incorporates features of React.

Two, principle exploration

2.1 How does React-Redux update the page

In Redux, we need to manually register with the subscribe method to listen for state changes in the store to update the page in the callback method. But with react-Redux, we don’t register manually. So how does the page update?

The secret is in the hooks method useSelector, but before we get to that method, it’s worth looking at the Provider component.

2.1.1 Provider component

2.1.1.1 Using Provider

<Provider store={store}>
	<App/>
</Provider>
Copy the code

We pass the Store in Redux into the Store property of the Provider component and wrap the Provider component around the outermost layer of the App. The React component actually uses a Context. See Context for details

2.1.1.2 the Provider source

The source code for the Provider component is this one, and we will make some cuts to the source code and add some comments.

function Provider({ store, context, children }) {
  // Initialize contextValue, which returns an object containing store and subscription, which is the custom store passed in to our argument. Subscription can be understood as a collection of registered listeners
  const contextValue = useMemo(() = > {
    const subscription = createSubscription(store)
    subscription.onStateChange = subscription.notifyNestedSubs
    return {
      store,
      subscription,
    }
  }, [store])

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

  UseEffect is called when the Provider component is loaded
  useIsomorphicLayoutEffect(() = > {
    const { subscription } = contextValue
    // Initialize subscription
    subscription.trySubscribe()
		// Call all registered callbacks in subscription if state is not equal
    if(previousState ! == store.getState()) { subscription.notifyNestedSubs() }return () = > {
      subscription.tryUnsubscribe()
      subscription.onStateChange = null
    }
  }, [contextValue, previousState])

  const Context = context || ReactReduxContext
	// Use Context where value is an object that encapsulates store and subscription.
  return <Context.Provider value={contextValue}>{children}</Context.Provider>
}
Copy the code

You can see that the Provider finally returns a context. Provider component so that we can get its value in the Provider’s child, which is an object that encapsulates the Store and registration callback.

2.1.2 useSelector

2.1.2.1 Use of useSelector

const dayModel = useSelector((state) = > state.userName)
Copy the code

We typically use selector in functional components in this way, so that when state.username changes, the component is rerendered.

2.1.2.2 useSelector source

Why does useSelector cause the component to be rerendered when userName changes? Is it because we registered some listeners and so on? Let’s take a look at useSelector source code

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

3 function useSelectorWithStoreAndSubscription(selector, equalityFn, store, contextSub) {
9   const [, forceRender] = useReducer((s) = > s + 1.0)

11   const subscription = useMemo(
    () = > createSubscription(store, contextSub),
    [store, contextSub]
  )

16   const latestSubscriptionCallbackError = useRef()
  const latestSelector = useRef()
  const latestStoreState = useRef()
19   const latestSelectedState = useRef()

  const storeState = store.getState()
  let selectedState

  try {
    if (
26selector ! == latestSelector.current || storeState ! == latestStoreState.current || latestSubscriptionCallbackError.current ) {30      const newSelectedState = selector(storeState)
      if (
32       latestSelectedState.current === undefined ||
33! 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
  }

49  useIsomorphicLayoutEffect(() = > {
    latestSelector.current = selector
    latestStoreState.current = storeState
    latestSelectedState.current = selectedState
    latestSubscriptionCallbackError.current = undefined
  })

56  useIsomorphicLayoutEffect(() = > {
    function checkForUpdates() {
      try {
        const newStoreState = store.getState()
        if (newStoreState === latestStoreState.current) {
          return
        }

        const newSelectedState = latestSelector.current(newStoreState)

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

        latestSelectedState.current = newSelectedState
        latestStoreState.current = newStoreState
      } catch (err) {
        latestSubscriptionCallbackError.current = err
      }

      forceRender()
    }

79    subscription.onStateChange = checkForUpdates
    subscription.trySubscribe()

    checkForUpdates()

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

  return selectedState
}

90 export function createSelectorHook(context = ReactReduxContext) {
  const useReduxContext =
    context === ReactReduxContext
      ? useDefaultReduxContext
      : () = > useContext(context)
  return function useSelector(selector, equalityFn = refEquality) {
96     const { store, subscription: contextSub } = useReduxContext()

98    const selectedState = useSelectorWithStoreAndSubscription(
      selector,
      equalityFn,
      store,
      contextSub
    )

    return selectedState
  }
}

109 export const useSelector = /*#__PURE__*/ createSelectorHook()
Copy the code

There’s a lot of code, so I’m not going to comment it line by line, but I’m going to highlight it.

First, on line 109 we can see that useSelector actually calls the createSelector function, which returns the real useSelector method

As you can see from the createSelectorHook definition on line 90, it takes one parameter, context. The default value is ReactReduxContext. This context is used by the outermost Provider component. We can control the context we use by createSelectorHook.

In line 96, we read the value we passed in the outermost Provider component from the context, get store, and the subscription, and rename the subscription to contextSub

In line 98, we call useSelectorWithStoreAndSubscription method and introduced into four parameters

  • Selector is the selector function that we pass in when we use the useSelector method
  • EqualityFn This function, defined in line 1, defines how to compare the values returned by selector, and you can see that the default is shallow comparison.
  • Store is the store that we pass to the Context in the Provider
  • ContextSub is the subscription in our context

UseSelectorWithStoreAndSubscription method returns to a state, and in return line 105, selectedState is what we call the selector method after the return of data, So we have reason to speculate about the role of useSelectorWithStoreAndSubscription method is invoked the selector methods return data, and register to monitor.

Defines the useSelectorWithStoreAndSubscription function in row 3,

In line 9, the forceRender function is obtained using the useDispatch method.

The forceRender function is just an ordinary dispatch function, but the effect of calling the dispatch function is similar to setState, which will make the current component render again. So it’s okay to call it the forceRender function.

In line 11, we can simply read store and subscription in the context to generate a new subscription

So line 16, line 19 defines a bunch of refs, and then line 26 determines if the selector has changed, if the state has changed, and so on, and the first ref is empty is definitely not equal, so we go to line 30

In line 30, we finally call the selector method, pass in state, and get the data we want

At line 32, line 33, we do the selector return, and if it’s the same as the old value, we use the old value, otherwise we use the new value

Line 49 is the assignment to ref

The 56th line useIsomorphicLayoutEffect is more important, it can be as simple as useEffect, focus on line 79 and line 80.

79 line, we’ll checkForUpdates functions assigned to the subscription onStateChange method, and in line 80 call subscription. TrySubscribe, look into trySubscribe method

function trySubscribe() {
    if(! unsubscribe) { unsubscribe = parentSub ? parentSub.addNestedSub(handleChangeWrapper) : store.subscribe(handleChangeWrapper) listeners = createListenerCollection() } }Copy the code

You can see that it registers a callback with subscribe to the store, calling handleChangeWrapper every time it dispatches an action, and the handleChangeWrapper function is implemented as shown below

  function handleChangeWrapper() {
    if (subscription.onStateChange) {
      subscription.onStateChange()
    }
  }
Copy the code

Call the onStateChange subscription, we already know useSelectorWithStoreAndSubscription onStateChange method assigned to checkForUpdates,

Therefore, the Store calls the checkForUpdates function every time the Dispatch action is performed.

If we go back to line 57 and look at the implementation of the checkForUpdates function, we can see that it evaluates the values by selector and calls the equalityFn function to compare them. If they are not equal, it will eventually call forceRender to render the current interface again. Rerender the interface with the new selectedState.

conclusion

What useSelector does is it gets the data it needs from the selector, registers a callback with subscribe to the store, and every time the state changes it calls the selector again and compares the value it returns with the comparison function, If they differ, the current component is re-rendered.

2.1.3 useDispatch method

Compared to useSelector, useDispatch is much simpler and posts the source code directly

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

So you can see it’s a dispatch method that takes a store directly from the context and returns a store.

2.1.4 the connect method

The useSelector method can be used in functional components, but cannot use Hooks if we are using class components, instead use connect.

2.1.3.1 Use of CONNECT

import { connect } from 'react-redux'

const VisibleTodoList = connect(
  mapStateToProps,
  mapDispatchToProps
)(TodoList)
Copy the code

Well, there is no more explanation to use, you can refer to this article

2.1.3.2 connect source

The source address is this

There is too much code associated with CONNECT to go into all of them, so let’s pick out a few core areas.

First, we know that connect() returns a higher-order component. For details on what a higher-order component is, see this article: Higher-order Component

We need to focus on two things:

  1. How do we get state data and Dispatch into component props using mapStateToProps and mapDispatchToProps, respectively
  2. How is our component updated when state is updated

For point 1, we need to look at this code and note its ConnectFunction function

function ConnectFunction(props) {
      const [propsContext, reactReduxForwardedRef, wrapperProps] =
        useMemo(() = > {
4          const{ reactReduxForwardedRef, ... wrapperProps } = propsreturn [props.context, reactReduxForwardedRef, wrapperProps]
        }, [props])

      const ContextToUse = useMemo(() = > {
        return propsContext &&
          propsContext.Consumer &&
          isContextConsumer(<propsContext.Consumer />)? propsContext : Context }, [propsContext, Context])const contextValue = useContext(ContextToUse)
      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.`)}const store = didStoreComeFromProps ? props.store : contextValue.store

37      const childPropsSelector = useMemo(() = > {
        return createChildSelector(store)
      }, [store])

      const [subscription, notifyNestedSubs] = useMemo(() = > {
        if(! shouldHandleStateChanges)return NO_SUBSCRIPTION_ARRAY
        const subscription = createSubscription(
          store,
          didStoreComeFromProps ? null : contextValue.subscription
        )

        const notifyNestedSubs =
          subscription.notifyNestedSubs.bind(subscription)

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

      const overriddenContextValue = useMemo(() = > {
        if (didStoreComeFromProps) {
          return contextValue
        }

        return {
          ...contextValue,
          subscription,
        }
      }, [didStoreComeFromProps, contextValue, subscription])
      const [[previousStateUpdateResult], forceComponentUpdateDispatch] =
        useReducer(storeStateUpdatesReducer, EMPTY_ARRAY, initStateUpdates)

      if (previousStateUpdateResult && previousStateUpdateResult.error) {
        throw previousStateUpdateResult.error
      }

      const lastChildProps = useRef()
      const lastWrapperProps = useRef(wrapperProps)
      const childPropsFromStoreUpdate = useRef()
      const renderIsScheduled = useRef(false)

 76 const actualChildProps = usePureOnlyMemo(() = > {
        if (
          childPropsFromStoreUpdate.current &&
          wrapperProps === lastWrapperProps.current
        ) {
          return childPropsFromStoreUpdate.current
        }
 83       return childPropsSelector(store.getState(), wrapperProps)
      }, [store, previousStateUpdateResult, wrapperProps])

      useIsomorphicLayoutEffectWithArgs(captureWrapperProps, [
        lastWrapperProps,
        lastChildProps,
        renderIsScheduled,
        wrapperProps,
        actualChildProps,
        childPropsFromStoreUpdate,
        notifyNestedSubs,
      ])

      useIsomorphicLayoutEffectWithArgs(
        subscribeUpdates,
        [
          shouldHandleStateChanges,
          store,
          subscription,
          childPropsSelector,
          lastWrapperProps,
          lastChildProps,
          renderIsScheduled,
          childPropsFromStoreUpdate,
          notifyNestedSubs,
          forceComponentUpdateDispatch,
        ],
        [store, subscription, childPropsSelector]
      )

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

      const renderedChild = useMemo(() = > {
        if (shouldHandleStateChanges) {
          return (
            <ContextToUse.Provider value={overriddenContextValue}>
              {renderedWrappedComponent}
            </ContextToUse.Provider>)}return renderedWrappedComponent
      }, [ContextToUse, renderedWrappedComponent, overriddenContextValue])

      return renderedChild
    }
Copy the code

And we know from the code,

This is a functional component, which means that it can use Hooks! This is important because useSelector, which we analyzed earlier, uses Hooks a lot.

In line 116 of the above block, we added props to the returned component, so let’s look at where actualChildProps comes from.

The value of actualChildProps is assigned at line 76, via a Hooks function that can simply be understood as useMemo,

As you can see in line 76, memo, if there is no change in the props, use the same value, then recalculate if there is a change, and we need to look at the change, so we need to look at line 83

Line 83 calls childPropsSelector and passes in the current state and wrapperProps. Before we look at the definition of this function, it’s worth looking at what wrapperProps is.

WrapperProps is a common prop written by a user when using a component. Now we can rest easy and look at the childPropsSelector function

You can see on line 37, the function is generated by createChildSelector

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

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

As you can see on line 15 above, with the createChildSelector function, with the selectorFactory function, SelectorFactory takes store.dispatch, selectorFactoryOptions, dispatch we’re familiar with, selectorFactoryOptions we can think of as simply a bunch of configuration information, The point is what a selectorFactory is.

SelectorFactory is the first argument to connectAdvanced, passed in from the outside, and by looking at the call logic, you can see that selectorFactory is actually this

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

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

  const selectorFactory = options.pure
    ? pureFinalPropsSelectorFactory
    : impureFinalPropsSelectorFactory

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

You can see the call pureFinalPropsSelectorFactory or impureFinalPropsSelectorFactory mapStateToProps and according to the options MapDispatchToProps combines and returns a new object.

Now that we know the answer to the first question, we passed the props of the child component by combining the props returned by mapStateToProps and the props returned by mapDispatchToProps.


Now let’s look at the second question, how our component is updated when state is updated.

The first thing to think about is that when the state updates, our component will update, so in theory we need to subscribe to the store to register listening, but this code is important

useIsomorphicLayoutEffectWithArgs(
        subscribeUpdates,
        [
          shouldHandleStateChanges,
          store,
          subscription,
          childPropsSelector,
          lastWrapperProps,
          lastChildProps,
          renderIsScheduled,
          childPropsFromStoreUpdate,
          notifyNestedSubs,
          forceComponentUpdateDispatch,
        ],
        [store, subscription, childPropsSelector]
      )
//-----------
function useIsomorphicLayoutEffectWithArgs(effectFunc, effectArgs, dependencies) {
  useIsomorphicLayoutEffect(() = >effectFunc(... effectArgs), dependencies) }Copy the code

This code is called inside the functional component ConnectFunction, which can be simply understood as calling the subscribeUpdates function in the useEffect method. Next, let’s look at the implementation of the subscribeUpdates function

function subscribeUpdates(shouldHandleStateChanges, store, subscription, childPropsSelector, lastWrapperProps, lastChildProps, renderIsScheduled, childPropsFromStoreUpdate, notifyNestedSubs, forceComponentUpdateDispatch) {
  if(! shouldHandleStateChanges)return
  let didUnsubscribe = false
  let lastThrownError = null
  const checkForUpdates = () = > {
    if (didUnsubscribe) {
      return
    }

    const latestStoreState = store.getState()

    let newChildProps, error
    try {
25      newChildProps = childPropsSelector(
        latestStoreState,
        lastWrapperProps.current
      )
    } catch (e) {
      error = e
      lastThrownError = e
    }

    if(! error) { lastThrownError =null
    }

38    if (newChildProps === lastChildProps.current) {
      if(! renderIsScheduled.current) { notifyNestedSubs() } }else {
      lastChildProps.current = newChildProps
      childPropsFromStoreUpdate.current = newChildProps
      renderIsScheduled.current = true
      forceComponentUpdateDispatch({
        type: 'STORE_UPDATED'.payload: {
          error,
        },
      })
    }
  }
54  subscription.onStateChange = checkForUpdates
  subscription.trySubscribe()
  checkForUpdates()

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

    if (lastThrownError) {
      throw lastThrownError
    }
  }

  return unsubscribeWrapper
}
Copy the code

The above code block is similar to what we saw in useSelector, with the emphasis on store and subscription in the parameters.

We in 54 to subscription. The onStateChange method specified as checkForUpdates function, and through subscription. TrySubscribe () to store the registered monitoring (about this approach is how to sign up to monitor, See the useSelector section)

Thus, at each dispatch action, the checkForUpdates method is called. In the checkForUpdates method, we evaluate the new newChildProps at line 25 and compare the old props at line 38. As you can see, only shallow comparisons can be made and no custom comparison functions can be defined. If not equal, call forceComponentUpdateDispatch send STORE_UPDATED Action, similar to the forceRender useSelector, Is this forceComponentUpdateDispatch assignment in the following code block

const [[previousStateUpdateResult], forceComponentUpdateDispatch] =
        useReducer(storeStateUpdatesReducer, EMPTY_ARRAY, initStateUpdates)
Copy the code

As you can see, this is also a dispatch returned by the useReducer. Calling the Dispatch will cause the current component to be re-rendered, thus implementing the effect of updating the page with the latest props.

Conclusion:

Because Hooks cannot be used in the class component, we package a functional component with the higher-order component, and register the store listener in the functional component, so that when the store changes, we will generate new Props again, Determine whether to update the current functional component by comparing it to the props generated last time.

conclusion

React – story by using Context peculiar to React, Hooks, high-level components, etc., make the story in the use of the React more convenient and efficient.