React-redux is a library for people who are familiar with React. React updates redux stores, listens for store changes, and notifies react components of updates. React allows the state to be managed externally (it facilitates centralized management of models, takes advantage of the Redux single-node data flow architecture, which is easy to predict and maintain, and greatly facilitates communication between components at any level).

The react-redux version comes from v8.0.0-beta.2 as of 2022.02.28. The react-redux version comes from v8.0.0-beta.2 as of 2022.02.28.

React-redux 8 includes but is not limited to these changes compared to version 7:

  • All refactoring in typescript

  • The original Subscription class was refactored by createSubscription and it is helpful to replace classes with closure functions, as discussed in that part of the code.

  • Use React18’s useSyncExternalStore instead of its own subscription updates (originally the internal useReducer). UseSyncExternalStore and its predecessor useMutableSource solved the tearing problem in Concurrent mode and made the library itself simpler, UseSyncExternalStore does not care about selector compared to its predecessor useMutableSource. Not react-redux) immutable mental burden.


The following section is not directly related to source code parsing, but it is instructive to read and understand why this article is written. If you want to see the react-redux source code parsing section, go to the react-redux source code parsing section

Stage 1: Since it is “second reading”, what about “first reading”?

I don’t know if you’ve ever seen comments like “Redux is bad, Mobx smells better…

Inquisitive people (like me) saw this and asked more questions:

  1. Is redux bad or React-Redux bad?
  2. What’s wrong with it?
  3. Can it be avoided?

If you ask these questions, you will probably get only a few words. How does react-redux relate to React? There are quite a few source code parsing articles on this issue. I have read a very detailed one, but unfortunately it is an older version that still uses class Component, so I decided to look at the source code myself. The simple summary is that there is a Subscription in the Provider, there is a Subscription in the connect higher-order component, and there are hooks that update themselves: UseReducer dispatches are registered as Subscription listeners, and notify calls each listener. Notify is registered with SUBSCRIBE from REdux so that redux’s state updates are notified to all connect components, Of course, each connect has a checkForUpdates method that checks whether it needs to be updated to avoid unnecessary updates, but the details will not be left out.

Anyway, I only read the overall logic at that time, but it can answer my question above:

  1. React-redux does have the potential to perform poorly. As for REdux, each dispatch requires state to go to each reducer, and there is additional replication overhead to create data immutable. However, frequent changes to the MUTABLE library can also cause V8’s object memory to change from sequential to lexicographical, query speed to slow, and inline caching to become highly hyperstate, so immutable closes the gap a bit. However, for a clear and reliable data flow architecture, this level of overhead is worth it or even negligible in most scenarios.

  2. What’s wrong with React-Redux performance? Because each connect will be notified once whether it needs to be updated or not, the selector defined by the developer will be called once or more, and if the selector logic is expensive, it will still consume performance.

  3. So does react-Redux have to be bad? Not necessarily. According to the above analysis, if your selector logic is simple (or complex derived calculations are put into the Reducer of Redux, but this may not be conducive to building a reasonable model), connect is not used much. Then performance won’t be stretched too much by fine-grained updates like Mobx. That is, there is no perceived performance problem at all when the business calculation in the selector is not complex and there are not many components that use global state management. What if the business computation inside the selector is complicated? Can you avoid it altogether? Sure, you can use the library reselect, which caches the result of the selector and recalculates the derived data only when the original data changes.

This is my “first read”. I read the source code with a purpose and a problem. Now that the problem has been solved, it is supposed to be over.

Water blowing stage 2: Why “reread”?

Zustand is the React state management library on Github.

Zustand is a very snazzy lattion-based state management library based on a simplified Flux architecture and Star’s fastest growing React state management library in 2021. Redux + React-Redux

Its Github introduction begins like this

It is a small, fast, scalable state management solution that uses a simplified Flux architecture. There are hooks based apis that are very comfortable and personal to use. Don’t overlook it just because it’s cute. It has a lot of claws and spends a lot of time dealing with common pitfalls like the dreaded Zombie Child problem, React Concurrency, And the problem of context loss between multiple renders using portals. It’s probably the only state manager in the React space that can handle all of these issues correctly.

Zombie Child Problem. When I clicked on the Zombie Child Problem, it was the official documentation for React-Redux, so let’s take a look at what the problem was and how react-Redux solved it. You can click on the link if you want to see the original article.

“Stale Props” and “Zombie Children”

Since v7.1.0, react-Redux has been able to use the hooks API, and it is officially recommended that hooks be used as the default in the component. But there are some edge situations that can occur, and this document makes us aware of them.

One of the hardest things about the react-Redux implementation is that if your mapStateToProps (state, ownProps) is used like this, it will be passed the “latest” props each time. Up until version 4, repeated bugs were reported in edge scenarios, such as an error in mapStateToProps when a list item was deleted.

Starting with version 5, React-Redux attempted to ensure the consistency of ownProps. In version 7, there is a custom Subscription class inside each connect(), which forms a nested structure when there is a connect inside it. This ensures that connect components lower down the tree will receive updates from the Store only after their nearest ancestor connect component has been updated. However, this implementation relies on each connect() instance overriding part of the internal React Context (the Subscription part), using its own Subscription instance for nesting. Then use the new React the Context (< ReactReduxContext. The Provider >) rendering child nodes.

There is no way to render a context.Provider if hooks are used, which means it cannot have a nested structure around subscriptions. Because of this, the “stale props” and “zombie child” problems can reoccur in “hooks instead of connect” applications.

Specifically, “stale props” would appear in this scenario:

  • The selector function evaluates the data based on the props of the component
  • The parent component rerenders and passes the component new props
  • But this component will perform the selector before the props update. Because the child components from store is registered in useLayoutEffect/useEffect update, so the child components registered before the parent component, redux trigger a subscription will trigger the subcomponents update method)

The old props and the latest Store state are likely to be incorrect, or even cause an error.

“Zombie Child “refers to the following scenarios:

  • Mounted Connect component. The child component is registered to the store earlier than its parent
  • An action Dispatch deletes data from a store, such as an item from a Todo list
  • The parent component loses an Item child when rendering
  • However, because the child component is subscribed first, its subscription precedes the parent component. When it evaluates a value based on the store and props, part of the data may not exist, and an error will be reported if the evaluation logic is not careful.

UseSelector () tries to solve this problem by catching any errors in the selector calculation caused by store updates. When an error occurs, the component forces an update and the selector executes again. This requires that the selector be a pure function and you don’t logically rely on the selector to throw an error.

