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."];
when creating the variable a new identifier is created and given a new memory address with the assigned array value.
When creating the same array and storing it in a new variable, the stored array will be given a new memory address.
let arrayB = ["I Read. You Learn."];
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:
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:
False.
True.
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:
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.
Math.random(); // 0.01775649548896152Math.random(); // 0.7830386246112944Math.random(); // 0.8706670813957647Date.now(); // 1599846942008Date.now(); // 1599846942512Date.now(); // 1599846942944
Letâs look at a simple add function:
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.
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:
// Results of Title component being a Pure Component.Render - React Pure Component - IRYLRender - New Title// Results of Title component being a regular Component.Render - React Pure Component - IRYLRender - New TitleRender - New TitleRender - 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.
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:
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:
<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:
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} />
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:
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.