Implement React Redux from Scratch (Part 2)

Subscription and Notification

Previous Story: Implement React Redux from Scratch (Part 1)

Subscribing container to the store is one of the most important functional requirement in react-redux. To limit the scope of the problem here, let’s focus on the most general scenario: Use Provider to expose the store in the context for all the child components.


The order matters

It seems intuitive to subscribe to the store at componentDidMount for each component since that’s when the UI gets settled down. But hold on! Do we subscribe each component to the single store passed down from the context? If so, since descendant components get mounted before ancestors, store notification will then trigger re-render of descendants before ancestors, which conflicts with the normal top-down data flow.

source of truth in react-redux
Think about the “source of truth” for the child component. It might come from the redux store as well as its parent (as props). The data source flows top down and thus notification and update should follow this pattern to avoid unnecessary re-rendering.

To cater the “top-down” or “unidirectional” data flow of React, the order of notification is important. And react-redux uses a very smart trick to ensure the ancestor components get notified and re-render before descendants.

The key to the trick is that each container component maintains its own subscription and notification system, thus instead of relying on the single store to notify every container, each container will be in charge of notifying the child components “below” it. You will see in a moment that react-redux has a special utility class for constructing this “system” called Subscription.

You can refer Dan Abramov’s tutorial video on egghead.io to see how he implements the store and its subscription system from scratch. It is a typical observation pattern using an array to store the listeners and notify them in order when the state changes.

But let’s take a step back and start from the Provider, which “provides” the context for components.

Surprise! Besides the store object, Provider also provides another context variable called parentSub. This parentSub refers to the Subscription instance of the ancestor container and it is initialized as null. However, this context variable gets updated when it is passed down! (check React’s document on Context to see how it get’s updated)

The above updated Connect Component does two additional things:

  1. In the constructor, it uses its parentSub to configure its own subscription.
  2. In the getChildContext, it replaces the parent’s subscription with its own subscription so its child component can get access to it.

Notice that when its onStateChange (the state change listener) gets called, it first resets itself, after that (componentDidUpdate), it uses its own subscription to notify the nested subscription (by calling its notifyNestedSubs function). That’s how it maintains the order of notification. The ancestor’s onStateChange get called first, update itself, then notify the descendants’ onStateChange recursively.

The question is how it maintains the order of subscription? How it ensures that the root container is notified at the very beginning?

The solution lies in the trySubscribe call when the container is mounted. It’s time to introduce the Subscription class:

You can see that a subscription has the trySubscribe and notifyNestedSubs method defined in the subscriptionShape. It also has another method addNestedSub which will be called in the trySubscribe. To illustrate the subscribing process, let’s image the scenario below:

<Provider>
<Container1>
<Container2 />
</Container1>
</Provider>

Container2 will be mounted first and trigger the trySubscribe method of its own subscription instance. What this method does is to try to subscribe the callback (onStateChange) function to its parent’s (Container1) subscription system by calling its addNestedSubs method.

In addNestedSubs, it(the subscription of Container1) will first call its own trySubscribe method and repeat the process above. However, since the root container does not have any ancestor Container, its parentSub will be the initial null value from the Provider. In this case, it will subscribe its’ own onStateChange to the store directly.

This recursive process ensures that only the onStateChange callback of the root container subscribes to the store and the following onStateChange will subscribe to the parentSub of each component.

You can test with the complete code to see how it works. In summary, the bottom-up subscribing process when mounted and the top-down notification process when state change help to ensure the order of re-rendering of the Redux app.

In the next story, I will show you another important part of react-redux: Selector, which is how we parse the raw data such as store state and own props and get the merged props to be injected to the wrapped component. It is where most of the optimizations take place.

Next Story: Implement React Redux from Scratch (Part 3)