If you prefer to do it yourself, here’s a potentially useful tip to help you avoid these problems when using useSelector()

  • Don’t rely on props in a selector evaluation
  • You need to guard against writing a selector if you have to rely on the props calculation and if the props might change in the future, or if the dependent store data might be deleted. Don’t directly likestate.todos[props.id].nameRead the value this way, but read it firststate.todos[props.id]Verify that it exists before readingtodo.namebecauseconnectAdded the necessary context providerSubscription, it will defer executing subsubscriptions until the Connected component re-rasterizes. If connected components are used in the component treeuseSelectorThe parent connect component has the same store updates as the hooks component. The parent connect component updates the child hooks component only after the parent connect component updates, and the connect component updates the child nodes, the deleted nodes are uninstalled in this parent component update: as described abovestate.todos[props.id].name, indicating that the hooks component is iterated by ids from above. Subsequent updates to the child hooks component from store will not be deleted.)

This gives you an idea of how the Stale Props and Zombie Children issues are generated and how the React-redux works by nesting updates from child connect to parent connect, Instead of iterating through all the connect updates, each redux update is triggered by the parent first, and then by the child. However, it seems that hooks make it less than perfect, and the details of the design are not addressed. This confusion and lack of clarity is why I’m going to reread the react-Redux source code.

React-redux source code

The React-Redux version comes from v8.0.0-beta.2 as of 2022.02.28

During the reading of the source code, I wrote some Chinese comments in fork’s React-Redux project, which was put into the react-Redux-with-comment repository as a new project. If you need to read the source code, you can check it out. The version is 8.0.0-beta-2

I want to talk a little bit about the general abstract design before I go into the details, so that you can read the details with the blueprint in mind, otherwise it’s hard to just look at the details and see how they connect and how they work together to accomplish the function.

The React-Redux Provider and connect provide their own context across the subtree, which all of their children can access and give them their update methods. Finally, the collection order of root <– parent <– child is formed. The update methods collected by the root are triggered by redux, and the update methods collected by the parent are updated after the parent, thus ensuring the order in which the child nodes are updated after the parent node is updated by Redux.

Simple macro design as shown above, the first look can not understand very deep, but it does not matter, look at the source code and source code analysis after a few times to look back here will have new harvest.

Start with the project construction entry

You can see that its UMD packages are built by rollup (build:umd, build:umd:min) and esM and CommonJS packages are compiled and output by Babel (Build: CommonJS, build:es). Build :es: “Babel SRC –extensions \”.js,.ts,.tsx\” –out-dir es” Js,.ts,.tsx files in SRC directory and output them to es directory using Babel. Each file still keeps the import introduced and just compile the content, which will eventually be built together in the developer’s project).

So what does babelrc.js do

You can see that @babel/preset-typescript in Babel’s presets is responsible for compiling TS to JS, and @babel/preset-env is responsible for compiling ECMA’s latest syntax to ES5. The API requires additional plug-ins). As for Babel plugins, @babel/transform-modules-commonjs solves the problem of repeating helper functions for Babel by introducing the corejs API Polyfill as needed. UseESModules configuration is used to decide whether to useESM or CommonJS helper, but this configuration has been deprecated in official documentation since 7.13.0, which can be directly determined by package.json exports. Other plugins are syntactic compilations, such as private methods, private properties, static properties, JSX, decorators, etc. * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * It determines whether the esM library or commonJS library is ultimately output.

According to the module field of package.json (regarding the priority of the main, module, and browser fields), the final entry is es/index.js in the root directory, and since it is output by Babel from the source directory, So the source entry is SRC /index.ts.

Start with a common API

As can be seen from the above figure, only batch and exports.ts files are exported, so we go to exports.ts

Provider, Connect, useSelector and useDispatch occupy most of the scenarios we use, so we start with these four apis.

Provider

The Provider from SRC/components/Provider. TSX.

It’s a React component, it doesn’t have any view content, it shows children, it just adds a Context Provider to children, that’s why it’s called a Provider. So what exactly does this component want to pass down.

const contextValue = useMemo(() = > {
  const subscription = createSubscription(store);
  return {
    store,
    subscription,
    getServerState: serverState ? () = > serverState : undefined}; }, [store, serverState]);Copy the code

You can see that pass-through is an object made up of Store, Subscription, and getServerState. Here are the three properties of an object.

Store is the Store of Redux, which is passed to the Provider component by the developer through Store Prop.

Subscription was created by the createSubscription object factory, which generates the Subscription object, which is key to subsequent nested collection subscriptions. Details about createSubscription are described later.

GetServerState is new in version 8.0.0 and is used in SSR to get a server-side status snapshot when initial “water” hydrate is injected to ensure consistency between the two states. It is entirely in the developer’s hands, as long as the state snapshot is passed to the Provider component through the serverState prop. For those not familiar with SSR/hydrate concepts, take a look at Dan Abramov’s discussions, which are not focused on SSR but begin with an introduction to the concepts, and Dan’s articles are always vivid and accessible.

The next thing the Provider component does is:

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

