Reconciliation

When a component’s props or state change, React decides whether an actual DOM update is necessary by comparing the newly returned element with the previously rendered one. When they are not equal, React will update the DOM. This process is called “reconciliation”.

It’s important to understand how React’s reconciler works in order to understand how to build performant apps as well as to avoid confusing bugs.

For a high-level introductory video, we suggest taking a look at https://youtu.be/EZV2rwnGgZA?start=905&end=1633

References

Official reconciliation documentation https://reactjs.org/docs/reconciliation.html

Understanding the difference between Components, Elements, and Instances: https://reactjs.org/blog/2015/12/18/react-components-elements-and-instances.html

Overview

To try to illustrate how the reconciler works at a high-level, we’ll look at an example.

Let’s say we had the following list of items rendered with React:

render() {
  return (
    <ul className="list">
      <li>A</li>
      <li>B</li>
      <li>C</li>
    </ul>
  );
}

And we wanted to transition to the following new state:

render() {
  return (
    <ul className="list">
      <li>A</li>
      <li>B</li>
      <li className="selected">C</li>
    </ul>
  );
}

One approach would be to re-render our component from scratch every time and swap out the DOM. However, components would lose their state and performance would suffer from recreating the same DOM elements every time. This could also affect overall user experience, for instance if we were constantly re-rendering an input as a user typed into it, the user would lose focus while typing every time our input re-rendered.

Instead, React uses a different approach called reconciliation.

When you use React, at a single point in time, you can think of the render() function as creating a tree of React elements. This tree is saved in memory. On the next re-render (typically triggered by a state or props update), that render() function will return a different tree of React elements. In this update stage, React will determine which child components to mount, update, and unmount.

React then needs to figure out how to efficiently update the UI to match the most recent tree. To do this, React diffs the new tree with the old tree, and will translate the differences to a set of DOM operations (in the case of an application running React Native, it would translate to a set of native operations).

Reconciliation versus rendering

The DOM is just one of the rendering environments React can render to, the other major targets being native iOS and Android views via React Native. (This is why "virtual DOM" is a bit of a misnomer.)

The reason it can support so many targets is because React is designed so that reconciliation and rendering are separate phases. The reconciler does the work of computing which parts of a tree have changed; the renderer then uses that information to actually update the rendered app. This separation means that React DOM and React Native can use their own renderers while sharing the same reconciler, provided by React’s core.

The Diffing Process

The diffing algorithm in React is based on two assumptions:

  1. Two elements of different types are assumed to generate substantially different trees. React will not attempt to diff them, but rather replace the old tree completely.

  2. The developer can hint at which child elements may be stable across different renders with a key prop. Keys should be stable, predictable, and unique. More on this later…

With these assumptions in mind, React takes its internal model of the DOM tree (aka virtual DOM) and compares it with the actual DOM beginning with the two root elements.

When diffing two trees, React first compares the two root elements. The behaviour is different depending on the types of the root elements.

Elements of different types

Whenever the root elements have different types, React will tear down the old tree and build the new tree from scratch. In that case, all of the previous DOM nodes are destroyed and components from the root level and below then will receive the lifecycle method componentWillUnmount().

React will then build up a new tree, and components will receive what you would now expect, componentWillMount() and componentDidMount()

Elements of the same type

What happens when React encounters two elements of the same type, but have different attributes?

DOM Elements of the same type 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.

<button className="btn" title="stuff" />
<button className="btn" title="other-stuff" />

By comparing these two elements, React knows to only modify the title attribute on the underlying DOM node for our button.

After handling any changes on the DOM node, React then recursively repeats this same process on the children (if there are any).

Component of the same type When a component updates, the instance stays the same, so that state is maintained across renders. React updates the props of the underlying component instance to match the new element, and calls componentWillReceiveProps() and componentWillUpdate() on the underlying instance.

Next, the render() method is called and the diff algorithm recurses on the previous result and the new result.

Using Keys

Understanding the problem To understand how keys work in React, we’ll look at an example. Let’s say we had the following list of products:

<ul>
  <li>Red Umbrella</li>
  <li>Green Socks</li>
</ul>

If we added an element at the end of the list, React can efficiently transition between the previous and the next state since it will traverse the tree from top to bottom and notice that the only thing that has changed is we’ve added a <li>Purple Unicorn</li> element at the end of the list.

<ul>
  <li>Red Umbrella</li>
  <li>Green Socks</li>
  <li>Purple Unicorn</li>
</ul>

However, if we tried inserting the element at the beginning of the list instead, it would have worse performance.

<ul>
  <li>Purple Unicorn</li>
  <li>Red Umbrella</li>
  <li>Green Socks</li>
</ul>

In this scenario, React would compare each element in the list one by one and notice that the text of the two first elements has changed from Red Umbrella to Purple Unicorn and from Green Socks to Red Umbrella, so it would mutate the innerText of these two elements. Then, it would get to the bottom of the list and notice that it needs to add a <li>Green Socks</li> element. So instead of only having one DOM insertion operation, we would actually have three separate DOM mutations.

The reason for this is that React has no way to understand that what we want is to actually add an item at the top of the list, and keep all of the existing items.

Solution In order to solve this issue, React supports providing a key attribute to DOM Elements and Component Elements. When children have keys, React uses the key to match children in the original tree with children in the subsequent tree.

For example, adding a key to our inefficient example above can make the tree conversion efficient:

<ul>
  <li key="SKU-003">Red Umbrella</li>
  <li key="SKU-005">Green Socks</li>
</ul>
<ul>
  <li key="SKU-015">Purple Unicorn</li>
  <li key="SKU-003">Red Umbrella</li>
  <li key="SKU-005">Green Socks</li>
</ul>

Now React would look at the old tree and the new tree and see that the only thing that has changed is we inserted a new element at the top of the list with the key SKU-015, so React would be smart enough to only insert that element at the top of the list and would not mutate the other elements.

Practical Considerations In practice, finding a predictable key is usually not hard. The element you are going to display may already have a unique ID, so the key can just come from your data:

<li key={product.id}>{product.name}</li>

Keys only need to be unique among sibling elements in the same array. They don’t need to be unique across the whole application or even a single component.

Don’t pass something like Math.random() to keys. It is important that keys have a “stable identity” across re-renders so that React can determine when items are added, removed, or re-ordered. Ideally, keys should correspond to unique and stable identifiers coming from your data, such as post.id.

Limitations There are some notable limitations that are important to be aware of:

  1. The reconciler will not try to match subtrees of different component types. If you see yourself alternating between two component types with very similar output, you may want to make it the same type.

  2. Keys should be stable, predictable, and unique. Unstable keys (like those produced by Math.random()) will cause many component instances and DOM nodes to be unnecessarily recreated, which can cause performance degradation and lost state in child components.

Resources / Further Reading

Reconciliation – React official docs https://reactjs.org/docs/reconciliation.html

Stack reconciler – React official docs https://reactjs.org/docs/codebase-overview.html#stack-reconciler

Updating – React official docs https://reactjs.org/docs/implementation-notes.html#updating

React fiber architecture – React official docs https://github.com/acdlite/react-fiber-architecture

Design principles – React official docs https://reactjs.org/docs/design-principles.html

Last updated