Look at React-Redux source with questions

Welcome to the Public Number: One front end at a time, sharing what I understand on an irregular basis

Write before

While I was reading React-Redux source code, it was natural for me to go online and find some reference articles, but I found that none of them were very thorough.Many times the API is told one by one in a flat, straight-line way, and only what a line of code does, without a clear explanation of why it does so in combination with scenarios and usage, and the source code itself is abstract.The call relationships between functions are not very well organized, and the end result is that the more you look at them, the more confused they become.This time I'll try a different interpretation, starting with the most common usage.Combine usage, ask questions, and take questions to see how they are implemented in the source code so that you can work with everyone to gradually clarify the operating mechanism of React-Redux.

The article took more than a week to finish, after a rough look at the source code, and then read and write.There are not too few sources. I try to sort out the structure in the most understandable way and try to explain the principles in a simple way.However, the complexity of the code structure cannot be overwhelmed, and many places still need time to think about how functions are called and how they are used.The article is a bit long, you can see the last one is true love~

Limited level, inevitably there are inadequate explanations or errors, and we hope you can help point them out. Thank you.

Application of React-Redux in Project

Here, I'm going to default that you're already using Redux, which provides a global object (store) to manage state for our applications.So how do you apply Redux to React?Consider that our ultimate goal is to achieve unified management of communication and state across components at different levels.So you can use the Context feature.

Create a Provider and pass the store into the Provider as the value of the current context so that the component can get the Redux store from the context

store subscribes to the unified logic for component updates

When a component needs to update its data, it needs to call store.dispatch to dispatch the action, which triggers the update of the subscription

Use store.getState() to get data when the component gets data

And all this needs to be done manually by yourself, React-Redux encapsulates the top.Let's take a look at the use of React-Redux in a piece of code:

First, on the outermost application of React, package the Provider, which is the component provided by React-Redux, and what you do here is the equivalent of the first step above

The mapStateToProps is used to establish a mapping relationship between the component and the state stored in the store. It is a function. The first parameter is state, which is the top level data stored in redux, and the second parameter is props of the component itself.Returns an object whose field is the value that the component needs to get from the store.

MappDispatchToProps is used to establish a mapping relationship between components and store.dispatch.It can be an object or a function.When it is a function, the first parameter is dispatch, and the second parameter is the props of the component itself.

When mapStateToProps is not passed, it does not cause component UI updates when store s change.

When mapDispatchToProps is not passed, dispatch is injected into the props of the component by default.

Above, if mapStateToProps or mapDispatchToProps passed ownProps, both functions will be called when the props of the component itself change.

What React-Redux did

Let's first draw a conclusion about what React-Redux does:

Provides a Subscrption class to implement logic for subscription updates

Provider s are provided to pass stores into providers so that lower-level components can retrieve stores from context s or props; changes to stores are subscribed to so that providers themselves can be updated as stores change

Provides a selector that takes functions (or directly dispatch es) of stat and dispacth action s in the store or the component's own props and selects from them the values required by the component as return values

There are two main things you can do to provide high-level connect s components:

Execute selector, get values to inject into the component, and inject them into the props of the component

Subscribe to changes in props, responsible for updating components as props change

How to do it

With the above conclusion, but I think everyone is curious about how to achieve it. The above work is done in collaboration. The final appearance is reflected in the following issues:

How Provider puts store s into context s

How to inject a state and dispatch (or a function that calls dispatch) from the store into the props of a component

We all know that in Redux, store changes and UI updates can be achieved by subscribing to a function that updates a page through store.subscribe(), and how React-Redux changes stores and connect ed components update

Next, analyze the source code one by one with these questions.

How Provider puts store s into context s

Start with Provider components, not much code, directly up source