useIsomorphicLayoutEffect(() = > {
  const { subscription } = contextValue;
  subscription.onStateChange = subscription.notifyNestedSubs;
  subscription.trySubscribe();

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

const Context = context || ReactReduxContext;

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

Gets the latest state once and calls it previousState, which is not updated as long as the Store singleton does not change. Redux singletons don’t change much in a typical project.

UseIsomorphicLayoutEffect is just a facade, it can be seen from the isomorphic naming also is associated with homogeneous. Internally, it uses useEffect in the server environment and use elayouteffect in the browser environment

The code is simple:

import { useEffect, useLayoutEffect } from "react";

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

// Matches logic in React's `shared/ExecutionEnvironment` file
export constcanUseDOM = !! (typeof window! = ="undefined" &&
  typeof window.document ! = ="undefined" &&
  typeof window.document.createElement ! = ="undefined"
);

export const useIsomorphicLayoutEffect = canUseDOM
  ? useLayoutEffect
  : useEffect;
Copy the code

But the reason for this is not simple: first, using useLayoutEffect on the server throws a warning, and to circumvent it, useEffect on the server instead. Second, why must in useLayoutEffect/useEffect do? Since a store update can occur between the Render phase and the side effects phase, if done at render time, the update may be missed, you must ensure that the store Subscription callback has a selector from the latest update. Also ensure that the creation of Store Subscription must be synchronous, otherwise a store update may occur before the subscription (if the subscription is asynchronous) has been created, resulting in an inconsistent state.

If the reason is not clear, it will be clear with the following examples.

The Provider in useIsomorphicLayoutEffect did such a thing:

subscription.trySubscribe();

if(previousState ! == store.getState()) { subscription.notifyNestedSubs(); }Copy the code

Collect the subscription and see if the latest state is consistent with the previous state in Render, notifying the update if it is not. If this section is not put in useLayoutEffect/useEffect, but in the render, so now just subscribed to itself, its child components did not subscribe to, if the child components in the rendering process updated redux store, then the child components are missed the update notification. At the same time, the react useLayoutEffect/useEffect is invoked from bottom to top, child component to call first, after the parent component invocation. Because here is the react – the root node of the story, it useLayoutEffect/useEffect can be called at the last, you can ensure the subcomponents the registered subscriptions are registered, at the same time also can ensure child components rendering can occur in the process of updating all have happened. So read state one last time to compare whether to notify them of updates. This is why choose useLayoutEffect/useEffect.

Next we complete look at the Provider in useIsomorphicLayoutEffect do

useIsomorphicLayoutEffect(() = > {
  const { subscription } = contextValue;
  subscription.onStateChange = subscription.notifyNestedSubs;
  subscription.trySubscribe();

  if(previousState ! == store.getState()) { subscription.notifyNestedSubs(); }return () = > {
    subscription.tryUnsubscribe();
    subscription.onStateChange = undefined;
  };
}, [contextValue, previousState]);
Copy the code

First is to set up the subscription onStateChange (initial method is empty, it need to inject implementation), it will call when triggered update, it here want to call in the future is the subscription notifyNestedSubs, Subscription. NotifyNestedSubs will trigger the subscription to collect all of the children to subscribe to. This means that the update callback is not directly related to the “update”, but rather triggers the update methods of the child nodes.

Then call the subscription. TrySubscribe (), it will own the onStateChange to parent subscription or story to subscribe, triggered by them in the future the onStateChange

In the end it will determine whether before the state and the latest, if inconsistent will call subscription. NotifyNestedSubs (), it will trigger the subscription to collect all the sons of subscriptions to update them.

Returned to the cancellation of related functions, it will cancel the subscription of the parent, will the subscription. The onStateChange method is empty again. This function is called when the component is unloaded or re-render (a feature of react useEffect only).

There are many aspects of the Provider that involve subscription. The methods used for subscription are described in general terms, and details about it will be covered in the Subscription section later.

The complete Provider source code and comments are as follows:

function Provider<A extends Action = AnyAction> ({ store, context, children, serverState, }: ProviderProps) {
  // Generates an object for context passthrough, including functions that can be used for redux store, Subscription instance, SSR
  const contextValue = useMemo(() = > {
    const subscription = createSubscription(store);
    return {
      store,
      subscription,
      getServerState: serverState ? () = > serverState : undefined}; }, [store, serverState]);// Get the current redux state once. It is called previousState because the state may be changed by subsequent child rendering
  const previousState = useMemo(() = > store.getState(), [store]);

  // In useLayoutEffect or useEffect
  useIsomorphicLayoutEffect(() = > {
    const { subscription } = contextValue;
    // Set the subscription onStateChange method
    subscription.onStateChange = subscription.notifyNestedSubs;
    // Subscribe the subscription update callback to the parent, in this case to Redux
    subscription.trySubscribe();

    // Determine if state changes after rendering, and if it does, trigger all subsubscriptions
    if(previousState ! == store.getState()) { subscription.notifyNestedSubs(); }// Deregister operation during component uninstallation
    return () = > {
      subscription.tryUnsubscribe();
      subscription.onStateChange = undefined;
    };
  }, [contextValue, previousState]);

  const Context = context || ReactReduxContext;

  // The final Provider component just passes the contextValue transparently, and the component UI uses children entirely
  return <Context.Provider value={contextValue}>{children}</Context.Provider>;
}
Copy the code

The Provider component simply passes the contextValue transparently, allowing child components to take the Redux store, subscription instance, and server-side state function.

The Subscription/createSubscription Subscription factory function

I’ll cover the highly visible subscription part of the Provider, which is key to react-Redux’s ability to nest collection subscriptions. The title of this section is incorrectly called Subscription. Prior to version 8.0.0, React-Redux was implemented using the Subscription class, You can use new Subscription() to create a Subscription instance. However, after 8.0.0, it has changed to createSubscription functions that createSubscription objects and internally replace the original attributes with closures.

One advantage of using a function instead of a class is that you don’t need to worry about the direction of this. The method returned by a function always modiates the internal closure, and there is no problem with the direction of this changing when class methods are assigned to other variables, reducing the mental burden of development. Closures are also more private, increasing variable security. It is also more in line with the development paradigm to implement functions in a library that supports hooks.

Let’s take a look at createSubscription and its scription of the code. Each of its responsibilities is commented out

Note: The “subscription callback” used below refers specifically to the update method of a component that is triggered after a redux status update. Component update methods are collected by parent subscriptions and are subscription-to-publish.

function createSubscription(store: any, parentSub? : Subscription) {
  // Indicate whether you are subscribed
  let unsubscribe: VoidFunc | undefined;
  // The collector that collects subscriptions
  let listeners: ListenerCollection = nullListeners;

  // Collect subscriptions
  function addNestedSub(listener: () => void) {}

  // Notification subscription
  function notifyNestedSubs() {}

  // Own subscription callback
  function handleChangeWrapper() {}

  // Determine if you are subscribed
  function isSubscribed() {}

  // make yourself subscribed by the parent
  function trySubscribe() {}

  // Unsubscribe your subscription from the parent
  function tryUnsubscribe() {}

  const subscription: Subscription = {
    addNestedSub,
    notifyNestedSubs,
    handleChangeWrapper,
    isSubscribed,
    trySubscribe,
    tryUnsubscribe,
    getListeners: () = > listeners,
  };

  return subscription;
}
Copy the code

The createSubscription function is an object factory that defines variables and methods and returns an object subscription with these methods

First look at the handleChangeWrapper, which by its name is just a shell

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

It actually calls the onStateChange method inside. The reason for this is that when the subscription callback is collected by the parent, its own callback may not have been determined, so a shell is defined to be collected. The internal callback method is reset when determined, but the shell reference is unchanged, so the callback can still be triggered in the future. . That is why in the Provider ts source, before you collect subscription with my subscription. The onStateChange = subscription. NotifyNestedSubs.

Then watch trySubscribe

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

    listeners = createListenerCollection();
  }
}
Copy the code

It allows the parent subscription to collect its own subscription callbacks. First, it will decide that if unsubscribe signals that it’s already subscribed, then it doesn’t do anything. Second, it checks if the second argument, parentSub, is empty when the subscription was created. If parentSub is present, it means it has parent Subscription, and then it registers its subscription callback with the addNestedSub method. Otherwise, they think they’re at the top level, so they register with redux Store.

And by extension we need to look at what is the addNestedSub method

function addNestedSub(listener: () => void) {
  trySubscribe();
  return listeners.subscribe(listener);
}
Copy the code

AddNestedSub is a very clever use of recursion, and it calls trySubscribe. So they do this: when the lowest subscription initiates a trySubscribe and wants to be collected by the parent, it first triggers the parent’s trySubscribe and continues recursively until the root subscription, If we imagine such a hierarchical structure spanning tree (actually subscription. TrySubscribe does occur in the component tree), then the equivalent of from the root node to leaf node, in turn, will be the parent to collect subscription. Since this is initiated by the leaf node, the subscription callbacks of other nodes have not yet been set, so the handleChangeWrapper callback shell is designed to register only this callback shell, which can be triggered by the shell after the non-leaf node sets the callback.

