I’ve outlined main data structure and concepts that I’ll be using in this article, particularly Fiber nodes, current and work-in-progress trees, side-effects and the effects list. I’ve also provided a high-level overview of the main algorithm and explained the difference between the render
and commit
phases. If you haven’t read it, I recommend that you start there.
I’ve also introduced you to the sample application with a button that simply increments a number rendered on the screen:
You can play with it here. It’s implemented as a simple component that returns two child elements button
and span
from the render
method. As you click on the button, the state of the component is updated inside the handler. This results in the text update for the span
element:
class ClickCounter extends React.Component {
constructor(props) {
super(props);
this.state = {count: 0};
this.handleClick = this.handleClick.bind(this);
}
handleClick() {
this.setState((state) => {
return {count: state.count + 1};
});
}
componentDidUpdate() {}
render() {
return [
<button key="1" onClick={this.handleClick}>Update counter</button>,
<span key="2">{this.state.count}</span>
]
}
}
Here I’ve also added the componentDidUpdate
lifecycle method to the component. This is needed to demonstrate how React adds effects to call this method during the commit
phase.
In this article I want to show you how React processes state updates and builds the effects list. We’ll take a tour into what’s going on in the high-level functions for the render
and commit
phases.
Particularly, we’ll see how that in <a href="https://github.com/facebook/react/blob/cbbc2b6c4d0d8519145560bd8183ecde55168b12/packages/react-reconciler/src/ReactFiberCompleteWork.js#L532" target="_blank">completeWork</a>
React:
count
property in the state
of ClickCounter
render
method to get a list of children and performs comparisonspan
elementAnd, in <a href="https://github.com/facebook/react/blob/95a313ec0b957f71798a69d8e83408f40e76765b/packages/react-reconciler/src/ReactFiberScheduler.js#L523" target="_blank">commitRoot</a>
React:
textContent
property of the span
elementcomponentDidUpdate
lifecycle methodBut before that, let’s quickly take a look at how the work is scheduled when we call setState
in a click handler.
Note that you don’t need to know any of it to use React. This article is about how React works internally.
When we click on the button, the click
event is triggered and React executes the callback that we pass in the button props. In our application it simply increments the counter and updates the state:
class ClickCounter extends React.Component {
...
handleClick() {
this.setState((state) => {
return {count: state.count + 1};
});
}
}
Every React component has an associated updater
which acts as a bridge between the components and the React core. This allows setState
to be implemented differently by ReactDOM, React Native, server side rendering, and testing utilities.
In this article we’ll be looking at the implementation of the updater object in ReactDOM, which uses the Fiber reconciler. For the ClickCounter
component it’s a <a href="https://github.com/facebook/react/blob/6938dcaacbffb901df27782b7821836961a5b68d/packages/react-reconciler/src/ReactFiberClassComponent.js#L186" target="_blank">classComponentUpdater</a>
. It’s responsible for retrieving an instance of Fiber, queuing updates, and scheduling the work.
When updates are queued, they are basically just added to the queue of updates to process on a Fiber node. In our case, the Fiber node corresponding to the ClickCounter
component will have the following structure:
{
stateNode: new ClickCounter,
type: ClickCounter,
updateQueue: {
baseState: {count: 0}
firstUpdate: {
next: {
payload: (state) => { return {count: state.count + 1} }
}
},
...
},
...
}
As you can see, the function in the updateQueue.firstUpdate.next.payload
is the callback we passed to setState
in the ClickCounter
component.It represents the first update that needs to be processed during the render
phase.
The chapter on the work loop in my previous article explains the role of the nextUnitOfWork
global variable. Particularly, it states that this variable holds a reference to the Fiber node from the workInProgress
tree that has some work to do. As React traverses the tree of Fibers, it uses this variable to know if there’s any other Fiber node with unfinished work.
Let’s start with the assumption that the setState
method has been called. React adds the callback from <strong>setState</strong>
to the updateQueue
on the ClickCounter
fiber node and and schedules work. React enters the render
phase. It starts traversing from the topmost HostRoot
Fiber node using the renderRoot function. However, it bails out of (skips) the already processed Fiber nodes until it finds a node with unfinished work. At this point there’s only one Fiber node with some work to do. It’s the ClickCounter
Fiber node.
All work is performed on the cloned copy of this Fiber node is stored in the alternate
field. If the alternate node is not yet created, React creates the copy in the function createWorkInProgress before processing updates. Let’s assume that the variable nextUnitOfWork
holds a reference to the alternate ClickCounter
Fiber node.
First, our Fiber gets into the beginWork function.
The beginWork
function is basically a big switch
statement that determines the type of work that needs to be done for a Fiber node by the tag and then executes the respective function to perform the work. In the case of CountClicks
it’s a class component, so this branch will be taken:
function beginWork(current$1, workInProgress, ...) {
...
switch (workInProgress.tag) {
...
case FunctionalComponent: {...}
case ClassComponent:
{
...
return updateClassComponent(current$1, workInProgress, ...);
}
case HostComponent: {...}
case ...
}
and we get into the <a href="https://github.com/facebook/react/blob/1034e26fe5e42ba07492a736da7bdf5bf2108bc6/packages/react-reconciler/src/ReactFiberBeginWork.js#L428" target="_blank">updateClassComponent</a>
function. Depending on whether it’s the first rendering of a component, work being resumed, or a React update, React either creates an instance and mounts the component or just updates it:
function updateClassComponent(current, workInProgress, Component, ...) {
...
const instance = workInProgress.stateNode;
let shouldUpdate;
if (instance === null) {
...
// In the initial pass we might need to construct the instance.
constructClassInstance(workInProgress, Component, ...);
mountClassInstance(workInProgress, Component, ...);
shouldUpdate = true;
} else if (current === null) {
// In a resume, we'll already have an instance we can reuse.
shouldUpdate = resumeMountClassInstance(workInProgress, Component, ...);
} else {
shouldUpdate = updateClassInstance(current, workInProgress, ...);
}
return finishClassComponent(current, workInProgress, Component, shouldUpdate, ...);
}
We already have an instance of the ClickCounter
component, so we get into the <a href="https://github.com/facebook/react/blob/6938dcaacbffb901df27782b7821836961a5b68d/packages/react-reconciler/src/ReactFiberClassComponent.js#L976" target="_blank">updateClassInstance</a>
. That’s where React performs most of the work for class components. Here are the most important operations performed in the function in the order of execution:
UNSAFE_componentWillReceiveProps<strong>()</strong>
hook (deprecated)updateQueue
and generate new stategetDerivedStateFromProps
with this new state and get the resultshouldComponentUpdate
to ensure a component wants to update;false
, skip the whole rendering process, including calling render
on this component and its children; otherwise proceed with the updateUNSAFE_componentWillUpdate
(deprecated)componentDidUpdate
** **lifecycle hookstate
and props
on the component instance<em>state</em>
* and <em>props</em>
should be updated on the component instance before the *<em>render</em>
method is called, since the <em>render</em>
method output usually depends on the <em>state</em>
and <em>props</em>
. If we don’t do that, it will be returning the same output every time.
Here’s the simplified version of the function:
function updateClassInstance(current, workInProgress, ctor, newProps, ...) {
const instance = workInProgress.stateNode;
const oldProps = workInProgress.memoizedProps;
instance.props = oldProps;
if (oldProps !== newProps) {
callComponentWillReceiveProps(workInProgress, instance, newProps, ...);
}
let updateQueue = workInProgress.updateQueue;
if (updateQueue !== null) {
processUpdateQueue(workInProgress, updateQueue, ...);
newState = workInProgress.memoizedState;
}
applyDerivedStateFromProps(workInProgress, ...);
newState = workInProgress.memoizedState;
const shouldUpdate = checkShouldComponentUpdate(workInProgress, ctor, ...);
if (shouldUpdate) {
instance.componentWillUpdate(newProps, newState, nextContext);
workInProgress.effectTag |= Update;
workInProgress.effectTag |= Snapshot;
}
instance.props = newProps;
instance.state = newState;
return shouldUpdate;
}
I’ve removed some auxiliary code in the snippet above. For instance, before calling lifecycle methods or adding effects to trigger them, React checks if a component implements the method using the typeof
operator. Here is, for example, how React checks for the componentDidUpdate
method before adding the effect:
if (typeof instance.componentDidUpdate === 'function') {
workInProgress.effectTag |= Update;
}
Okay, so now we know what operations are performed for the ClickCounter
Fiber node during the render phase. Let’s now see how these operations change values on the Fiber nodes.When React begins work, the Fiber node for the ClickCounter
component looks like this:
{
effectTag: 0,
elementType: class ClickCounter,
firstEffect: null,
memoizedState: {count: 0},
type: class ClickCounter,
stateNode: {
state: {count: 0}
},
updateQueue: {
baseState: {count: 0},
firstUpdate: {
next: {
payload: (state, props) => {…}
}
},
...
}
}
After the work is completed, we end up with a Fiber node that looks like this:
{
effectTag: 4,
elementType: class ClickCounter,
firstEffect: null,
memoizedState: {count: 1},
type: class ClickCounter,
stateNode: {
state: {count: 1}
},
updateQueue: {
baseState: {count: 1},
firstUpdate: null,
...
}
}
Take a moment to observe the differences in properties values.
After the update is applied, the value of the property count
is changed to 1
** **in the memoizedState
and the baseState
in updateQueue
. React has also updated the state in the ClickCounter
component instance.
At this point, we no longer have updates in the queue, so firstUpdate
is null
.And importantly, we have changes in the effectTag
property. It’s no longer 0
, it’s value is <strong>4</strong>
. In binary this is 100
, which means that the third bit is set, which is exactly the bit for the Update
side-effect tag:
export const Update = 0b00000000100;
So to conclude, when working on the parent ClickCounter
Fiber node, React calls the pre-mutation lifecycle methods, updates the state and defines relevant side-effects.
Once that’s done, React gets into the finishClassComponent. This is where React calls the render
method on a component instance and applies its diffing algorithm to the children returned by the component. The high-level overview is described in the docs. Here’s the relevant part:
When comparing two React DOM elements of the same type, React looks at the attributes of both, keeps the same underlying DOM node, and only updates the changed attributes.
If we dig deeper, however, we can learn that it actually compares Fiber nodes with React elements. But I won’t go into much details now as the process is quite elaborate. I’ll write a separate piece that focuses particular on the process of child reconciliation.
If you’re anxious to learn details on your own, check out the reconcileChildrenArrayfunction since in our application therender
method returns an array of React Elements.
At this point there are two things that are important to understand. First, as React goes through the child reconciliation process, it creates or updates Fiber nodes for the child React elements returned from therender
method. ThefinishClassComponent
function returns the reference to the first child of the current Fiber node. It will be assigned to thenextUnitOfWork
and processed later in the work loop. Second, React updates the props on the children as part of work performed for the parent. To do that it uses data from the React elements returned fromrender
method.
For example, here’s what the Fiber node corresponding to the span
element looks like before React reconciles the children for the ClickCounter
fiber:
{
stateNode: new HTMLSpanElement,
type: "span",
key: "2",
memoizedProps: {children: 0},
pendingProps: {children: 0},
...
}
As you can see, the children
property in both memoizedProps
and pendingProps
is 0
. Here’s the structure of the React element returned from the render
for the span
element:
{
$typeof: Symbol(react.element)
key: "2"
props: {children: 1}
ref: null
type: "span"
}
As you can see, there’s a difference between the props in the Fiber node and the returned React element. Inside the <a href="https://github.com/facebook/react/blob/769b1f270e1251d9dbdce0fcbd9e92e502d059b8/packages/react-reconciler/src/ReactFiber.js#L326" target="_blank"><strong>createWorkInProgress</strong></a>
function that is used to create alternate Fiber nodes, React will copy the updated properties from the React element to the Fiber node.
So, after React has finished reconciling the children for the ClickCounter
component, the span
Fiber node will have the pendingProps
updated. They will match the value in the span
React element:
{
stateNode: new HTMLSpanElement,
type: "span",
key: "2",
memoizedProps: {children: 0},
pendingProps: {children: 1},
...
}
Later, when React will be performing work for the span
Fiber node, it will copy them to the memoizedProps
and add effects to update DOM.
Well, that’s all the work that React performs for the ClickCounter
fiber node during the render phase. Since the button is the first child of the ClickCounter
component, it will be assigned to the nextUnitOfWork
** **variable. There’s nothing to be done with it, so React will move to its sibling, which is span
Fiber node. According to the algorithm described here, it happens in the completeUnitOfWork
function.
So, the variable nextUnitOfWork
now points to the alternate of the span
fiber and React starts working on it. Similar to the steps performed for the ClickCounter
, we start with the beginWork function.
Since our span
node is of HostComponent
type, this time in the switch statement React takes this branch:
function beginWork(current$1, workInProgress, ...) {
...
switch (workInProgress.tag) {
case FunctionalComponent: {...}
case ClassComponent: {...}
case HostComponent:
return updateHostComponent(current, workInProgress, ...);
case ...
}
and ends up in the <a href="https://github.com/facebook/react/blob/cbbc2b6c4d0d8519145560bd8183ecde55168b12/packages/react-reconciler/src/ReactFiberBeginWork.js#L686" target="_blank">updateHostComponent</a>
function. You can see a parallel with the updateClassComponent
function called for class components. For a functional component it’ll be updateFunctionComponent
and so on. You can find all these functions in the <a href="https://github.com/facebook/react/blob/1034e26fe5e42ba07492a736da7bdf5bf2108bc6/packages/react-reconciler/src/ReactFiberBeginWork.js" target="_blank">ReactFiberBeginWork.js</a>
file.
In our case there nothing important happening for the span
node in the updateHostComponent
.
Once beginWork
is finished, the node gets into the completeWork
function. But before that, React needs to update the memoizedProps
on the span fiber. You may remember that when reconciling children for the ClickCounter
** **component, React updated the pendingProps
on the span
Fiber node:
{
stateNode: new HTMLSpanElement,
type: "span",
key: "2",
memoizedProps: {children: 0},
pendingProps: {children: 1},
...
}
So once beginWork
is finished for the <strong>span</strong>
fiber, React updates pendingProps
to match memoizedProps
:
function performUnitOfWork(workInProgress) {
...
next = beginWork(current$1, workInProgress, nextRenderExpirationTime);
workInProgress.memoizedProps = workInProgress.pendingProps;
...
}
It then calls the completeWork
function which is basically a big switch
statement similar to the one we saw in beginWork
:
function completeWork(current, workInProgress, ...) {
...
switch (workInProgress.tag) {
case FunctionComponent: {...}
case ClassComponent: {...}
case HostComponent: {
...
updateHostComponent(current, workInProgress, ...);
}
case ...
}
}
Since our span
Fiber node is HostComponent
, it runs the <a href="https://github.com/facebook/react/blob/cbbc2b6c4d0d8519145560bd8183ecde55168b12/packages/react-reconciler/src/ReactFiberBeginWork.js#L686" target="_blank">updateHostComponent</a>
function. In this function React basically does the following:
updateQueue
of the span
fiberBefore these operations are performed, the span
Fiber node looks like this:
{
stateNode: new HTMLSpanElement,
type: "span",
effectTag: 0
updateQueue: null
...
}
and when the work is completed it looks like this:
{
stateNode: new HTMLSpanElement,
type: "span",
effectTag: 4,
updateQueue: ["children", "1"],
...
}
Notice the difference in the effectTag
and updateQueue
fields. It’s no longer 0
, it’s value is 4
. In binary this is 100
, which means that the third bit is set,which is exactly the bit for the Update
side-effect tag. That’s the only job React needs to do for this node during the following commit phase. The updateQueue
field holds the payload that will be used for the update.
Once React has processed ClickCounter
and its children, it’s done with the render
phase. It can now assign the completed alternate tree to the finishedWork
property on FiberRoot
. This is the new tree that needs to be flushed to the screen. It can be processed immediately after the render
phase or picked up later when React is given time by the browser.
In our case, since the span
node and the ClickCounter
component have side effects, React will add a link to the span
Fiber node to the firstEffect
property of HostFiber
.
React builds the effects list in the <a href="https://github.com/facebook/react/blob/d5e1bf07d086e4fc1998653331adecddcd0f5274/packages/react-reconciler/src/ReactFiberScheduler.js#L999" target="_blank">compliteUnitOfWork</a>
function. Here’s what a Fiber tree with effects to update text of the span
node and calls hooks on ClickCounter
looks like:
And here’s the linear list of nodes with effects:
This phase begins with the function completeRoot. Before it gets to do any work, it sets the finishedWork
property on the FiberRoot
to null
:
root.finishedWork = null;
Unlike the first render
phase, the commit
phase is always synchronous so it can safely update HostRoot
to indicate that the commit work has started.
The commit
phase is where React updates the DOM and calls the post mutation lifecycle method componentDidUpdate
. To do that, it goes over the list of effects it constructed during the previous render
phase and applies them.
We have the following effects defined in the render
phase for our span
and ClickCounter
nodes:
{ type: ClickCounter, effectTag: 5 }
{ type: 'span', effectTag: 4 }
The value of the effect tag for ClickCounter
** is 5
or 101
in binary and defines the Update
**work which basically translates into the componentDidUpdate
lifecycle method for class components. The least significant bit is also set to signal that all work has been completed for this Fiber node in the render
phase.
The value of the effect tag for span
is 4
or 100
in binary and defines the update
** work for the host component DOM update. In the case of the span
element, React will need to update textContent
**for the element.
Let’s see how React applies those effects. The function <a href="https://github.com/facebook/react/blob/95a313ec0b957f71798a69d8e83408f40e76765b/packages/react-reconciler/src/ReactFiberScheduler.js#L523" target="_blank">commitRoot</a>
, which is used to apply the effects, consists of 3 sub-functions:
function commitRoot(root, finishedWork) {
commitBeforeMutationLifecycles()
commitAllHostEffects();
root.current = finishedWork;
commitAllLifeCycles();
}
Each of those sub-functions implements a loop that iterates over the list of effects and checks the types of the effects. When it finds the effect pertaining to the function’s purpose, it applies it. In our case, it will call the componentDidUpdate
lifecycle method on the ClickCounter
component and update the text of the span
element.
The first function commitBeforeMutationLifeCycles looks for the <a href="https://github.com/facebook/react/blob/b87aabdfe1b7461e7331abb3601d9e6bb27544bc/packages/shared/ReactSideEffectTags.js#L25" target="_blank">Snapshot</a>
effect and calls the getSnapshotBeforeUpdate
method. But, since we didn’t implement the method on the ClickCounter
component, React didn’t add the effect during the render
stage. So in our case, this function does nothing.
Next React moves to the <a href="https://github.com/facebook/react/blob/95a313ec0b957f71798a69d8e83408f40e76765b/packages/react-reconciler/src/ReactFiberScheduler.js#L376" target="_blank">commitAllHostEffects</a>
function. This is where React will change the text on the span
element from 0
to 1
. There’s nothing to do for the ClickCounter
fiber because nodes corresponding to class components don’t have any DOM updates.
The gist of the function is that it selects the correct type of effect and applies the corresponding operations. In our case we need to update the text on the span
element, so we take the Update
branch here:
function updateHostEffects() {
switch (primaryEffectTag) {
case Placement: {...}
case PlacementAndUpdate: {...}
case Update:
{
var current = nextEffect.alternate;
commitWork(current, nextEffect);
break;
}
case Deletion: {...}
}
}
By going down to commitWork
, we will eventually get into the <a href="https://github.com/facebook/react/blob/8a8d973d3cc5623676a84f87af66ef9259c3937c/packages/react-dom/src/client/ReactDOMComponent.js#L326" target="_blank">updateDOMProperties</a>
function. It takes the updateQueue
payload that was added during the render
stage to the Fiber node, and updates the textContent
property on the span
element:
function updateDOMProperties(domElement, updatePayload, ...) {
for (let i = 0; i < updatePayload.length; i += 2) {
const propKey = updatePayload[i];
const propValue = updatePayload[i + 1];
if (propKey === STYLE) { ...}
else if (propKey === DANGEROUSLY_SET_INNER_HTML) {...}
else if (propKey === CHILDREN) {
setTextContent(domElement, propValue);
} else {...}
}
}
After the DOM updates have been applied, React assigns the finishedWork
tree to the HostRoot
. It sets an alternate tree as current:
root.current = finishedWork;
The last remaining function is <a href="https://github.com/facebook/react/blob/d5e1bf07d086e4fc1998653331adecddcd0f5274/packages/react-reconciler/src/ReactFiberScheduler.js#L479" target="_blank"><strong>commitAllLifecycles</strong></a>
. This where React calls the post mutational lifecycle methods. During the render
phase, React added the Update
effect to the ClickCounter
component. This is one of the effects that the function commitAllLifecycles
looks for and calls componentDidUpdate
method:
function commitAllLifeCycles(finishedRoot, ...) {
while (nextEffect !== null) {
const effectTag = nextEffect.effectTag;
if (effectTag & (Update | Callback)) {
const current = nextEffect.alternate;
commitLifeCycles(finishedRoot, current, nextEffect, ...);
}
if (effectTag & Ref) {
commitAttachRef(nextEffect);
}
nextEffect = nextEffect.nextEffect;
}
}
The function also updates refs, but since we don’t have any this functionality won’t be used. The method is called in the <a href="https://github.com/facebook/react/blob/e58ecda9a2381735f2c326ee99a1ffa6486321ab/packages/react-reconciler/src/ReactFiberCommitWork.js#L351" target="_blank">commitLifeCycles</a>
function:
function commitLifeCycles(finishedRoot, current, ...) {
...
switch (finishedWork.tag) {
case FunctionComponent: {...}
case ClassComponent: {
const instance = finishedWork.stateNode;
if (finishedWork.effectTag & Update) {
if (current === null) {
instance.componentDidMount();
} else {
...
instance.componentDidUpdate(prevProps, prevState, ...);
}
}
}
case HostComponent: {...}
case ...
}
You can also see that this is the function where React calls the componentDidMount
method for components that have been rendered for the first time.
And that’s it!
We’re finally done. Let me know what you think about the article or ask questions in the comments. I have many more articles in the works providing in-depth explanation for scheduler, children reconciliation process and how effects list is built. I also have plans to create a video where I’ll show how to debug the application using this article as a basis.
Recommended Reading
☞ React Native and Apollo GraphQL Tutorial: Build Mobile Apps
☞ The React Cookbook: Advanced Recipes to Level Up Your Next App
☞ Firebase login functionality from scratch with React + Redux
☞ Breaking Down the Last Monolith
☞ Have Confidence in Your Redux Apps With Integration and Unit Testing
☞ Setup Continuous Integration (CI) With React, CircleCI, and GitHub
☞ React Redux binding with React Hooks and Proxy
#reactjs #javascript