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.
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:
- In the constructor, it uses its
parentSub
to configure its own subscription. - 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)