Demystifying re-rendering, diffing, and reconciliation in React

Re-render in React

React, one of the most popular JavaScript libraries for building user interfaces, is known for its efficient rendering and seamless UI updates. Behind the scenes, React employs a clever mechanism called re-rendering, which triggers updates to the user interface when the component state changes. However, have you ever wondered how React manages to update the UI so swiftly, without unnecessary overhead?

In this article, we'll explore how React's re-rendering works under the hood. We'll also learn about the concepts of diffing and reconciliation and how they optimize UI updates in React applications.

What is re-rendering?

When React app starts, React builds a tree of components that begins with the root component and uses that tree to render the UI. When you want to update something in the UI, React provides a mechanism that you can use to update any component you want. The mechanism that you can only use to update a component is called "state". state is a variable that you can use to store data in a component. When you update the state of a component, React will trigger that change and it will re-render the component and all the nested components inside it.

Re-render means that React will call that component function again that its state has been changed. this is re-render.

And when the component re-render all its internal functions, variables, and UI will be re-created again.

How does React triggers a re-render?

For every component you create, React actually knows that component's props, state, event handlers (the functions inside your component), and the UI. And the only place you can call your setState function that will update the state of your component is inside the event handlers. So when the event handler is called, and if it contains an invocation of the setState function, React sees that new state is different from the old state, and it will trigger a re-render.

Note: Keep in mind that React will only update the state after the event handler is finished running. So if you have multiple setState functions that is called inside the event handler, React will not update the state until that function is finished running. and that is called batching.

So when React trigger a re-render in a component, React will create a new tree of components, starting from the component that its state has been changed. And React will compare the new tree with the old tree, and it will see what components are different between the two trees, and it will update only the different components. Then It will paint the UI with the updated components.

In the above description there are three important concepts hidden in it, "tree of components", "compare the new tree with the old tree", and update only the different components.

Let introduce the first concept, "tree of components".

The tree of components also known as "Virtual DOM" or in the new version of React, it is called "Fiber Tree".

The virtual DOM is a tree of components that React builds from the root component, and it will recursively build the tree of components from the root component to the nested components.

React uses the virtual DOM to keep track of the components and their state, and it uses it to render the UI.

The second concept, "compare the new tree with the old tree".

Also known as "Diffing Algorithm".

The diffing algorithm in React is responsible for efficiently comparing the previous virtual DOM representation of a component with the new updated virtual DOM representation. It determines the minimal set of changes required to update the actual browser DOM, minimizing unnecessary updates and improving performance.

The third concept, "update only the different components".

Also known as "Reconciliation Process".

The reconciliation process is the mechanism through which the framework determines how to apply the changes identified during the diffing algorithm. It is responsible for updating the browser DOM to reflect the new state and virtual DOM representation of a component.

In a nutshell, the reconciliation process is applied to the entire virtual DOM tree, to extract information about what DOM elements should be (updated, added, or removed) and how to update them efficiently.

Let's put our above knowledge into appropriate format.

  • The only thing that triggers re-rendering in React is the state change.
  • React by default will also re-renders all nested components inside the component that its state has been changed.
  • When the App component mounts for the first time, React will builds a tree of components, called "Virtual DOM", or in the new version of React, it is called Fiber Tree, but let's stick with the Virtual DOM.
  • When a component's state changes, React will build a second Virtual DOM tree making the component that its state has been changed as a root, and it will compare the two trees and this also know as "diffing", then React will extract some information that things it should to do such as what DOM elements should be (updated, added, or removed) and this is called "Reconciliation".

Understanding how does React do the diffing and the reconciliation.

Before that, it's very important to know how does React build the Virtual DOM tree, and how does React represents the node in the Virtual DOM tree.

But first, we know that React build the virtual DOM from the components output we build, right? so let's begin by understanding what is a component means in React.

A Component is just a function that that accept arguments "props" and return a React Element.

Copy
1function App(props) {
2    return <Button label="Hello World!" />
3}

In React any thing is wrapped with the < > is a React Element. React Element is just a plain Javascript object that represents a DOM element or another component.

Copy
1
2const element = <h1 className="greeting">Hello, world!</h1>;
3

As we know browser can only understand HTML, CSS, and Javascript, so React will transform the above code into a code that the browser can understand. React Element that we are creating above is just a syntactic sugar for the below code.

Copy
1
2const element = React.createElement(
3  'h1', // type
4  {className: 'greeting'}, // props
5  'Hello, world!' // children
6);
7

The React.createElement function will return a descriptive object with a type property that represents the type of the element that can be points to a DOM element or React component , props property that represents the props of the element.

Copy
1
2{
3    type: 'h1',
4    props: {
5        className: 'greeting',
6        children: 'Hello, world!'
7    }
8  }
9

Let's put focus about the type property. The type property can be a string that represents a DOM element or a function that represents a React component.

Copy
1
2const elementAsDOMElement = <h1 className="greeting">Hello, world!</h1>;
3const elementAsReactComponent = <App />;
4

this will be transformed into the below code.

Copy
1
2const elementAsDOMElement = {
3    type: 'h1',
4    ...
5}
6
7const elementAsReactComponent = {
8    type: App,
9    ...
10}
11

Note: to be not confused, children is a props property too, but it is a special props property that represents the children of the element.

Copy
1
2const App = ()=>{
3    return <SomeComponent children={<Button label="Hello World!" />} />
4}
5
6// is the same thing as
7
8const App = ()=>{
9    return <SomeComponent>
10        <Button label="Hello World!" />
11    </SomeComponent>
12}
13