At the end of the process, subscription callbacks to handleChangeWrapper from the root node to the leaf node are being collected by the parent, return listeners. Subsubscription callbacks are collected into collector listeners (future updates will trigger the associated handleChangeWrapper, which will indirectly call all listeners collected).

So each Subscription addNestedSub does two things: 1. Let your subscription callback be collected by the parent first; 2. Collect the subscription callback for subsubscription.

Back to trySubscribe with addNestedSub, it wants its subscription callback to be collected by the parent, so when it is passed parent subscription, its addNestedSub is called, This causes each layer of the root subscription to be collected by the parent as a callback, and each subscription then collects its child subscriptions in a nested manner, making it possible for the child to be updated after the parent is updated. Also, because of the unsubscribe lock, if a parent subscription’s trySubscribe is called, the “nested registration” is not triggered repeatedly.

We’ve looked at what happens to nested registrations, but now we’re looking at what happens to the actual operations of registrations, and how the data structure of registrations is designed.

function createListenerCollection() {
  const batch = getBatch();
  // A collection of listeners, which is a bidirectional list
  let first: Listener | null = null;
  let last: Listener | null = null;

  return {
    clear() {
      first = null;
      last = null;
    },

    // Triggers a callback for all nodes of the linked list
    notify() {
      batch(() = > {
        let listener = first;
        while(listener) { listener.callback(); listener = listener.next; }}); },// Return all nodes as an array
    get() {
      let listeners: Listener[] = [];
      let listener = first;
      while (listener) {
        listeners.push(listener);
        listener = listener.next;
      }
      return listeners;
    },

    // Add a node to the end of the list and return an undo function to delete the node
    subscribe(callback: () => void) {
      let isSubscribed = true;

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

      if (listener.prev) {
        listener.prev.next = listener;
      } else {
        first = listener;
      }

      return function unsubscribe() {
        if(! isSubscribed || first ===null) return;
        isSubscribed = false;

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

Listeners are created by the createListenerCollection. Listeners are composed of clear, notify, get, and subscribe.

Listeners are collected by listeners (subscription callbacks) and maintained as a two-way list, starting with first and ending with last.

Clear method is as follows:

clear() {
  first = null
  last = null
}
Copy the code

Use to clear the linked list of collections

The notify method is as follows:

notify() {
  batch(() = > {
    let listener = first
    while (listener) {
      listener.callback()
      listener = listener.next
    }
  })
}
Copy the code

Batch is used to iterate through the list of nodes and call the function that is used to call the input parameter. Details of the function can be derived from many React principles (such as batch update, fiber, etc.).

The get method is as follows:

get() {
  let listeners: Listener[] = []
  let listener = first
  while (listener) {
    listeners.push(listener)
    listener = listener.next
  }
  return listeners
}
Copy the code

Used to convert the linked list node to an array and return

Subscribe as follows:

subscribe(callback: () => void) {
  let isSubscribed = true

  // Create a linked list node
  let listener: Listener = (last = {
    callback,
    next: null.prev: last,
  })

  // If the list already has nodes
  if (listener.prev) {
    listener.prev.next = listener
  } else {
    // If the list has no nodes, it is the first node
    first = listener
  }

  // Unsubscribe is an operation that removes a specified node from a bidirectional list
  return function unsubscribe() {
    // Prevent meaningless execution
    if(! isSubscribed || first ===null) return
    isSubscribed = false

    // If the added node already has a subsequent node
    if (listener.next) {
      // Next's prev should be the node's prev
      listener.next.prev = listener.prev
    } else {
      // If there is no prev, this node is the last
      last = listener.prev
    }
    // If there is a former node prev
    if (listener.prev) {
      // Next of prev should be next of this node
      listener.prev.next = listener.next
    } else {
      // If not, the node is the first, and its next is given to first
      first = listener.next
    }
  }
}
Copy the code

Function used to add a subscription to the Listeners list and return an unsubscribe function.

So each subscription collection actually maintains a bidirectional linked list.

The last things subscription needs to say are notifyNestedSubs and tryUnsubscribe

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

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

NotifyNestedSubs calls Listeners. Notify. Based on the analysis of listeners above, all subscriptions are called

TryUnsubscribe is the unsubscribe operation. This. Unsubscribe is injected into the trySubscribe method. It is the return value of addNestedSub or redux Subscribe. The listeners on this. Unsubscribe () are clear unsubscribe and clear listeners.

This completes the subscription analysis, which is used to collect subscriptions in a nested call so that the subscription callback of the child node is updated after the parent update. Those unfamiliar with react-Redux may wonder if only Provider components use subscription. Where is the nested call? Where does the collection subsubscription come from? Don’t worry, the connect higher-order function, which also uses subscription, is nested here.

Connect High-level components

TSX replaces connectAdvanced.js with connect.tsx in 8.0.0. Essentially, these are multi-layer higher-order functions, but the connect.tsx structure is more intuitive after refactoring.

We all know that when we use Connect: Connect (mapStateToProps, mapDispatchToProps, mergeProps, connectOptions)(Component), So its entry should be a function that takes parameters like mapStateToProps, mapDispatchToProps, etc., and returns a higher-order function that takes parameters like Component, which ultimately returns jsX.Element.

A quick look at the structure of CONNECT looks like this:

function connect(mapStateToProps, mapDispatchToProps, mergeProps, { pure, areStatesEqual, areOwnPropsEqual, areStatePropsEqual, areMergedPropsEqual, forwardRef, context, }) {
  const wrapWithConnect = (WrappedComponent) = > {
    return <WrappedComponent />;
  };
  return wrapWithConnect;
}
Copy the code

If I break down what Connect does, I think there are several pieces: subscribe to your updates to the parent, select data from the Redux Store, determine if you need updates, and more

Connect the selector

const initMapStateToProps = match(
  mapStateToProps,
  // @ts-ignore
  defaultMapStateToPropsFactories,
  "mapStateToProps")! ;const initMapDispatchToProps = match(
  mapDispatchToProps,
  // @ts-ignore
  defaultMapDispatchToPropsFactories,
  "mapDispatchToProps")! ;const initMergeProps = match(
  mergeProps,
  // @ts-ignore
  defaultMergePropsFactories,
  "mergeProps")! ;const selectorFactoryOptions: SelectorFactoryOptions<any.any.any.any> = {
  pure,
  shouldHandleStateChanges,
  displayName,
  wrappedComponentName,
  WrappedComponent,
  initMapStateToProps,
  initMapDispatchToProps,
  // @ts-ignore
  initMergeProps,
  areStatesEqual,
  areStatePropsEqual,
  areOwnPropsEqual,
  areMergedPropsEqual,
};

const childPropsSelector = useMemo(() = > {
  // The child props selector needs the store reference as an input.
  // Re-create this selector whenever the store changes.
  return defaultSelectorFactory(store.dispatch, selectorFactoryOptions);
}, [store]);

const actualChildPropsSelector = childPropsSelector(
  store.getState(),
  wrapperProps
);
Copy the code

The match function is the first to be analyzed

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

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

The factories, as an array of factories, are passed in as arG traversal calls. Each factory checks the ARG (mapStateToProps, mapDispatchToProps, mergeProps). Return until the factories[I](arg) has a value, and if it is never truly, an error is reported. Factories, like the chain of responsibility mode, process and return the factory responsibilities that belong to them.

The factories on initMapStateToProps and initMapDispatchToProps are different. DefaultMapStateToPropsFactories, defaultMapDispatchToPropsFactories, defaultMergePropsFactories, let’s see what they are.

// defaultMapStateToPropsFactories

function whenMapStateToPropsIsFunction(mapStateToProps? : MapToProps) {
  return typeof mapStateToProps === "function"
    ? wrapMapToPropsFunc(mapStateToProps, "mapStateToProps")
    : undefined;
}

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

const defaultMapStateToPropsFactories = [
  whenMapStateToPropsIsFunction,
  whenMapStateToPropsIsMissing,
];
Copy the code

Traverse defaultMapStateToPropsFactories is invoked the whenMapStateToPropsIsFunction, whenMapStateToPropsIsMissing these two factories, The first is handled when mapStateToProps is a function, and the second is handled when mapStateToProps is omitted.

Inside wrapMapToPropsFunc function (namely whenMapStateToPropsIsFunction) will be packed mapToProps agent in a function, it did a few things:

  1. Checks whether the called mapToProps function depends on props, which is used by the selectorFactory to determine whether it should be called again if the props changes.
  2. On the first call, ifmapToPropsReturns another function, then processesmapToPropsAnd handles the real mapToProps for subsequent calls to the new function.
  3. On the first call, verify that the result is a flat object to warn the developer that the mapToProps function did not return a valid result.

WrapMapToPropsConstant function (namely whenMapStateToPropsIsMissing) will return an empty object when the default (not return immediately, returns the higher-order functions), has value when expectations that value is a function, dispatch into the function, Returns the return value of this function at the end (again not immediately)

Two other defaultMapDispatchToPropsFactories, defaultMergePropsFactories factory group, responsibility and defaultMapStateToPropsFactories, It’s essentially an ARG that handles different cases

const defaultMapDispatchToPropsFactories = [
  whenMapDispatchToPropsIsFunction,
  whenMapDispatchToPropsIsMissing,
  whenMapDispatchToPropsIsObject,
];

const defaultMergePropsFactories = [
  whenMergePropsIsFunction,
  whenMergePropsIsOmitted,
];
Copy the code

I’m sure you can guess what they’re responsible for from their names, but I won’t go into details.

After match processing, initMapStateToProps, initMapDispatchToProps, and initMergeProps are returned. The purpose of these functions is to return the value of select

const selectorFactoryOptions: SelectorFactoryOptions<any.any.any.any> = {
  pure,
  shouldHandleStateChanges,
  displayName,
  wrappedComponentName,
  WrappedComponent,
  initMapStateToProps,
  initMapDispatchToProps,
  // @ts-ignore
  initMergeProps,
  areStatesEqual,
  areStatePropsEqual,
  areOwnPropsEqual,
  areMergedPropsEqual,
};
Copy the code

They and other attributes make up an object called selectorFactoryOptions

Finally, it goes to the defaultSelectorFactory

const childPropsSelector = useMemo(() = > {
  // The child props selector needs the store reference as an input.
  // Re-create this selector whenever the store changes.
  return defaultSelectorFactory(store.dispatch, selectorFactoryOptions);
}, [store]);
Copy the code

And childPropsSelector is the function that eventually returns the desired value (it’s really the end of higher-order functions).

What did so only need to see defaultSelectorFactory function, it actually called finalPropsSelectorFactory

export default function finalPropsSelectorFactory<
  TStateProps.TOwnProps.TDispatchProps.TMergedProps.State = DefaultRootState> (dispatch: Dispatch
       
        , { initMapStateToProps, initMapDispatchToProps, initMergeProps, ... options }: SelectorFactoryOptions< TStateProps, TOwnProps, TDispatchProps, TMergedProps, State >
       ) {
  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);
  }

  return pureFinalPropsSelectorFactory<
    TStateProps,
    TOwnProps,
    TDispatchProps,
    TMergedProps,
    State
    // @ts-ignore>(mapStateToProps! , mapDispatchToProps, mergeProps, dispatch, options); }Copy the code

MapStateToProps, mapDispatchToProps, and mergeProps are functions that return their final values. More should pay attention to focus on pureFinalPropsSelectorFactory function

export function pureFinalPropsSelectorFactory<
  TStateProps.TOwnProps.TDispatchProps.TMergedProps.State = DefaultRootState> (
  mapStateToProps: MapStateToPropsParam<TStateProps, TOwnProps, State> & {
    dependsOnOwnProps: boolean;
  },
  mapDispatchToProps: MapDispatchToPropsParam<TDispatchProps, TOwnProps> & {
    dependsOnOwnProps: boolean;
  },
  mergeProps: MergeProps<TStateProps, TDispatchProps, TOwnProps, TMergedProps>,
  dispatch: Dispatch,
  {
    areStatesEqual,
    areOwnPropsEqual,
    areStatePropsEqual,
  }: PureSelectorFactoryComparisonOptions<TOwnProps, State>
) {
  let hasRunAtLeastOnce = false;
  let state: State;
  let ownProps: TOwnProps;
  let stateProps: TStateProps;
  let dispatchProps: TDispatchProps;
  let mergedProps: TMergedProps;

  function handleFirstCall(firstState: State, firstOwnProps: TOwnProps) {
    state = firstState;
    ownProps = firstOwnProps;
    // @ts-ignorestateProps = mapStateToProps! (state, ownProps);// @ts-ignoredispatchProps = mapDispatchToProps! (dispatch, ownProps); mergedProps = mergeProps(stateProps, dispatchProps, ownProps); hasRunAtLeastOnce =true;
    return mergedProps;
  }

  function handleNewPropsAndNewState() {
    // @ts-ignorestateProps = mapStateToProps! (state, ownProps);if(mapDispatchToProps! .dependsOnOwnProps)// @ts-ignore
      dispatchProps = mapDispatchToProps(dispatch, ownProps);

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

  function handleNewProps() {
    if(mapStateToProps! .dependsOnOwnProps)// @ts-ignorestateProps = mapStateToProps! (state, ownProps);if (mapDispatchToProps.dependsOnOwnProps)
      // @ts-ignore
      dispatchProps = mapDispatchToProps(dispatch, ownProps);

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

  function handleNewState() {
    const nextStateProps = mapStateToProps(state, ownProps);
    conststatePropsChanged = ! areStatePropsEqual(nextStateProps, stateProps);// @ts-ignore
    stateProps = nextStateProps;

    if (statePropsChanged)
      mergedProps = mergeProps(stateProps, dispatchProps, ownProps);

    return mergedProps;
  }

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

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

Its closure hasRunAtLeastOnce differentiates whether it is called for the first time. The first and subsequent calls are different functions. If it is called for the first time, it uses the handleSubsequentCalls function. Then put them into mergeProps to calculate the final props and set hasRunAtLeastOnce to true to indicate that this is not the first time.

All subsequent calls go to handleSubsequentCalls, whose main purpose is to use cached data if neither the state nor the props have changed. DependsOnOwnProps if both the state and the props are changed, or if only one of them is changed, then call the respective functions (dependsOnOwnProps) to obtain the new value.

The childPropsSelector function, then, is the pureFinalPropsSelector function returned, internally accessing the closure, which holds the persistent value so that in case the component executes multiple times, you can decide whether or not caching is needed to optimize performance.

So we’re done analyzing the selector.

And in general, if you want to implement the simplest selector, you just need to

const selector = (state, ownProps) = > {
  const stateProps = mapStateToProps(reduxState);
  const dispatchProps = mapDispatchToProps(reduxDispatch);
  const actualChildProps = mergeProps(stateProps, dispatchProps, ownProps);
  return actualChildProps;
};
Copy the code

So why is react-Redux so complicated? React can only use the memo when wrapperProps is unchanged. It is difficult to make a more fine-grained separation, such as whether selector depends on props. So the props don’t need to be updated if they change. This requires a large number of nested higher-order functions to store persistent closure intermediate values so that updates can be detected without losing state when the component executes multiple times.

So now we’re ready to talk about something else, and if you’re a little dizzy with a bunch of call stacks, you just have to remember that childPropsSelector is the value that gets returned.

Connect updates to register subscriptions

function ConnectFunction<TOwnProps> (props: InternalConnectProps & TOwnProps) {
  const [propsContext, reactReduxForwardedRef, wrapperProps] = useMemo(() = > {
    const{ reactReduxForwardedRef, ... wrapperProps } = props;return [props.context, reactReduxForwardedRef, wrapperProps];
  }, [props]);

  / /.....................
  / /.....................
}
Copy the code

Firstly, the actual business props and behavior control related props are divided from the props. The so-called business props refers to the props of the parent component of the project that is actually transmitted to the CONNECT component. The behavior control props are business-independent props related to internal registration and subscription, such as forward ref and context. The deconstructed values are cached using useMemo.

const ContextToUse: ReactReduxContextInstance = useMemo(() = > {
  return propsContext &&
    propsContext.Consumer &&
    // @ts-ignore
    isContextConsumer(<propsContext.Consumer />)? propsContext : Context; }, [propsContext, Context]);Copy the code

This step defines the context. Remember the context in the Provider component, connect, you can get it through the context. If the user passes in a custom context via props, the custom context is preferred, Otherwise use the “think of it as global” React. CreateContext (also used by providers and other connect, useSelector, etc.)

conststore: Store = didStoreComeFromProps ? props.store! : contextValue! .store;const getServerState = didStoreComeFromContext
  ? contextValue.getServerState
  : store.getState;

const childPropsSelector = useMemo(() = > {
  // The child props selector needs the store reference as an input.
  // Re-create this selector whenever the store changes.
  return defaultSelectorFactory(store.dispatch, selectorFactoryOptions);
}, [store]);
Copy the code

We then get the Store (which can come from props or context) and the server render state (if any). And then we create a selector function that returns the selected value, and we’ve seen the details of the selector above.

The highlights of the subscription appear below!

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

  const subscription = createSubscription(
    store,
    didStoreComeFromProps ? undefined: contextValue! .subscription );const notifyNestedSubs = subscription.notifyNestedSubs.bind(subscription);

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

const overriddenContextValue = useMemo(() = > {
  if (didStoreComeFromProps) {
    returncontextValue! ; }return {
    ...contextValue,
    subscription,
  } as ReactReduxContextValue;
}, [didStoreComeFromProps, contextValue, subscription]);
Copy the code

CreateSubscription function was used to create a subscription instance. CreateSubscription function is described in detail above and has a nested subscription logic which is used here. The third parameter to createSubscription was passed to the subscription instance in the context and was adopted according to nested subscription logic. CreateSubscription what role in the third parameter), subscribe to the callback is actually registered in the connect to the parent of the contextValue. The subscription, if the parent is the top of the Provider, Then its subscription callbacks are actually registered with Redux, and if the parent is not the top level, there are still layers of nested registration callbacks. This implements “parent update first – child update later” to avoid stale props and zombie node issues.

To register the subconnect subscription callback with itself, a new ReactReduxContextValue: overriddenContextValue is generated with its own subscription for subsequent nested registrations.

const lastChildProps = useRef<unknown>();
const lastWrapperProps = useRef(wrapperProps);
const childPropsFromStoreUpdate = useRef<unknown>();
const renderIsScheduled = useRef(false);
const isProcessingDispatch = useRef(false);
const isMounted = useRef(false);

const latestSubscriptionCallbackError = useRef<Error> ();Copy the code

It then defines a batch of “persistent data” (not initialized as the component executes repeatedly) that will be used later for future “update judgments” and “updates driven by the parent component, updates from the store do not repeat.”

We just saw the subscription creation, and there is no specific update associated with it. The following code will go to

const subscribeForReact = useMemo(() = > {
  // Subscribes to updates and returns a function to unsubscribe
}, [subscription]);

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

let actualChildProps: unknown;

try {
  actualChildProps = useSyncExternalStore(
    subscribeForReact,
    actualChildPropsSelector,
    getServerState
      ? () = > childPropsSelector(getServerState(), wrapperProps)
      : actualChildPropsSelector
  );
} catch (err) {
  if (latestSubscriptionCallbackError.current) {
    (
      err as Error
    ).message += `\nThe error may be correlated with this previous error:\n${latestSubscriptionCallbackError.current.stack}\n\n`;
  }

  throw err;
}
Copy the code

After subscribeForReact, it is mainly used to judge whether to update or not, and it is the main entrance for initiating updates.

UseIsomorphicLayoutEffectWithArgs is a utility function, internal useIsomorphicLayoutEffect, front also talked about this function. Eventually they do: the second array parameter of each as parameters to the first call, the third parameter is the useIsomorphicLayoutEffect cache dependency.

Implemented the first parameter to the captureWrapperProps, its main function is to determine if they are updated from the store, is in the update (such as useEffect) trigger after the completion of the subscription. NotifyNestedSubs, notify the child to subscribe to updates.

It then wants to generate actualChildProps, the props needed for the selected business component, using useSyncExternalStore. If you look at useSyncExternalStore, You’ll see that it’s an empty method, and calling it directly throws an error, so it’s externally injected. In the entry index.ts, initializeConnect(useSyncExternalStore) initializes it, useSyncExternalStore comes from React. So actualChildProps actually React. UseSyncExternalStore (subscribeForReact actualChildPropsSelector, getServerState? () => Result of childPropsSelector(getServerState(), wrapperProps) : actualChildPropsSelector).

UseSyncExternalStore is a new API for React18, previously useMutableSource, in case a third party store is modified after a task has been disconnected in Concurrent mode and the task recovers in a tearing state. Updates to an external store can cause component updates through it. React-redux8 was implemented manually by useReducer prior to react-Redux8. This is the first time that react-Redux8 uses the new API. This also means that you must use replay 18+ along with it. Import {useSyncExternalStore} from ‘use-syncexternal-store/shim’; To do backward compatibility.

UseSyncExternalStore The first argument is a subscription function that causes updates to the component when triggered, and the second function returns an IMmutable snapshot that indicates whether or not updates should be made, and the returned result.

Let’s look at what the subscription function subscribeForReact does.

const subscribeForReact = useMemo(() = > {
  const subscribe = (reactListener: () => void) = > {
    if(! subscription) {return () = > {};
    }

    return subscribeUpdates(
      shouldHandleStateChanges,
      store,
      subscription,
      // @ts-ignore
      childPropsSelector,
      lastWrapperProps,
      lastChildProps,
      renderIsScheduled,
      isMounted,
      childPropsFromStoreUpdate,
      notifyNestedSubs,
      reactListener
    );
  };

  return subscribe;
}, [subscription]);
Copy the code

First use useMemo cache function, useCallback can also be used, and I think useCallback is more semantic. This function actually calls subscribeUpdates, so let’s look at subscribeUpdates.

function subscribeUpdates(
  shouldHandleStateChanges: boolean,
  store: Store,
  subscription: Subscription,
  childPropsSelector: (state: unknown, props: unknown) => unknown,
  lastWrapperProps: React.MutableRefObject<unknown>,
  lastChildProps: React.MutableRefObject<unknown>,
  renderIsScheduled: React.MutableRefObject<boolean>,
  isMounted: React.MutableRefObject<boolean>,
  childPropsFromStoreUpdate: React.MutableRefObject<unknown>,
  notifyNestedSubs: () => void,
  additionalSubscribeListener: () => void
) {
  if(! shouldHandleStateChanges)return () = > {};

  let didUnsubscribe = false;
  let lastThrownError: Error | null = null;

  const checkForUpdates = () = > {
    if(didUnsubscribe || ! isMounted.current) {return;
    }

    const latestStoreState = store.getState();

    let newChildProps, error;
    try {
      newChildProps = childPropsSelector(
        latestStoreState,
        lastWrapperProps.current
      );
    } catch (e) {
      error = e;
      lastThrownError = e as Error | null;
    }

    if(! error) { lastThrownError =null;
    }

    if (newChildProps === lastChildProps.current) {
      if (!renderIsScheduled.current) {
        notifyNestedSubs();
      }
    } else {
      lastChildProps.current = newChildProps;
      childPropsFromStoreUpdate.current = newChildProps;
      renderIsScheduled.current = true; additionalSubscribeListener(); }}; subscription.onStateChange = checkForUpdates; subscription.trySubscribe(); checkForUpdates();const unsubscribeWrapper = () = > {
    didUnsubscribe = true;
    subscription.tryUnsubscribe();
    subscription.onStateChange = null;

    if (lastThrownError) {
      throwlastThrownError; }};return unsubscribeWrapper;
}
Copy the code

CheckForUpdates, which gets the latestStoreState: latestStoreState (this is still obtained manually, and will be given to uSES by react-redux), the latest to be given to the props of the business component: NewChildProps, if the childProps is the same as the last time, it will not update and only notify the child Connect to try to update. If it changed childProps calls the React. UseSyncExternalStore incoming update method, called additionalSubscribeListener here, it will cause the component updates. React-redux8 used useReducer dispatches. CheckForUpdates will be handed over to the subscription. The onStateChange, we analyzed above, the subscription. The onStateChange will eventually be nested when redux store update call.

SubscribeUpdates function inside also calls the subscription. TrySubscribe () will be collected onStateChange parent subscription. We then call checkForUpdates in case the data changes the first time we render it. Finally, a function to unsubscribe is returned.

From the above analysis, the actual component updates are done by checkForUpdates. It is called in two ways:

  1. After redux store is updated, it is called by the parent hierarchy

  2. Render (parent render, component state) and useSyncExternalStore snapshot changed, causing the call

We’ll see that the checkForUpdates of a single connect will be called multiple times in a total update. For example, an update from Redux causes the parent to render, and its child element is the Connect component. Normally, we don’t do memo on the Connect component, so it will also be render, and its selectorProps will change, So checkForUpdates is called during render. When the parent updates are complete, the process itself is triggered, causing the child connect’s checkForUpdates to be called again. Won’t this cause the component to re-render more than once? I had this question when I first looked at the code. After the brain simulates the code scheduling of various scenarios, it is found that it avoids repeating the render in such a way, which can be summarized into these scenarios:

  1. Updates from the Redux Store and its own stateFromStore

  2. Update from redux Store, and its own stateFromStore is not updated

  3. Updates from the parent render component, with updates to its own stateFromStore

  4. Update from the parent render component, with its own stateFromStore not updated

  5. Updates from its own state, and updates to its own stateFromStore

  6. Updates from its own state, and its own stateFromStore has not been updated

ActualChildProps uses the cache directly and does not call checkForUpdates. It does not worry about multiple render

The updates for 1 and 2 are from the Redux store, so the parent component must update first (unless the connect is on top of the Provider). The connect is updated later, and the props from the parent component may change when connect render. Own stateFromStore may have changed, and so are called checkForUpdates, useRef childPropsFromStoreUpdate is set new childProps, interrupt the current render, Rerender, the component gets the new childProps value in render. The useEffect of the parent connect component is followed by a second wave of checkForUpdates. The childProps are already the same as the previous one, so it will not be updated, but will trigger the lower child CONNECT’s checkForUpdates. The underlying CONNECT logic is the same.

Type 3 and 4 updates are actually part of 1 and 2, so I won’t go into details.

Type 5 updates may occur when setState and Redux dispatch are called at the same time. According to the react-Redux nesting policy, the redux dispatch update must occur after setState. In the render process childPropsSelector(store.getState(), wrapperProps) gets the latest childProps, which is obviously changed. So the checkForUpdates, subsequent Redux dispatch updates childProps are already the same as the last time, so only notifyNestedSubs are used.

So far all the updates of all links in all scenarios have closed loops.

At the end of the CONNECT component:

const renderedWrappedComponent = useMemo(() = > {
  return (
    // @ts-ignore
    <WrappedComponent {. 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

The WrappedComponent is the business component passed in by the user, and the Contexttouse. Provider passes the connect subscription to the lower layer. Whether context passthrough is required is determined by the shouldHandleStateChanges variable, which is false if mapStateToProps is not available. That is, if mapStateToProps is not available, there is no need for this component and its children to subscribe to Redux.

useSelector

Then let’s look at useSelector:

function createSelectorHook(
  context = ReactReduxContext
).TState = DefaultRootState.Selected = unknown> (selector: (state: TState) => Selected, equalityFn? : EqualityFn
       ) = >Selected {
  const useReduxContext =
    context === ReactReduxContext
      ? useDefaultReduxContext
      : () = > useContext(context);

  return function useSelector<TState.Selected extends unknown> (selector: (state: TState) => Selected, equalityFn: EqualityFn
       
         = refEquality
       ) :Selected {
    const{ store, getServerState } = useReduxContext()! ;const selectedState = useSyncExternalStoreWithSelector(
      store.subscribe,
      store.getState,
      getServerState || store.getState,
      selector,
      equalityFn
    );

    useDebugValue(selectedState);

    return selectedState;
  };
}
Copy the code

UseSelector is created by createSelectorHook()

Just like connect, ReactReduxContext is used to retrieve data such as the Provider’s store.

Methods useSyncExternalStoreWithSelector is also empty, Be/SRC/index. The ts is set to import the from {useSyncExternalStoreWithSelector} ‘the use – sync – external – store/with – the selector’ useSyncExternalStoreWithSelector, and useSyncExternalStore works in a similar way. It subscribed directly to redux.store.subscribe. When the Redux Store is updated, components that use it are triggered to update and get the new selectedState.

Hooks are only state logic. They cannot provide Context to child components like the Connect component does, so they can only subscribe directly to redux horizontally. This is why hooks do not have nested subscriptions when we talked about zombie nodes at the beginning of this article. The useSelector code is much more concise than the version 7 code, you can see that there is not much after removing the non-production code, compared to the version 7 is much longer (165 lines), if you are interested.

A derivative of the React principle

useSelectorThere is another important difference from version 7! Learn more about React internals!

In version 7, subscription is registered in useEffect/useLayoutEffect execution. React traverses the fiber tree in the same order as before, using the beginWork method and calling completeWork when a leaf node is reached. CompleteWork puts things like useEffect, useLayoutEffect into the effectList, which will be executed sequentially in the COMMIT phase. The completeWork is done bottom-up, which means that the child executes before the parent, so in version 7, the child hooks register earlier and will be executed earlier. This is typically a “stale props”, “zombie children” problem.

Because I know the internal mechanism of React, I thought that the react-redux7 hooks would bug me at first, so I ran the code locally using several test cases via NPM link. The listener was actually called several times. This means that multiple Connect components will be updated, just as I thought the child component would be updated before the parent component, but the final render is only once, by the topmost parent connect Render, which will update the child connect below.

This leads to React’s batch update strategy. For example, in React16, all React events and life cycles are configured with a logic that sets a lock at the beginning. Therefore, all setState updates in React16 will not actually initiate updates. So React-Redux borrows this strategy and lets the components that need to be updated be batch updated from top to bottom as a whole. This comes from one of its less obvious aspects: SetBatch (batch), and I also misjudged it because I didn’t pay attention to the usefulness of this, but setBatch(batch) actually does what we’ll talk about later.

For batch update, take another example, for example, A has A child component B, B has A child component C, and call setState of C, B and A respectively in sequence. Normally, C, B and A will be updated in sequence respectively, while batch update will merge the three updates into one, and update directly from component A. B and C are updated in passing.

If there is an asynchronous task in the code, then the setState in the asynchronous task will “escape” the batch update. In this case, setState will update the component once every time. For example, react-redux cannot guarantee that the user will not call dispatch in a request callback (in fact, it is all too common to do so), so react-redux does setBatch(batch) in/SRC /index.ts. Batch from the import {unstable_batchedUpdates as batch} from ‘. / utils/reactBatchedUpdates’, Unstable_batchedUpdates is a manual batch update method provided by react-DOM, which can help set states that are out of control to batch update again. Batch is used in createListenerCollection in Subscription. Ts:

const batch = getBatch();
/ /...
return {
  notify() {
    batch(() = > {
      let listener = first;
      while(listener) { listener.callback(); listener = listener.next; }}); }};Copy the code

The notify method is used to manually update all updates to the listeners. Thus in React-Redux7, hooks register subscriptions from bottom up and do not cause problems.

And react – redux8 directly using the new API useSyncExternalStoreWithSelector subscriptions, happened during the render, so the order of the subscription is from top to bottom, to avoid the subscription to perform first. However, version 8 still has the above batch logic, the code is the same as 7, because batch update can save a lot of performance.

useDispatch

The last part is useDispatch

function createDispatchHook<S = RootStateOrAny.A extends Action = AnyAction> (context? : Context
       
        > = ReactReduxContext
       ) {
  const useStore =
    context === ReactReduxContext ? useDefaultStore : createStoreHook(context);

  return function useDispatch<
    AppDispatch extends Dispatch<A> = Dispatch<A> > () :AppDispatch {
    const store = useStore();
    return store.dispatch;
  };
}

export const useDispatch = createDispatchHook();
Copy the code

UseDispatch is very simple. You can use useStore() to fetch the Redux store and return to store.dispatch.

In addition to the above four apis, there are some other apis in/SRC /index.ts, but we have already analyzed the most difficult ones. The rest can be left to you to explore.

During the reading of the source code, I wrote some Chinese comments in fork’s React-Redux project, which was put into the react-Redux-with-comment repository as a new project. If you need to read the source code, you can check it out. The version is 8.0.0-beta-2

The last

So much for parsing the react-Redux source code. From the initial doubts about the performance of React-Redux, we read the source code for the first time. Later, we were curious about the solutions to the “stale props” and “Zombie children” problems in the official website, which drove us to explore deeper details. Through the exploration of principles, and then feed back to our business applications, and draw lessons from the design of excellent framework to trigger more thinking, this is a virtuous cycle. Read with curiosity and questions and ultimately apply them to the project, rather than reading for some purpose (for an interview, for example). A technique that can’t be applied to engineering has no value. If you see the end, you are very curious about technology, I hope you always keep a curious heart.

Follow me on Github, share-Technology, a repository where high-quality front-end technology articles are shared from time to time.