DEV Community

loading...
Cover image for Avoid Reconciliation using shouldComponentUpdate()

Avoid Reconciliation using shouldComponentUpdate()

ip127001 profile image Rohit Kumawat Updated on ・6 min read

There are many performance optimization concepts in react which helps us to build faster applications.

Today I am going to discuss one of most discussed concept in performance optimization: how to avoid Reconciliation

Table Of Content

What is Reconciliation?

  1. React maintains the internal representation of UI by creating a tree like structure of every DOM object in memory which is called Virtual DOM:

Basically it is just a javascript object which keeps information of every DOM object.

[Imagine virtual DOM as a javascript object which keeps information about every element]

{
    node: 'html',
    properties: {
        attributes: [],
        classes: [],
        events: []
    },
    children: {
        node: 'body'
        properties: {
            attributes: [],
            classes: [],
            events: []
        },
        children: [
            {        
                node: 'div',
                properties: {
                    attributes: [],
                    classes: [],
                    events: []
                },

            },
            {
                node: 'button',
                properties: {
                    attributes: [],
                    classes: [],
                    events: []
                }
            }
        ]
    }
}
Enter fullscreen mode Exit fullscreen mode
  1. Whenever we update the state or props changes:
    • Component returns the elements.
    • React compares the newly returned elements with the previously rendered ones by comparing virtual DOM snapshot of new object with last updated object. This way React has to update only changed nodes in React DOM.

This whole process is how React updates DOM which is called Reconciliation.
This process is way faster than Real DOM manipulation.

Even though React is clever enough to update only changed nodes. But when props and state changes, re-rendering takes place which takes some time.

So we need to avoid unnecessary re-rendering for such cases.


The Case where we need to avoid Reconciliation or stop the re-rendering process:

When parent component renders, all its child components are re-rendered even if their props or states didn't change.
And if child component contains a very slow computation then, it will be computed in every re-render.

[Our Case Example-1 ]

The parent component contains:

  • button element which changes the counter value.
  • A child component which is dependent of parent prop called color which is changed by selecting a color from select element.

In child component:

  • There is a delay function which replicates a time taking computation.

Even if you change the counter value using button element in Parent, the child component re-renders hence the computation along with it takes place which is quite noticable in example below.

Parent Component:

import React from 'react';
import Child from './Child';

class Parent extends React.Component {
  constructor() {
    super();
    this.state = {
      counter: 0,
      color: 'red'
    }
  }

  render() {
    console.log('[Parent] rendered');

    return (
      <div className="container">
          <div>Counter:  {this.state.counter}</div>

          <button onClick={() => this.setState({counter: this.state.counter + 1})}>Click me to change counter</button>

          <select defaultValue="red" onChange={(e) => this.setState({color: e.target.value})}>
            <option value="red">red</option>
            <option value="blue">blue</option>
            <option value="grey">grey</option>
          </select>

          <Child color={this.state.color} />
      </div>
    )
  }
}

export default Parent;
Enter fullscreen mode Exit fullscreen mode

Child Component:

import React from 'react';

class Child extends React.Component {
    delay() {
        console.log('[Delay] function called');

        for(let i = 0; i < 5000000000; i++) {
            i++;
        }
        return 'delayed text';
    }

    render() {
        console.log('[Child] rendered');

        return (
            <div className="child">
                <hr />
                <div>{this.delay()}</div>
                Selected Color: {this.props.color}
            </div>
        )
    }
}

export default Child;
Enter fullscreen mode Exit fullscreen mode

That's where shouldComponentUpdate() comes in. It is the lifecycle method which is called before re-rendering starts in class based components.

1. How shouldComponentUpdate() works:

shouldComponentUpdate(nextProps, nextState)
Enter fullscreen mode Exit fullscreen mode
  • Invoked before re-rendering.
  • It receives updated prop object and state.
  • Returns true by default which means component will re-render by default.
  • we can compare previous prop and state changes with nextProps and nextState respectively and return false if we don't want to re-render.
  • If we retun false then UNSAFE_componentWillUpdate(), render(), and componentDidUpdate() will not be invoked - Hence no re-rendering.
  • This lifecycle method can be skipped when used forceUpdate().

[Example:]

/* Assume props = {color: 'red'} state={counter: 0} */