class Provider extends Component {
constructor(props) {
super(props)
// Remove store from props
const { store } = props
this.notifySubscribers = this.notifySubscribers.bind(this)
// Declare a Subscription instance.Subscriptions, listening for state changes to execute listener s, are all implemented by instances.
const subscription = new Subscription(store)
// Bind monitoring, notify subscribers to update pages when state changes
subscription.onStateChange = this.notifySubscribers
// Put store and subscription into the state, which will be used as the value of the context later
this.state = {
store,
subscription
}
// Getting the state in the current store, as the last state, will be done after the component is mounted.
// Update Provider component if inconsistent with store new state
this.previousState = store.getState()
}
componentDidMount() {
this._isMounted = true
// Subscribe to updates after the component has been mounted.As for how to subscribe, the Subscription class is described below.
// Understand that you need to subscribe to the update function initially to update the Provider component when the state changes
this.state.subscription.trySubscribe()
// If the state s in the store s before and after have changed, then go and update the Provider component
if (this.previousState !== this.props.store.getState()) {
this.state.subscription.notifyNestedSubs()
}
}
componentWillUnmount() {
// Unsubscribe when component is uninstalled
if (this.unsubscribe) this.unsubscribe()
this.state.subscription.tryUnsubscribe()
this._isMounted = false
}
componentDidUpdate(prevProps) {
// When the component is updated, check to see if the current store is consistent with the previous store. If it is not, you should make changes based on the new data.
// It is no longer meaningful to make changes to the original data, so the Subscription instance will be redeclared after unsubscribing.
// Bind listening, set state to new data
if (this.props.store !== prevProps.store) {
this.state.subscription.tryUnsubscribe()
const subscription = new Subscription(this.props.store)
subscription.onStateChange = this.notifySubscribers
this.setState({ store: this.props.store, subscription })
}
}
notifySubscribers() {
// notifyNestedSubs() actually notifies listener to execute, which is to update the UI
this.state.subscription.notifyNestedSubs()
}
render() {
const Context = this.props.context || ReactReduxContext
// Pass this.state as the value of the context
return (
<Context.Provider value={this.state}>
{this.props.children}
</Context.Provider>
)
}
}

So look at this in conjunction with the code: It's easy to understand how Provider puts store s into context s.The main function of Provider is to get the store we passed in from props and send it down to the lower components as one of the values of the context.

However, once the store changes, the Provider has to react to it to ensure that the latest store is always in the context.So subscriptions are used here to update.The Subscription class is naturally introduced, and through its instances onStateChange listens for an event that updates the UIOn this.notifySubscribers:

subscription.onStateChange = this.notifySubscribers

After the component is mounted, subscribe to the update, and what you subscribe to here depends on the implementation of Subscription.The first conclusion here is that the onStateChange subscription is essentially an onStateChange subscription, and the function that implements the subscription is trySubscribe within the Subscription class

this.state.subscription.trySubscribe()

Then, if the States before and after are different, go and notify the subscriber to update, onStateChange will be executed, and the Provider component will be updated.When the update is complete (componentDidUpdate),It compares whether the stores before and after are the same, and if they are different, uses the new store as the value of the context, unsubscribes and re-subscribes to a new Subscription instance.The data guaranteed to be up to date.

So with all this said, it's actually just an update of the Provider component, not an internal update mechanism of a connect ed component.I guess there should be one reason to think about the potential for providers to be nested, so there is this practice of retrieving data and re-subscribing after providers are updated so that the context s passed to subcomponents are up-to-date.

Subscription

We have found that the Provider component is updated by a method in the Subscription class, and that the update of the connect higher-order component, which we will talk about later, is also implemented by it. Subscription is the core mechanism for React-Redux to implement subscription updates.

Subscription links the work of updating a page to changes in its state, specifically listener (the method that triggers page updates, in this case handleChangeWrapper), which, through the trySubscribe method, is subscribed to within the store or Subscription as appropriate.Place in the listeners array, and when the state changes, the listeners loop executes each listener, triggering page updates.

Let's talk about whether trySubscribe uses store subscriptions directly or calls addNestedSub to implement internal subscriptions, depending on the circumstances.Because there may be multiple stores in an application, the judgement here is to have different stores subscribe to their listener s without interruption.

How to inject state and dispatch into components

After injecting store s from the top level of the application, it's time to consider how to inject States and dispatch es into the component.

The normal order must be to get to the store first, then execute the two functions separately in some way, passing in the state and dispatch in the store, along with the props of the component itself as parameters to mapStateToProps and mapDispatchToProps, so that we can get these values in both functions.Their return values are then injected into the props of the component.

Speaking of this, you have to come up with a concept: selector.The props that are eventually injected into the component are the return values of selectors generated by the selectorFactory function, so that is, mapStateToProps and mapDispatchToProps are essentially selectors.

The generation process is in the core function connectAdvanced of connect, at which point you can get the store in the current context and use the store passed into the selectorFactory to generate the selector in the form of

function selector(stateOrDispatch, ownProps) {
...
return props
}

It can be seen that selector is equivalent to mapStateToProps or mapDispatchToProps, and the return value of selector is injected into the component as props.

From mapToProps to selector

The Title mapToProps refers generally to mapStateToProps, mapDispatchToProps, mergeProps

In combination with everyday use, we know that our components don't get States and dispatch es until they are wrapped by connects, so with the above conclusion, we comb the selector's mechanism separately and see the source code of connects first:

Connect is actually createConnect, and createConnect simply returns a connect function, which returns the call to connectHOC (that is, the call to connectAdvanced), and then proceeds. The call to connectAdvanced eventually returns a wrapWithConnect higher-order component, the parameter of which is what we pass inComponents of.So there's the usual use of connect:

connect(mapStateToProps, mapDispatchToProps)(Component)

You should note that mapStateToProps, mapDispatchToProps, mergeProps are all initialized once in the connect function. Why do you want to initialize them instead of using them directly?With questions, let's look down.

Initialize the selector process

First look at the code, mainly initMapStateToProps and initMapDispatchToProps, to see what this code means.

mapStateToPropsFactories and mapDispatchToPropsFactories are arrays of functions, each of which receives a parameter, either mapStateToProps or mapDispatchToProps.The match function acts as an array of looping functions, either mapStateToProps or mapDispatchToProps being executed as an input to each function and assigned to the left when the return value of the function is not false.Take a look at the match function:

function match(arg, factories, name) {
// Loop through factories, where factories are the array of processing functions exposed in the mapStateToProps and mapDisPatchToProps files
for (let i = factories.length - 1; i >= 0; i--) {
// arg is also mapStateToProps or mapDispatchToProps
// This is equivalent to passing the Star of each function in the array and passing in our mapToProps function as a parameter
const result = factories[i](arg)
if (result) return result
}
}

The match loop is an array of functions, so let's look at these two arrays, mapStateToPropsFactories and mapDispatchToPropsFactories:(The whenMapStateToPropsIsFunction function in the lower source code is explained later)

In fact, let both whenMapStateToPropsIsFunction and whenMapStateToPropsIsMissing execute mapStateToProps once, and then choose the function with the result of execution to assign to initMapStateToProps based on the situation of the incoming mapStateToProps.

wrapMapToPropsConstant returns a function that receives the parameter we passed in () => ({}), which calls the parametric function internally and assigns a constant to the constantSelector.This constant is essentially the selector generated when we do not pass mapStateToProps, which returns an empty object and therefore does not accept any state from the store.You can also see constantSelector.dependsOnOwnProps = false, indicating that the return value is independent of the props received by the connect or higher-order component.

When mapDispatchToProps is not passed, when MapDispatchToPropsIsMissing is called, constantSelector returns only one dispatch, so dispatches can only be received in the component.

When the incoming mapDispatchToProps is an object, it also calls wrapMapToPropsConstant, where, according to the previous understanding, the properties injected into the component areThe result of execution of bindActionCreators(mapDispatchToProps, dispatch).

Now let's look at the whenMapStateToPropsIsFunction function.It is called when both mapDispatchToProps and mapStateToProps are functions and the implementation is complex.Only mapStateToProps is used to illustrate this.

Reminder: the following mapToProps refer to mapDispatchToProps or mapStateToProps

// Determine whether a component should depend on its props based on the number of arguments to the mapStateToProps function
export function getDependsOnOwnProps(mapToProps) {
return mapToProps.dependsOnOwnProps !== null && mapToProps.dependsOnOwnProps !== undefined
? Boolean(mapToProps.dependsOnOwnProps)
: mapToProps.length !== 1
}
export function wrapMapToPropsFunc(mapToProps, methodName) {
// The final wrapMapToPropsFunc returns a proxy function, which is returned in the selectorFactory function
// Is called within finalPropsSelectorFactory and assigned to another variable.
// This proxy function is executed in the selectorFactory to generate the final selector
return function initProxySelector(dispatch, { displayName }) {
const proxy = function mapToPropsProxy(stateOrDispatch, ownProps) {
// Depending on whether the component relies on its props, it decides what parameters to pass when calling
return proxy.dependsOnOwnProps
? proxy.mapToProps(stateOrDispatch, ownProps)
: proxy.mapToProps(stateOrDispatch)
}
proxy.dependsOnOwnProps = true
proxy.mapToProps = function detectFactoryAndVerify(stateOrDispatch, ownProps) {
// Assign proxy.mapToProps to the mapToProps we passed in
proxy.mapToProps = mapToProps
// Determines whether ownProps need to be injected into a component based on whether the component passes in props that the component itself receives from the parent component.
// Ultimately it will be used to implement the props change of the component itself, and it will also call the effect of mapToProps
proxy.dependsOnOwnProps = getDependsOnOwnProps(mapToProps)
// Go ahead and execute proxy, when proxy.mapToProps has been assigned to the mapToProps function that we passed in,
// So props are assigned to the return value of the incoming mapToProps
let props = proxy(stateOrDispatch, ownProps)
if (typeof props === 'function') {
// If the return value is a function, then execute the function and pass in the state or dispatch in the store, as well as ownProps
proxy.mapToProps = props
proxy.dependsOnOwnProps = getDependsOnOwnProps(props)
props = proxy(stateOrDispatch, ownProps)
}
if (process.env.NODE_ENV !== 'production')
verifyPlainObject(props, displayName, methodName)
return props
}
return proxy
}
}