Now we understood how does React represent the node in the Virtual DOM tree, let's understand how does React build the Virtual DOM tree.

Copy
1
2function Component4() {
3    return <h1>Hello World! 3</h1>
4}
5
6function Component3() {
7    return <h1>Hello World!</h1>
8}
9
10function Component4() {
11    return <Component3 />
12}
13
14function Component1() {
15    return <Component3 />
16}
17
18
19function App() {
20    return <div>
21      <Component1 />
22      <Component2 />
23    </div>
24}
25

React will build the Virtual DOM tree from the App component, and it will recursively build the Virtual DOM tree from the App component to the nested components.

Copy
1
2{
3    type: 'div',
4    props: {
5        children: [
6            {
7                type: Component1,
8                props: {
9                    children: {
10                        type: Component3,
11                        props: {
12                            children: {
13                                type: 'h1',
14                                props: {
15                                    children: 'Hello World!'
16                                }
17                            }
18                        }
19                    }
20                }
21            },
22            {
23                type: Component2,
24                props: {
25                    children: {
26                        type: 'h1',
27                        props: {
28                            children: 'Hello World! 2'
29                        }
30                    }
31                }
32            }
33        ]
34    }
35}
36

All things are clear now to understand how does React do the diffing and the reconciliation.

Let us put a simple example that triggers a re-render in React.

Copy
1
2const Button1 = ({label})=>{
3    return <button className"red">{label}</button>
4}
5
6const Button2 = ({label})=>{
7    return <button className="blue">{label}</button>
8}
9
10const App = ()=>{
11    const [isError, setIsError] = useState(false);
12    ... 
13    ...
14    return isError ? <Button1 label="Hello World!" /> : <Button2 label="Hello World!" />
15}
16
17

isError begins as false, so React will build the Virtual DOM tree from the App component.

Copy
1{
2    type: Button1,
3    props: {
4        label: 'Hello World!'
5    }
6}
7

When isError changes to true React will build a second Virtual DOM tree, and it will compare the two trees and this also know as "diffing"

Copy
1// New Tree
2{
3    type: Button2,
4    props: {
5        label: 'Hello World!'
6    }
7}
8

React checks if the type is the same, and if the props are the same, and if the children are the same, and if they are the same, React will not do anything. But in our case here React sees that the type is different, so React will extract some information such as Button1 should be removed "unmounted", and Button2 should be added "mounted", ("Reconciliation").

Easy right? let's put a more complex example.

Copy
1const Button= ({label})=>{
2    return <butto>{label}</butto>
3}
4
5const App = ()=>{
6    const [isMorning, setIsMorning] = useState(true);
7    ... 
8    ...
9    return isMorning ? <Button label="Good Morning" /> : <Button label="Good Night" />
10}
11

Now Can you guess what will happen when isMorning change to false?

Like we have mentioned above, React will build a second Virtual DOM tree, and it will compare the two trees.

It will sees that the type is the same reference in the memory and the props are different, so React will extract some information such as Button should be updated "re-rendered".

Copy
1{
2    type: Button,
3    props: {
4        label: 'Good Morning'
5    }
6}
7

The final result will be.

Copy
1{
2    type: Button,
3    props: {
4        label: 'Good Night'
5    }
6}
7

Can you guess how does React compare the type and the props?

React will compare the type and the props by using the Object.is function.

Copy
1
2Object.is(1, 1) // true
3Object.is(Button, Button) // true
4Object.is(Button1, Button2) // false
5

Anti-patterns that can cause re-rendering in React.

Don't initialize a component inside the render function.

Copy
1const App = ()=>{   
2    const Component = ()=>{
3        return <h1>Hello World!</h1>
4    }
5    ... 
6    ...
7    return <div>
8        ...
9        <Component />
10    </div>
11}
12

In the above code React will re-render the Component every time the App component re-renders, because the Component is initialized inside the render function. and It's reference will be different in each render.

Copy
1 
2Object.is(Component, Component) // false
3

Cases that React will not re-render a child of a component.

Like we saw above when a component's state changes, React will re-render that component and all the nested components inside it.

But there are some cases that React will not re-render a child of a component.

  1. When the child component passed as a prop to the parent component, and in that way it will not change during child component re-renders. because the type will points to the same reference in the memory before and after rendering.
Copy
1const Child = ({children}) =>{
2    const [count, setCount] = useState(0);
3
4    return <div>
5        <button onClick={()=>setCount(count+1)}>Click Me</button>
6        {children}
7    </div>
8}
9
10const Parent = ()=>{
11    return <Child>
12        <h1>Hello World!</h1>
13    </Child>
14}
15

In that case when React builds the Virtual DOM tree from the Parent component, and when the state changes in the Child Component and React builds the Virtual DOM tree from the Child component, React will not re-render the h1 element. because it is passed as a prop to the Child component, and it will not change during Child component re-renders.

  1. When the child component wrapped with the higher order component React.memo and the props of the child component does not change during re-renders.
Copy
1const Child = React.memo(({title}) =>{
2
3    return <div>
4    {title}
5    </div>
6})
7
8const Parent = ()=>{
9    const [count, setCount] = useState(0);
10
11    return <div>
12        <button onClick={()=>setCount(count+1)}>Click Me</button>
13        <Child title="Hello World!" />
14    </div>
15}
16

in that case when the parent re-renders React will not re-render the Child component, because the props of the Child component is memoized (did't changed) and it's props didn't changed during re-renders.

At the end.

To not Make this article very long, I will write another article about how to deal with re-rendering in React and what techniques you can use to prevent re-rendering in React.

Until then, Happy Coding!