TABLE OF CONTENTS

Learn About React Pure Components, Shallow Comparison And Common Pitfalls

Author Sagi Liba
Sagi Liba on Sep 13, 2020
9 min 🕐

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”:

Copy
let arrayA = ["I Read. You Learn."];

when creating the variable a new identifier is created and given a new memory address with the assigned array value.

Variable in memory example

When creating the same array and storing it in a new variable, the stored array will be given a new memory address.

Copy
let arrayB = ["I Read. You Learn."];

Variable in memory example 2

When you read the variable “arrayB” you immediately know that the variable “arrayB” equals [“I Read. You Learn.”]. You can also look at it as the variable “arrayB” points to a memory address holding our array.

As said before, data structures / reference types are being compared by checking if they have the same memory address.

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

Copy
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);

If you already know the difference between comparing by value and comparing by reference, and how the memory is used behind the scenes, then the short quiz above should not be hard at all.

Here are the answers:

  1. False.

  2. True.

  3. True.

Now you know how shallow comparison actually works.

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.

What Makes a Component Pure

One of the fundamental principals of functional programming is that functions must be pure. For a function to be pure two conditions must be met:

  • Given the same input, always return the same output. It also must not rely on data from the external function state that may change during evaluation.

  • No side effects should occur, by side effects I mean that the function should not inflict any changes beyond its scope, such as modifying a global object or a parameter passed by reference.

Let’s look at a few simple examples, the following code shows a function that increments the variable counter:

Copy
let counter = 0;
function increment() {
return ++counter;
}

As you can see the function updates a value outside its scope, therefore the function is not pure.

Let’s say you were using this function to increment the counter across your application, if a new developer joins the team and is not aware of the use of counter as a global mutating variable, he might decide to set it to a certain value to fit his current needs, and by doing so change the counter you relied on for your calculations.

*  *  *

The following code will call Math.random and Date.now functions several times. For each function invocation, we will get a different return value because these functions relies on time, which is an ever-changing factor, therefore these functions are definitely not pure.

Copy
Math.random(); // 0.01775649548896152
Math.random(); // 0.7830386246112944
Math.random(); // 0.8706670813957647
Date.now(); // 1599846942008
Date.now(); // 1599846942512
Date.now(); // 1599846942944

Let’s look at a simple add function:

Copy
function add(a, b) {
return a + b;
}
let one = 5;
let two = 10;
console.log(add(one, two));

We can be certain that the function is pure because for every two arguments ( a & b ) passed to the function we will always get the same result, and we are not relying on outside data sources that might change over time.

The above examples were meant as an introduction to the concept of pure functions, it should give you a basic understanding of what are pure functions and how to identify them in your code.

Now that you are aware of what is a pure function you can understand why the React’s team decided to call the component a Pure Component. When the props and state are exactly the same we will always get the same value, in case of a component we will always display the same rendered elements.

*  *  *

Let’s see the power of pure components with the following example. Here I’ve created a very simple application, it displays a title text to the user that changes over time.

Copy
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>
);
}
}

The above example will replace the title value at three different times. When using a regular Component the Title component will re-render each time, while using the Pure Component the title will re-render only once because the prop “text” is always the same.

Here are the console prints of the Title Component:

Copy
// 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

Common Pitfalls of Working With Pure Components

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.

Copy
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 mounted component will always display the value “0” to the user even though we have pushed the values 1,2,3 to the numbers variable.

The problem happens because we are pushing the value to the numbers property which is saved in a specific memory address, when the App updates the shouldComponentUpdate which is implemented for us by the PureComponent, will fire and return false because it seems that no changes have been made.

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.

To solve this, all we have to do is create a new array instance which will get a new memory address, when the shallow comparison checks the reference it will see that it points to different memory location and will decide to update our component:

Copy
componentDidMount() {
this.setState({ numbers: [...this.state.numbers,1,2,3] });
}

Avoid Creatings Functions Inside The Render method

One of the most common ways to pass functions to our props is by creating an arrow function inline:

Copy
<Window scrollToStart={() => this.scrollToPosition(0)} />

Creating your functions inside the render method will create a new function each time the component updates, if the function is passed to a Pure Component then we are forcibly re-rendering that component even though the function is always the same.

The following Window component inherits from PureComponent:

Copy
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.

Copy
<Window scrollToStart={this.handleScroll} />

Now that we passed a reference to a function every time the shallow comparison checks if there are any new changes, it will know that the passed function is always the same by comparing the same memory addresses.

Avoid Creating New Data At The Render Function

Let’s look at the following code, we are sorting our data prop and creating a new array using the spread operator:

Copy
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']);
}
...
}

The code above adheres to the principal of immutability from functional programming, which states that you should never mutate an existing value, and instead you should copy and then add the values you need to the new copy.

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.

You can avoid this situation by keeping the data on your state, and whenever new props come along, sort the data again and update the state. by doing so the data being passed to the pure components will always hold the same reference and will only update when the data values actually change.

That’s all I got for you on pure components, I hope this will become another skill in your skillset, and help you face performance issues much better.

© 2020-present Sagi Liba. All Rights Reserved