WapMapToPropsFunc actually returns the initProxySelector function, and the execution result of initProxySelector is a proxy, which can be understood as proxying incoming data (state or dispatch, ownProps) to our incoming mapToProps function.The result of proxy execution is proxy.mapToProps, which is essentially selector.

When the page initialization executes, dependsOnOwnProps is true, so proxy.mapToProps(stateOrDispatch, ownProps), also known as detectFactoryAndVerify, is executed.In subsequent executions, the proxy's mapToProps are assigned to the mapStateToProps or mapDispatchToProps we pass in to connect, and then whether the component should depend on its own props is assigned to dependsOnOwnProps as appropriate.(Note that this variable is used in the selectorFactory function as a basis for whether the component performs the mapToProps function based on its own props changes).

To summarize, what this function essentially does is hang the mapToProps function that we pass in to connect onto proxy.mapToProps, and then mount a dependsOnOwnProps on the proxy to make it easier to distinguish whether a component depends on its own props.Finally, the proxy is also used as the return value of the initProxySelector, so the initialization process is actually referenced by the functions of the initMapStateToProps, initMapDispatchToProps, initMergeProps assigned to them, which are executed followed by the proxy, as to where their three proxys are executed to generate a specific selector for meYou'll see below.

Now, recall our question, why do you want to initialize those three mapToProps functions?The goal is obviously to prepare the function that generates the selector to execute it at the right time and decide whether the selector should respond to changes in ownProps.

Create a selector and inject props into the component

Once you're ready to generate the selector function, you need to execute it and inject its return value into the component as props.First, give a brief overview of the injection process:

Get the store's state or dispatch, and ownProps

Execute selector

Inject the returned value of execution into the component

Next, we need to go backwards from the last step of injection to see how the selector performs.

The injection process occurs in the core function connectAdvanced of connect. Ignore other processes in the function, focus on the injection process, and simply look at the source code

There is one important thing during the injection process: the selectorFactory.This function is an important part of generating selectors.It serves as an upload and download function, passing the received dispatch, along with the three mapToProps functions, into the processing function (pureFinalPropsSelectorFactory or impureFinalPropsSelectorFactory) inside the selectorFactory, whose execution result is a call to the internal processing function.The result of the internal processing function is that the three selectors (mapStateToProps, mapDispatchToProps, mergeProps)The result of the merge after execution.This is the props that will eventually be passed to the component

Let's look at the internal implementation of selectorFactory.For clarity, just look at the internal structure

So far, we have understood when the mapToProps function was executed.Let's review this section again: How to inject state and dispatch into the component, let's comb it out from the beginning:

Incoming mapToProps

First, when you connect, you pass in mapStateToProps, mapDispatchToProps, mergeProps.Recall the usage of functions that receive state s or dispatch es and ownProps internally, and their return values pass into the props of the component.

Generating selector based on mapToProps

The return values of these functions need to be recalculated based on ownProps, so proxy functions are generated based on these functions. The result of proxy function execution is selector. The dependsOnOwnProps property is mounted on it, so it is only when it is actually executed inside selectorFactory.Basis for when to recalculate.

Pass the results of selector execution into the component as props

This step creates a call to selectorFactory within the connectAdvanced function to pass in the store and the initialized mapToProps function and other configurations.Execute mapToProps (that is, selector) within the selectorFactory, get the returned values, and finally pass them into the component.

Be accomplished

Update mechanism for React-Redux

The React-Redux update mechanism is also a subscription publishing mode.Similar to Redux, call listener to update the page once the state changes.Let's follow this process to capture the key points:

Update Who?

What is the update function for subscriptions?

How can I judge the state change?

Don't look at the code in a hurry. I think it's easier to understand these key issues by describing them in words first, instead of just looking at the code in a fog.

Update Who?

Recall that when you normally use React-Redux, are only components that have been connect ed and passed into mapStateToProps responding to changes in store s?So what's updated is the component that has been connected, and connect returns connectAdvanced, and connectAdvanced returns the component that we passed in.Essentially, connectAdvanced updates itself internally based on changes in store s to update the real components.

