Pure component caveats
When used appropriately, pure components can boost your application's performance by avoiding useless renders. A pure component performs a shallow comparison of its props
and state
against their previous values and skips the re-render if nothing has (shallowly) changed.
This shallow comparison is reasonably fast, and definitely faster than a re-render. But it's still a cost, and in some cases, you may find that some prop has surprisingly changed even though it didn't look like it. The pure component does the shallow comparison, and then re-renders anyways, and you get the worst of both worlds.
You're mostly sheltered if your component receives simple props such as strings, booleans, or numbers. Things get trickier when there are functions, objects, arrays and React elements involved.
Here are some cases I've stumbled upon that negate the benefit of using a pure component:
Sending functions (callbacks) to a component
When the component receives a function as a prop, make sure its parent is not constantly sending new functions, as can happen with defining anonymous functions or bind
-ing functions in-place. Let's say we have the following components:
class Snow extends React.PureComponent {
render() {
<div className="snow" onClick={this.props.onClick} />;
}
}
But then the parent component does:
class SnowGlobe extends React.Component {
render() {
return (
<Snow
onClick={
e => console.log(e);
}
/>
);
}
}
Here, because we create an anonymous function inside SnowGlobe
's render()
method, our Snow
component will get a fresh new function each time SnowGlobe
re-renders, and will itself re-render. Same with bind
-ing the function in-place:
class SnowGlobe extends React.Component {
log(e) {
console.log(e);
}
render() {
return <Snow onClick={log.bind(this)} />;
}
}
For a solution to this, see the property
pattern for callbacks.
Sending React elements
When you send React elements on any prop, including children
, will cause a re-render in the pure component. That's because JSX resolves to JavaScript and when you're doing:
const App = props => (
<PureComponent>
<h1>So pure</h1>
</PureComponent>
);
PureComponent
will receive this as the children
prop:
{
type: 'h1',
props: {
children: 'So pure'
}
}
And these JavaScript objects are not equal by reference, even though they have the same content.
Having children
It's not just React elements as children
that cause problems. Any time you have more than one child to a component, it will cause the component to re-render each time. For example:
const App = props => <PureComponent>Sum: {1 + 1}</PureComponent>;
Here, the children
prop is ['Sum: ', 1 + 1]
, and arrays, much like objects, are not shallowly equal even if they contain the same elements. And again, the pure component re-renders.
You're not sending what you think you're sending
Let's assume you have a list and a little piece of UI that is shown when there are no items to display:
class List extends React.Component {
render() {
return (
<div className="list">
<ul>
{this.props.items.map(item => (
<li>...</li>
))}
</ul>
<PureMessage hidden={this.props.items.length} />
</div>
);
}
}
everything works as expected and PureMessage
only shows up when there are no items, but inspecting things with React Dev Tools you notice it's re-rendering more often than expected. What gives?
You're sending a number to the hidden
prop, even though it looks like it should be a boolean. But everything worked, and you hadn't noticed. It's just that PureMessage
silently got re-rendered each time an item was added to the list.
The easiest way to fix this is to add the prop to the propTypes
definition, and React will let you know you're setting a value of the wrong type to the prop.
Other inferred booleans spotted in the wild:
// when `enabled = true`, resolves to the length of the array.
let should_display = enabled && arr.length;