PureComponent Caveats
October 28, 2018• ☕️ 4 min read
Extending your class components from PureComponents gives you the benefit of avoiding wasteful re-renders. It does a shallow comparison of your new props/state with previous props/state and returns true if a re-render is required; otherwise, returns false. This can be handy if you don’t want to write your own shouldComponentUpdate lifecycle hook.
But, there are a few caveats associated with PureComponents and knowing them can help you write better class components. Before diving into the caveats let’s understand the shallow comparison concept for objects.
Shallow comparison for objects is the process of comparing the references of the objects and returning true if they are equal.
// consider this
let a = { location: {city: ‘Sydney’}};
let b = { location: {city: ‘Sydney’}};
a === b; // false(references are different)
let c = a;
a === c //true (same reference)
// let’s mutate the nested object
a.location.city = ‘Melbourne’;
a === c // true (reference is still the same)
[] === [] // false
{} === {} // false, both are different objects
React always loops through the props object keys and compares the value to check if they are equal or not. In case the key is an object, React won’t do a deep equality check, it will just perform a reference equality check.
Now armed with the knowledge of shallow comparisons, let’s check out the pitfalls of where you may lose the benefit of extending from PureComponent.
1. Using empty or new object as a prop
Consider the following examples:
// Inside the render method of your Parent component
render(){
return <ChildPureComponent data={this.state.data || []} />
}
Passing a new instance of an empty array like this will cause your shallow comparison to return false. An empty array creates a new reference every time our parent re-renders.
To avoid this, consider creating a static reference and passing it as a prop:
// Create a static reference outside of your parent
// component or outside of a parent render function.
const EMPTY_ARRAY = [];
// Then somewhere inside your parent component render method
<ChildPureComponent data={this.state.data || EMPTY_ARRAY} />
Passing a static reference for the array would make your shallow comparison return true and thus would prevent your child component re-rendering. This also applies when passing a new empty object as a prop.
2. Passing an anonymous function as a prop
Functions are nothing but objects in the JS world. A shallow comparison would operate similarly as it does on plain old JavaScript objects.
Consider this:
const a = () => {};
const b = () => {};
a === b; // false
const c = a; // copy the reference of a and assign it to c
a === c // true;
As you can see it works similarly as the POJO (plain old JavaScript object) reference equality check.
Now, when you pass an anonymous function as a prop into your PureComponent, the shallow comparison of that function prop would always fail and thus cause a re-render.
// Inside your Parent component render method
render(){
return <ChildPureComponent
handleClick={() => this.fetchData()} // highlight-line
title=”title”
/>;
}
Our handleClick is always getting a new anonymous function every time your parent component renders. Even though ‘title’ is always the same, your child component would re-render unnecessarily. Shallow comparison would always fail on handleClick prop.
To avoid this wasteful re-render, consider creating an instance method and passing its reference into your handleClick prop.
class ParentComponent extends React.Component {
fetchData = () => {
/* fetch data */
};
render() {
return <ChildPureComponent
handleClick={this.fetchData}
title="title"
/>;
}
}
Now handleClick will always refer to the same instance method, and its value won’t change when your parent re-renders.
3. Handling the case when you need to bind your function with an argument
You might come across a scenario where you need to bind an argument to your function. Especially when you iterate over an Array and create child components via the map function.
// Inside the render method of your parent component
return map(data, el =>
<ChildPureComponent
key={el.id}
handleClick={() => this.fetchData(el.id)}
title=“title”
/>);
This situation can be tricky. One way to solve this problem is to use memoization.
Memoization is an optimization technique of storing the result of a function execution into a cache and returning the cached result when the same input is passed.
// Inside your parent class component
// Instance method
const getClickHandler = memoize(key => () => this.fetchData(key); // highlight-line
render(){
return map(data, el =>
<ChildPureComponent
key={el.id}
handleClick= {this.getClickHandler(el.id)}
title=“title”
/>);
}
Now every time the same input (key) is passed, getClickHandler will return the same reference for your function, which means your ‘handleClick’ prop will pass the shallow comparison check inside your child component.
Depending upon which library you use (or you can implement your own memoization), just be mindful that memoization trades memory for speed.
Hope this was helpful. Happy coding 😃.
Written by Amandeep Singh. Developer @ Avarni Sydney. Tech enthusiast and a pragmatic programmer.