What is the update function for subscriptions?This can be easily seen when subscribing internally to connectAdvanced:

The function of the subscription is checkForUpdates, and what matters is what this checkForUpdates does to update the components.A reducer is built into useReducer in connectAdvanced, and what this function does is dispatch an action to trigger an update when the precondition (state change) is established.

How can I judge the state change?This is a good question to understand, because every time redux returns a new state.Just decide if the references to the States before and after are the same.

connect Core--ConneAdvanced

ConneAdvanced is a relatively heavy higher-order function. The update mechanism is outlined above, but many of the specific practices are implemented in ConneAdvanced.The source is long, the logic is a bit complex, and I've written detailed comments.The process of viewing requires thinking about the call relationships between functions and their purposes, the meaning of each variable, and with the above conclusions, it is not difficult to understand.

// This is a library of static methods to preserve components
import hoistStatics from 'hoist-non-react-statics'
import React, {
useContext,
useMemo,
useEffect,
useLayoutEffect,
useRef,
useReducer
} from 'react'
import { isValidElementType, isContextConsumer } from 'react-is'
import Subscription from '../utils/Subscription'
import { ReactReduxContext } from './Context'
const EMPTY_ARRAY = []
const NO_SUBSCRIPTION_ARRAY = [null, null]
// Built-in reducer
function storeStateUpdatesReducer(state, action) {
const [, updateCount] = state
return [action.payload, updateCount + 1]
}
const initStateUpdates = () => [null, 0]
// React currently throws a warning when using useLayoutEffect on the server.
// To get around it, we can conditionally useEffect on the server (no-op) and
// useLayoutEffect in the browser. We need useLayoutEffect because we want
// `connect` to perform sync updates to a ref to save the latest props after
// a render is actually committed to the DOM.
// My own translation of the above English comment:
// react warns when useLayoutEffect is used in a server-side environment. To solve this problem, useEffect needs to be used on the server-side and useLayoutEffect on the browser-side.
// useLayoutEffect synchronously invokes callbacks into all DOM changes.
// So you need to use it in a browser environment, because connect will synchronize the update ref to save the latest props after rendering is committed to the DOM
// The ReactHooks document describes useLayoutEffect: The update plan inside useLayoutEffect will be refreshed synchronously before the browser performs drawing.
// useEffect's effectects will be executed after each rendering round, and useLayoutEffect's effects will be executed before drawing after the dom changes.
// What effect ect does here is update
// By the time the server renders, the page has come out, and it is possible that the js has not been loaded yet.
// So you need to use useEffect in the SSR phase to make sure that after the page is taken over by js, you update it if you need to.
// There is no such problem in the browser environment
// Determine whether it's server or browser based on whether there's a window
const useIsomorphicLayoutEffect =
typeof window !== 'undefined' ? useLayoutEffect : useEffect
export default function connectAdvanced(
selectorFactory,
// options object:
{
// Get the name of the component after it is wrapped by connect
getDisplayName = name => `ConnectAdvanced(${name})`,
// For display of error message
methodName = 'connectAdvanced',
// Direct translated English comment: If defined, an attribute named this value will be added to the props passed to the packaged component.Its value will be the number of times the component is rendered, which is useful for tracking unnecessary re-rendering.Default value: undefined
renderCountProp = undefined,
// Should the connect component respond to changes in store s
shouldHandleStateChanges = true,
// This is required when multiple stores are used, in order to distinguish which store to get
storeKey = 'store',
// If true, a reference is stored in the wrapped component instance.
// And obtained by getWrappedInstance().
withRef = false,
// Used to pass ref s in
forwardRef = false,
// context used internally by components that users can customize
context = ReactReduxContext,
// The remaining configuration items, selectorFactory should use
...connectOptions
} = {}
) {
//Some error logic was omitted
// Get context
const Context = context
return function wrapWithConnect(WrappedComponent) {
const wrappedComponentName =
WrappedComponent.displayName || WrappedComponent.name || 'Component'
const displayName = getDisplayName(wrappedComponentName)
// Define selectorFactoryOptions in preparation for constructing selectors
const selectorFactoryOptions = {
...connectOptions,
getDisplayName,
methodName,
renderCountProp,
shouldHandleStateChanges,
storeKey,
displayName,
wrappedComponentName,
WrappedComponent
}
const { pure } = connectOptions
/* Call createChildSelector => createChildSelector (store) (state, ownProps)
createChildSelector Returns a parameterized call to selectorFactory, which is actually returned internally according to options.pure
impureFinalPropsSelectorFactory Or a call to pureFinalPropsSelectorFactory, which requires parameters (state, ownProps)
*/
function createChildSelector(store) {
// Here is the call to finalPropsSelectorFactory in selectorFactory.js, passing in dispatch, and options
return selectorFactory(store.dispatch, selectorFactoryOptions)
}
// Pure means something similar to React's PureComponent here, depending on whether it is pure mode or not to determine whether updates need to be optimized
const usePureOnlyMemo = pure ? useMemo : callback => callback()
function ConnectFunction(props) {
// Props changes, get the latest context,forwardedRef, and other props for components
const [propsContext, forwardedRef, wrapperProps] = useMemo(() => {
const { context, forwardedRef, ...wrapperProps } = props
return [context, forwardedRef, wrapperProps]
}, [props])
// The propsContext or Context changes, deciding which context to use and preferring if it exists
const ContextToUse = useMemo(() => {
// Users may replace ReactReduxContext with a custom context to cache which context instance we should use
// Users may optionally pass in a custom context instance to use instead of our ReactReduxContext.
// Memoize the check that determines which context instance we should use.
return propsContext &&
propsContext.Consumer &&
isContextConsumer(<propsContext.Consumer />)
? propsContext
: Context
}, [propsContext, Context])
// Get store in context from upper components
// Returns the current value of the context, that is, the store, when the context of the upper component has recently changed
const contextValue = useContext(ContextToUse)
// store must exist in prop or context
// Determine if store is from a store in props
const didStoreComeFromProps = Boolean(props.store)
// Determine if the store is from a store in the context
const didStoreComeFromContext =
Boolean(contextValue) && Boolean(contextValue.store)
// Remove the store from the context and prepare it for injection into the component after being processed by the selector.Prefer stores in props
const store = props.store || contextValue.store
// Create selector only when store changes
// ChildPropsSelector call method: childPropsSelector(dispatch, options)
const childPropsSelector = useMemo(() => {
// The creation of selector depends on the incoming store
// Recreate this selector whenever the store changes
return createChildSelector(store)
}, [store])
const [subscription, notifyNestedSubs] = useMemo(() => {
if (!shouldHandleStateChanges) return NO_SUBSCRIPTION_ARRAY
// If the store is from props, the subscription instance is no longer passed in, otherwise the subscription instance passed in from the context is used
const subscription = new Subscription(
store,
didStoreComeFromProps ? null : contextValue.subscription
)
const notifyNestedSubs = subscription.notifyNestedSubs.bind(
subscription
)
return [subscription, notifyNestedSubs]
}, [store, didStoreComeFromProps, contextValue])
// contextValue is the store, overwrites the store once and injects the subscription so that the connect ed component can get the subscription in the context
const overriddenContextValue = useMemo(() => {
if (didStoreComeFromProps) {
// If the component is directly subscribed to a store from props, use the context from props directly
return contextValue
}
// Otherwise, put this component's subscription instance into context, so that
// connected descendants won't update until after this component is done
// Interpretation:
// If the store is retrieved from the context, place the subscription in the context.
// To ensure that the subcomponents that are connect ed before the component update is complete will not be updated
return {
...contextValue,
subscription
}
}, [didStoreComeFromProps, contextValue, subscription])
// Built-in reducer to update components, which is used in the checkForUpdates function as the core of the update mechanism
const [
[previousStateUpdateResult],
forceComponentUpdateDispatch
] = useReducer(storeStateUpdatesReducer, EMPTY_ARRAY, initStateUpdates)
if (previousStateUpdateResult && previousStateUpdateResult.error) {
throw previousStateUpdateResult.error
}
// Set up refs to coordinate values between the subscription effect and the render logic
/*
* Official explanation:
* useRef Returns a mutable ref object whose.current property is initialized as an incoming parameter (initialValue).
* The returned ref object remains unchanged throughout the life cycle of the component.
*
* ref Not only for DOM, the current property of useRef() can be used to hold values, similar to the instance property of a class
*
* */
const lastChildProps = useRef() // props of components, including, store, dispatch from parent
const lastWrapperProps = useRef(wrapperProps) // The component itself comes from the props of the parent component
const childPropsFromStoreUpdate = useRef() // Mark if props from store have been updated
const renderIsScheduled = useRef(false) // Mark when to update
/*
* actualChildProps Is the props that are really going to be injected into the component
* */
const actualChildProps = usePureOnlyMemo(() => {
// Tricky logic here:
// - This render may have been triggered by a Redux store update that produced new child props
// - However, we may have gotten new wrapper props after that
// If we have new child props, and the same wrapper props, we know we should use the new child props as-is.
// But, if we have new wrapper props, those might change the child props, so we have to recalculate things.
// So, we'll use the child props from store update only if the wrapper props are the same as last time.
/*
* Interpretation:
* This rendering will be triggered when an update to the store generates a new props, however, we may receive a new props from the parent component after that, if there is a new props,
* And props from parent components are unchanged, so we should update them based on the new child props.However, updates to props from parent components can also result in changes to the overall props and have to be recalculated.
* So only update if the new props change and the props from the parent component agree with the last one (the judgement in the code below is true)
*
* That is, relay only on props updates caused by store changes
* */
if (
childPropsFromStoreUpdate.current &&
wrapperProps === lastWrapperProps.current
) {
return childPropsFromStoreUpdate.current
}
return childPropsSelector(store.getState(), wrapperProps)
}, [store, previousStateUpdateResult, wrapperProps])
// We need this to execute synchronously every time we re-render. However, React warns
// about useLayoutEffect in SSR, so we try to detect environment and fall back to
// just useEffect instead to avoid the warning, since neither will run anyway.
/*
* We need to synchronize this effect every time we re-render.But react will drop a warning for useLayoutEffect in the case of SSR.
* So the end result of useIsomorphic LayoutEffect is a useEffect or useLayoutEffect judged by the environment.Use useEffect when rendering on the server side.
* Since useEffect will wait until js takes over the page in this case, it will not warning
* */
/*
* Overall, there are two useIsomorphic LayoutEffects above and below, but the difference is when they are executed.
*
* The first one does not pass in an array of dependencies, so effect s execute each time they are re-rendered, responsible for each re-rendered
* When checking for changes in data from store s, listeners are notified to update
*
* The second depends on store, subscription, child PropsSelector.So during these three changes, execute the effect.
* Its internal effects do something different from the first one, defining the update function checkForUpdates, subscribing to the update function, so that when the first effect responds to store updates,
* Update functions can be executed as listener s to update the page
*
* */
useIsomorphicLayoutEffect(() => {
lastWrapperProps.current = wrapperProps // Get the props of the component itself
lastChildProps.current = actualChildProps // Get props injected into the component
renderIsScheduled.current = false // Indicates that the rendering phase has passed
// If the render was from a store update, clear out that reference and cascade the subscriber update
// If the props from the store are updated, notify listeners to execute, that is, execute the previously subscribed this.handleChangeWrapper (in the Subscription class).
// handleChangeWrapper calls onStateChange, which is the function checkForUpdates assigned below that updates the page
if (childPropsFromStoreUpdate.current) {
childPropsFromStoreUpdate.current = null
notifyNestedSubs()
}
})
// Our re-subscribe logic only runs when the store/subscription setup changes
// Resubscription is only executed when subscription changes within the store.These two changes mean re-subscribing, since guaranteeing the latest data is delivered, previous subscriptions are no longer meaningful
useIsomorphicLayoutEffect(() => {
// If there is no subscription, return directly, shouldHandleStateChanges defaults to true, so the default will continue
if (!shouldHandleStateChanges) return
// Capture values for checking if and when this component unmounts
// When a component is uninstalled, use a closure to declare whether two variable markers have been unsubscribed and the error object
let didUnsubscribe = false
let lastThrownError = null
// When the store or subscription changes, the callback is re-executed, enabling re-subscription
const checkForUpdates = () => {
if (didUnsubscribe) {
// If you cancel your subscription, do nothing
return
}
// Get the latest state
const latestStoreState = store.getState()
let newChildProps, error
try {
// Use selector to get the latest props
newChildProps = childPropsSelector(
latestStoreState,
lastWrapperProps.current
)
} catch (e) {
error = e
lastThrownError = e
}
if (!error) {
lastThrownError = null
}
// If props do not change, just notify listeners of updates
if (newChildProps === lastChildProps.current) {
/*
* In a browser environment, useLayoutEffect is executed after the DOM changes and before drawing.
* Since the useIsomorphicLayoutEffect above executes at this time setting renderIsScheduled.current to false,
* So go inside and make sure you trigger the update at the right time
*
* */
if (!renderIsScheduled.current) {
notifyNestedSubs()
}
} else {
/*
* If props change, cache the new props and set childPropsFromStoreUpdate.current to the new props to facilitate the first
* useIsomorphicLayoutEffect When executed, it is recognized that props are indeed updated
* */
lastChildProps.current = newChildProps
childPropsFromStoreUpdate.current = newChildProps
renderIsScheduled.current = true
// When dispatch has built-in action s, the ConnectFunction component is updated to update the component
forceComponentUpdateDispatch({
type: 'STORE_UPDATED',
payload: {
latestStoreState,
error
}
})
}
}
// The role of onStateChange is also listener.In the provider, assign a value to update listeners.Assign checkForUpdates to ConnectFunction
// What checkForUpdates does is update the ConnectFunction itself, which corresponds to a listener, depending on the props
subscription.onStateChange = checkForUpdates
subscription.trySubscribe()
// Run once after the first rendering to synchronize data from the store
checkForUpdates()
// Returns a function to unsubscribe from when a component is uninstalled
const unsubscribeWrapper = () => {
didUnsubscribe = true
subscription.tryUnsubscribe()
if (lastThrownError) {
throw lastThrownError
}
}
return unsubscribeWrapper
}, [store, subscription, childPropsSelector])
// Inject the props of the component into the real component we passed in
const renderedWrappedComponent = useMemo(
() => <WrappedComponent {...actualChildProps} ref={forwardedRef} />,
[forwardedRef, WrappedComponent, actualChildProps]
)
const renderedChild = useMemo(() => {
if (shouldHandleStateChanges) {
// If this component is subscribed to store updates, we need to pass its own
// subscription instance down to our descendants. That means rendering the same
// Context instance, and putting a different value into the context.
/*
* Interpretation:
If this component subscribes to updates from store s, it needs to download instances it subscribes to, meaning it and it
All descendant components render the same Context instance, but may put different values into the context
Layer one more Provider and place the overridden context in value.
What does this mean?That is, there is a connected component and a nested connected component.
Ensure that the two subscription s obtained from the context are the same, and that they may both add new values to the context.
I added one, and so did my subcomponents.The final context is the integration of the value s of all components, and subscription s are always the same
* */
return (
<ContextToUse.Provider value={overriddenContextValue}>
{renderedWrappedComponent}
</ContextToUse.Provider>
)
}
// Depending on the received context, the incoming component, and the change in the value of the context, it determines whether or not to re-render
return renderedWrappedComponent
}, [ContextToUse, renderedWrappedComponent, overriddenContextValue])
return renderedChild
}
// Rendering logic based on pure
const Connect = pure ? React.memo(ConnectFunction) : ConnectFunction
// Add Component Name
Connect.WrappedComponent = WrappedComponent
Connect.displayName = displayName
// If forwardRef is true, ref is injected into the Connect component for easy access to DOM instances of the component
if (forwardRef) {
const forwarded = React.forwardRef(function forwardConnectRef(
props,
ref
) {
return <Connect {...props} forwardedRef={ref} />
})
forwarded.displayName = displayName
forwarded.WrappedComponent = WrappedComponent
return hoistStatics(forwarded, WrappedComponent)
}
// Static methods to preserve components
return hoistStatics(Connect, WrappedComponent)
}
}

