If you've used React, then you're going to be familiar with React.Component
. It's probably what you're extending every time you create a new stateful component. Here's a class component that does just that:
App.js
import React from 'react';
export default class Parent extends React.Component {
constructor(props) {
super(props);
this.state = {
message: 'hello',
};
}
render() {
return (
<main>
{this.state.message}
</main>
);
}
}
But there is another lesser-known component class that you can extend called React.PureComponent
. What are the differences between these two classes, and why would you want to use PureComponent
?
As outlined in the React docs:
If your React component’s
render()
function renders the same result given the same props and state, you can useReact.PureComponent
for a performance boost in some cases.
What exactly does that mean? Let's say that we have a child component:
Child.js
import React from 'react';
const Child = props => <div>{ props.message }</div>;
export default Child;
This component receives props from its parent. We want this component to re-render whenever it receives new props. What if its parent is tracking different kinds of state, though? Will the parent component trigger a re-render of the Child component even if Child's received props did not change?
Let's say the parent component looks like this:
Parent.js
import React from 'react';
import Child from './Child';
export default class Parent extends React.Component {
constructor(props) {
super(props);
this.state = {
counter: 0,
message: 'hello',
};
}
render() {
return (
<main>
{this.state.counter}
<br />
<Child message={this.state.message} />
</main>
);
}
}
Parent.js renders a <main>
element that displays two sets of state, counter
and message
. The message
state is passed to the <Child />
component and is rendered there.
Taking a closer look at the state in the constructor
method, the parent is keeping track of our state here:
this.state = {
counter: 0,
message: 'hello'
};
Given these components, if we run yarn start
, we get this result in the viewport:
0
hello
0
is rendered from the Parent component and hello
is rendered from the Child component.
We have both counter
and message
props that are set in Parent.js. Looking at Child.js again, you can see that it's only receiving the message
prop. If the counter
prop changes, we don't want Child.js re-rendering since counter
has nothing to do with Child.js. For a tiny project like this, it may not make much of a difference worrying about re-renders on this level, but unnecessarily re-rendering bunches and bunches of child components could handicap the performance of a larger project.
There are some things we can do to test when our Child component re-renders. This YouTube Video shows that, by putting console.log()
statements in each of our components, we can check to see which ones re-render after certain events.
Let's test this out with our Parent and Child components. First, let's add a console.log()
to our Parent component. Let's also provide a way to alter our counter
state:
import React from 'react';
import Child from './Child';
export default class Parent extends React.Component {
constructor(props) {
super(props);
this.state = {
counter: 0,
message: 'hello',
};
}
handleClick = () => {
this.setState(prevState => ({ counter: prevState.counter + 1 }));
};
render() {
console.log('parent rendered');
return (
<main>
{this.state.counter}
<br />
<Child message={this.state.message} />
<button type='button' onClick={this.handleClick}>Increment</button>
</main>
);
}
}
We've added a handleClick()
method that's attached to a <button>
. This way, we can increment the counter
state, which will trigger a re-render of the Parent component.
If we click the button a few times and check the console, we'll see that the Parent component is re-rendering every time the counter
state is incremented:
parent rendered
parent rendered
parent rendered
...
So far, so good. Let's also add a console.log()
statement to the Child component and check to see if it's also re-rendering every time counter
is incremented:
import React from 'react';
export default class Child extends React.Component {
render() {
console.log('child rendered');
return (
<div>{ this.props.message }</div>
);
}
}
I turned the Child component into a class component simply to add a console.log()
statement inside of it. In the real world, this isn't a good enough reason to make this component a class. Nonetheless, despite the console.log()
statement, this verion of the Child component is basically the same as the stateless component.
Now when we increment in the browser, we get this in the console:
Parent rendered
Child rendered
Parent rendered
Child rendered
...
Given that these console.log()
statements only fire when the render()
methods are called, we can definitively say that changing the counter
state is unnecessarily re-rendering the Child component. This is where React.PureComponent
comes in. If we instead extend the Child component from React.PureComponent
, then we'll stop it from unnecessarily re-rendering:
import React from 'react';
export default class Child extends React.PureComponent {
render() {
console.log('child rendered');
return (
<div>{ this.props.message }</div>
);
}
}
Now when we increment the counter
state, we get this in the console:
parent rendered
child rendered
parent rendered
parent rendered
parent rendered
...
We've successfully prevent unnecessary re-render in the Child component.
The big difference between Component
and PureComponent
is that PureComponent
automatically implements the shouldComponentUpdate()
lifecycle method.
Right in the docs for shouldComponentUpdate()
, it states:
Use
shouldComponentUpdate()
to let React know if a component’s output is not affected by the current change in state or props. The default behavior is to re-render on every state change, and in the vast majority of cases you should rely on the default behavior.
So, there you have it -- as our console.log()
experiment shows, the default behavior of our components is to re-render on every state change. shouldComponentUpdate()
is a checkpoint before this automatic re-rendering, evaluating whether the change in state has anything to do with the component at hand.
Yes, you can implement shouldComponentUpdate()
manually, but PureComponent
does this implicitly, and sometimes it's exactly what you're looking for. You could see how switching dozens of stateful children to extending PureComponent
could prevent lots of unnecessary re-renders.
Sometimes, it won't make much sense to use PureComponent
, even if it does help optimize performance. Given our example above, there wasn't really much of a need to make Child a stateful class component other than to use a console.log()
. If there's no reason for it being a class, then it won't be extending either Component
or PureComponent
, and turning it into a class just to use PureComponent
is unnecessary.
However, if children component are classes and are being passed only some of the props of their parents, then PureComponent
might be worth looking into.