Learn About React Pure Components, Shallow Comparison And Common Pitfalls

by | Sep 13, 2020 | 0 comments

When creating a high performing application we must consider how to avoid re-rendering our components unnecessarily. Here we will explore how to use React’s Pure Component to avoid re-rendering by truly understanding what is a pure component. We will go over how shallow comparison works, what are pure functions, and what are some common pitfalls you might encounter.

Before React added pure components, to avoid re-rendering we needed to manually implement the shouldComponentUpdate life cycle method, and check if the new props & state are equal to the old props & state values, if so then we would not need to re-render the component.

React Pure Component helps us automatically avoid re-rendering by implementing the shouldComponentUpdate life cycle method for us, and doing a shallow state and props comparison.

 

HOW SHALLOW COMPARISON WORKS

Shallow comparison works by comparing primitive types by their value, and data structure types / reference types by comparing if they reference the same memory address.

Checking by reference won’t check the nested values inside.

This is what happens when storing a new value inside a variable:

  • Create a new unique identifier for the variable.
  • Allocate an address in memory for the new value.
  • Store the value inside that memory address.

Here I’ve stored an array inside a variable named “arrayA”:

let arrayA = ["I Read. You Learn."];
memory-example
let arrayB = ["I Read. You Learn."];
memory-example-2

After the above explanation, here are a few questions to check your understanding:

let a = [0];
let b = [0];

console.log("Test 1:", a === b);
// --------------------------------------
let c = [4];
let d = c;
c.push(5);

console.log("Test 2:", c === d);
// --------------------------------------
let e = { name: "Sagi" };
let c = e;

e.name = "Liba";

console.log("Test 3:", c === e);

React Pure Component uses a shallow comparison instead of a deep comparison because it is much much faster. By using pure components you can easily check if the props and state were meaningfully changed.

let counter = 0;

function increment(){
  return ++counter;
}

*     *     *

Math.random() // 0.01775649548896152
Math.random() // 0.7830386246112944
Math.random() // 0.8706670813957647

Date.now() // 1599846942008
Date.now() // 1599846942512
Date.now() // 1599846942944
function add(a,b){
  return a + b;
}

let one = 5;
let two = 10;

console.log(add(one,two));
import React from "react";

class Title extends React.PureComponent {
  render() {
    console.log("Render - ", this.props.text);
    return <h2>{this.props.text}</h2>;
  }
}

export default class App extends React.Component {
  constructor(props) {
    super(props);
    this.state = {
      text: "React Pure Component - IRYL"
    };
  }

  componentDidMount() {
    const text = "New Title";
    setTimeout(() => this.setState({ text }), 1500);
    setTimeout(() => this.setState({ text }), 3000);
    setTimeout(() => this.setState({ text }), 6000);
  }


  render() {
    return (
      <div className="App">
        <Title text={this.state.text} />
      </div>
    );
  }
}
// Results of Title component being a Pure Component.
Render -  React Pure Component - IRYL 
Render -  New Title 

// Results of Title component being a regular Component.
Render -  React Pure Component - IRYL 
Render -  New Title 
Render -  New Title 
Render -  New Title 

Avoid updating references

Let’s look at an example where shallow comparison can fail

Here we have another simple application like the previous one, where we display a value to the user. the application has a numbers array on its state and we try to update it when the component mounts.

Side Note: this is not how I would normally update a states value, but it is important as an example of specific pitfalls you will encounter.

Notice that our App component is now a Pure Component.

import React from "react";

const Title = ({text}) => <h2>{text}</h2>;
// Notice that App is a Pure Component now.
export default class App extends React.PureComponent {

  state = {
    numbers: [0]
  };

  componentDidMount() {
    let {numbers} = this.state;
    numbers.push(1);
    numbers.push(2);
    numbers.push(3);
    this.setState({ numbers });
  }

  render() {
    return (
      <div className="App">
        <Title text={this.state.numbers} />
      </div>
    );
  }
}

The shallow comparison failed us because it checked if the “numbers” array memory address is equal to the newly passed value memory address, which it is, instead of checking the arrays actual passed values.

componentDidMount() {
  this.setState({ numbers: [...this.state.numbers,1,2,3] });
}
<Window scrollToStart={() => this.scrollToPosition(0)} />
class Window extends PureComponent {
  ...
  componentDidMount() {
    this.props.scrollToStart()
  }
  ...
}

We can easily stop re-creating functions and re-rendering the Window component by passing a reference to a function that will handle our scrolling, instead of creating a new function for each render.

<Window scrollToStart={this.handleScroll} />
class Window extends PureComponent {
  ...
  render() {
    const {data} = this.props;
    // creating a new array in memory when using the spread operator:
    const sortedData = _.sortBy([...data],['time']);
  }
  ...
}

While the principal of immutability can help us avoid bugs, by creating the new data array in the render method, each time the component updates the state you will cause your pure child component to re-render needlessly.

IF YOU GOT ANY VALUE SHARE 😄