Having looked at the source code, let's summarize the update mechanism for the connect ed component in React-Redux as a whole:Three of these elements are essential:

Depending on who changes (store)

Update function (checkForUpdates)

Subscription linking store s to update functions

Within the connectAdvanced function, get the store from the context, get the subscription instance (possibly from the context or a new creation), and create the update function checkForUpdates.Subscribe or resubscribe when a component is initialized, or when a store, Subscription instance, or selector changes.Every time a component is updated, check to see if the store has changed and notify the update if it has changed.In fact, checkForUpdates is executed, essentially calling the built-in reducer update component.Each update causes the selector to recalculate, so the component always gets the latest props.So, the bottom level of the update mechanismThis is achieved through ConneAdvanced's built-in Reeducer.

summary

So far, the source code for React-Redux has been interpreted around the common functions.Back to the first three questions in the article:

How Provider puts store s into context s

How to inject a state and dispatch (or a function that calls dispatch) from the store into the props of a component

We all know that in Redux, store changes and UI updates can be achieved by subscribing to a function of an update page through store.subscribe(), and how React-Redux does that

store changes, and connect ed components update

Now it should be clear that these three questions correspond to the three core concepts of React-Redux:

Provider injects data from top level

Selector generates props for components

Update mechanism for React-Redux

They work together, which is how React-Redux works: Provider puts data into context, connect s removes store from context, gets mapStateToProps, mapDispatchToProps, and generates Selector as props injection component using selectorFactory.Subscribe to store changes and get the latest props for each update component.

The best way to read the source code is to identify the problem and read it purposefully.At the beginning, I was hard-headed. The more I looked, the more I lost sight of it. I learned a lot after I changed the way I thought you were.

Welcome to my public number: one front end at a time, sharing what I understand on an irregular basis