In the update stage of a component, React has to determine which child components to mount, update, and unmount.
One naive approach would be to render components from scratch every time and swap out the DOM. However, components would lose their state and performance would suffer from recreating the same DOM every time. Instead, React keeps the output of the previous render call and compares it with the new output to decide what to do. This comparison is called reconciliation.
It’s important to understand how React’s reconciliation works to build performant apps as well as avoid confusing bugs. Understanding the reconciliation process will also give you more confidence to bypass React and work with the underlying DOM. Fortunately, React’s reconciliation process is simple (for good reasons) and easy to understand.
In this article, we will take a more practical approach to understanding reconciliation. It is also worth reading the official reconciliation docs to understand the design motivations.
The heart of the reconciler lives in ReactChildReconciler.updateChildren
. Let's walk through the code. Follow the source here.
nextChildren
).prevChildren
) that has the same key as the new child. If an explicit key is not provided, React uses its position. [getComponentKey
source].shouldUpdateReactComponent
to decide whether we should update the instance vs doing a clean unmount/mount. [source]If React decides to update a child instance, React will then call render
on the instance and again reconcile the output and its children. In the other case where an instance is unmounted and a new one is mounted, there is no further reconciling or DOM updates.
Let's look at how reconciliation can drastically change the behavior and performance of a React application. Take a look at these two implementations of render.
render1() {
if (this.state.showWarning) {
return (
<div>
<Warning />
<StatefulComponent />
</div>
);
}
return (
<div>
<StatefulComponent />
</div>
);
}
render2() {
return (
<div>
{this.state.showWarning ? <Warning /> : null}
<StatefulComponent />
</div>
);
}
While they might look the same at first glance, their behaviors can be very different.
Let's start with the first example. When the value of this.state.showWarning
changes, <StatefulComponent>
will always unmount and then remount a new instance. This is almost always undesirable. Let's take a closer look why this happens.
Pass | 1 | 2 |
this.state.showWarning | false | true |
render1 div children | [<StatefulComponent>] | [<Warning>, <StatefulComponent>] |
render2 div children | [null, <StatefulComponent>] | [<Warning>, <StatefulComponent>] |
With the render1
's output, React will compare the <StatefulComponent>
with the <Warning>
and notice that they have different types. Therefore, React will decide that this pair of components should perform an unmount and mount (unmount the <StatefulComponent>
and mount the <Warning>
in its place). React will then mount the “new” <StatefulComponent>
because there is no previous component in the same place.
render3() {
if (this.state.showWarning) {
return (
<div>
<Warning />
<StatefulComponent key="a" />
</div>
);
}
return (
<div>
<StatefulComponent key="a" />
</div>
);
}
This implementation will have the same desirable behavior of render2
, although it is a little harder to read. This works because React will reconcile the <StatefulComponent>
with each other because they have the same keys.
Components have lifecycles like you and me. Understanding the different lifecycle stages will help you do more with React.
Subscribe for a range of articles from React basics to advanced topics such as performance optimization and deep dives in the React source code.