class Parent extends React.Component {
shouldComponentUpdate(nextProps, nextState) {
  if(this.nextProps.color === this.props.color) {
    return false;
  }
  if(this.nextState.counter === this.state.counter) {
    return false;
  }
  return true;
}
Enter fullscreen mode Exit fullscreen mode

so if the color prop and counter variable in state doesn't change then this component won't re-render.

Optimizing performance of above example:

Example-1

Now we use shouldComponentUpdate() to compare the props and return false if props doesn't changes.

Child Component with shouldComponentUpdate:

import React from "react";

class Child extends React.Component {
  delay() {
    console.log("[Delay] function called");

    for (let i = 0; i < 800000000; i++) {
      i++;
    }
    return "delayed text";
  }

  shouldComponentUpdate(nextProps, nextState) {
    if (this.props.color === nextProps.color) {
      return false;
    } else {
      return true;
    }
  }

  render() {
    console.log("[Child] rendered");

    return (
      <div className="child">
        <hr />
        <div>{this.delay()}</div>
        Selected Color: {this.props.color}
      </div>
    );
  }
}

export default Child;
Enter fullscreen mode Exit fullscreen mode

Live Example:

I have added the codesandbox urls below for both state of application before optimization and after optimization with shouldComponentUpdate().

In the example I have also added consoles so that we can observe which component are rendered and re-rendered.

  1. Without any performance optimization:

2.With shouldComponentUpdate():


2. React.PureComponent:

React also provides React.PureComponent which does shallow comparision of props and state to skip the re-rendering.

Using this we don't have to write shouldComponentUpdate() manually, PureComponent internally manages it.

Example-1: our case when delay() function should be skipped if prop or state does not changes. We can achieve it using PureComponent.

import React from "react";

class Child extends React.PureComponent {
  delay() {
    console.log("[Delay] function called");

    for (let i = 0; i < 800000000; i++) {
      i++;
    }
    return "delayed text";
  }

  render() {
    console.log("[Child] rendered");

    return (
      <div className="child">
        <hr />
        <div>{this.delay()}</div>
        Selected Color: {this.props.color}
      </div>
    );
  }
}

export default Child;
Enter fullscreen mode Exit fullscreen mode

It is much clearer code and does the work if shallow comparision works fine.


But it does not work if there is complex data structure or state is mutated in any way [Like exmaple below]:

  • Parent component use select to push selected colors to array.
  • child component receives the color prop as array of colors.

Parent Component:

import React from "react";
import Child from "./Child";

class Parent extends React.Component {
  constructor() {
    super();
    this.state = {
      color: ["red"]
    };
  }

  render() {
    return (
      <div className="container">
        <select
          onChange={(e) => {
            console.log(this.state.color);
            let updatedColor = this.state.color;
            if (Array.isArray(this.state.color)) {
              updatedColor.push(e.target.value);
            }
            return this.setState({ color: updatedColor });
          }}
        >
          <option value="red">red</option>
          <option value="blue">blue</option>
          <option value="grey">grey</option>
        </select>

        <Child color={this.state.color} />
      </div>
    );
  }
}

export default Parent;

Enter fullscreen mode Exit fullscreen mode

Child Component:

import React from "react";

class Child extends React.PureComponent {
  render() {
    return (
      <div className="child">
        <hr />
        Selected Color:
        <ul>
          {this.props.color.map((el, index) => {
            return <li key={index}>{el}</li>;
          })}
        </ul>
      </div>
    );
  }
}

export default Child;
Enter fullscreen mode Exit fullscreen mode

Here child component doesn't re-render even if you change the values in colors array.

Array is a reference type data struture in javascript and because PureComponent only does shallow comparision and both references are same each time.

To avoid this we can avoid mutation and send new array or objects each time.

Live codesandbox url of PureComponent example where it state is mutated:

  • Please check the console here: as we select any color it adds it to the state array but doesn't re-render the child component.


3 React.memo:

  • It is a higher order component.
  • It is similar to React.PureComponent but for function components instead of classes.
  • But only shallow compares the props not state as there is no single state object to compare.

Example-1: our case when delay() function should be skipped if prop or state does not changes. We can achieve it using memo().

import React from "react";

function Child (props) {
    function delay() {
        for (let i = 0; i < 800000000; i++) {
            i++;
        }
        return "delayed text";
    }

    return (
    <div className="child">
        <hr />
        <div>{delay()}</div>
        Selected Color: {props.color}
    </div>
    );
}

export default React.memo(Child);
Enter fullscreen mode Exit fullscreen mode

just wrap your component in React.memo() and it does shallow comparision on props.


Note by React docs: All the above methods only exists as a performance optimization. Do not rely on it to prevent a render, as this can lead to bugs

References:
React docs


Thanks for reading article!
Feedback is more than welcome.

If you any questions, please comment here or ask me on twitter

Discussion (0)

pic
Editor guide