I made something to play with.
I think the important part to realize, is that there is only one difference, it is the shouldComponentUpdate
implementation on PureComponent
.
I am also confused about Make sure all the children components are also “pure”
. From my understanding, it is meant as a warning, as a guideline, not as an absolute don't. When you put a parent as PureComponent, all the childrens become impacted. But someone not used to shouldComponentUpdate
(PureComponent) may be surprised by childrens behavior (not updating) when mixing both in the tree.
Take for example in the second snippet this.state.quotes[0].text = "StackOverflow save the day";
. When using Component
on both <List/>
and <ListElement/>
, any setState
would update the view. But when using PureComponent
on only <List/>
, someone may expect <ListElement/>
to update, but it won't.
Imagine that, that instead of writing <List/>
as pure, and <ListElement/>
as unpure, what they warn NOT to do. You would have written both as pure. Would you even think of doing something like this.state.quotes[0].text = "StackOverflow save the day";
? No you would start with this.state.quotes[0] = {...}
. And then you would take a look at which component handle the data above, the quotes
array, the component handling the array, is it pure ? Yes it is, then write this.state = [{...}]
instead. You do this "recreating references/deep copy" until you find the first non-pure component. If you followed their advice, at the first non-pure component, you know there won't be any other stale reference blocking your rendering.
If using deep comparison, you would never have that issue. I am using hooks components, they are much more friendly. I recommend you skip class component and use hooks. In hooks to implement shouldComponentUpdate
you use React.memo. Since YOU decide how you compare props, it becomes quite obvious that if you compare previousList === newList
(shallow comparison), you will miss any update/add/delete on the list. And naturally you will do a deep comparison (use deep-equal
lib or (avoid) JSON.stringify(previousList) === JSON.stringify(newList)
. Note that React.Memo
is better for another reason, it only handle props comparison, not both state and props comparison as PureComponent.
I don't think you need every children to use shallow comparison, if the component render fast enough, using shouldComponentUpdate
(or React.memo
) can make it worse (remember it is a performance optimization).
This bellow demonstrate the issues we can get with shallow props comparison (first snippet) and shallow state comparison (second snippet):
import { Component, PureComponent } from 'react';
interface Quote {
author: string;
text: string;
}
export class Playground extends Component {
state = {
quotes: [
{
author: 'Ambroise',
text: 'I like React.'
}
]
}
onClickOK () {
this.setState({
quotes: [
{
author: 'Ariel',
text: 'Prefer VueJS.'
}
]
});
}
// https://reactjs.org/docs/react-api.html#reactpurecomponent
// React.PureComponent implements it with a shallow prop and state comparison
// "prop comparison"
onClickIssue() {
if (window['dontMix']) {
alert("Please don't mix on click issues. Reload page.")
return;
}
window['dontMix'] = true;
// This won't work if <List/> is a PureComponent
// Note that we ALSO CHANGED the props for <ListElement/> here, different object, differents strings,
// but <ListElement/> will NOT re-render, regardless of <ListElement/> being pure or not.
const quotesUpdated = this.state.quotes;
quotesUpdated.pop();
quotesUpdated.push({
author: 'Thomas',
text: 'Prefer Angular.'
});
// Shallow props comparison, <List/> will compare array references
// `quotesUpdated === this.state.quotes` and because it is true,
// it will not re-render, or any of the childrens.
//
// The quote object is different, so <ListElement/>
// would be expected to re-render.
this.setState({
quotes: quotesUpdated
});
}
onClickIssue2() {
if (window['dontMix']) {
alert("Please don't mix on click issues. Reload page.")
return;
}
window['dontMix'] = true;
// This won't work if <ListElement/> is a PureComponent since the object in first index stay the same.
// Since we recreate the array, <List/> will always re-render regardless of <List/> being pure.
this.state.quotes[0].author = "Thomas";
this.state.quotes[0].text = "Prefer Angular.";
this.setState({
quotes: [
this.state.quotes[0],
]
});
}
render() {
return (
<section>
<button onClick={this.onClickOK.bind(this)}>Get updated</button>
<button onClick={this.onClickIssue.bind(this)}>Problem</button>
<button onClick={this.onClickIssue2.bind(this)}>Problem2</button>
<List quotes={this.state.quotes}/>
</section>
);
}
}
// Change PureComponent to Component, no problem with shallow comparison anymore.
class List extends PureComponent<{ quotes: Quote[]; }>{
render() {
return (
<ul>
{
this.props.quotes.map(
(e, i) => <ListElement key={i} quote={e}/>
)
}
</ul>
);
}
}
class ListElement extends PureComponent<{quote: Quote}> {
render() {
return (
<li>
<h3>{this.props.quote.author}</h3>
<p>{this.props.quote.text}</p>
</li>
);
}
}
A second snippet, focusing on the state shallow comparison, and also <ListElement/>
start as a Component
this time instead of a PureComponent
.
import { Component, PureComponent } from 'react';
interface Quote {
author: string;
text: string;
}
export class Playground2 extends Component {
state = {
quotes: [
{
author: 'Ambroise',
text: 'I like React.'
}
]
};
render() {
return (
<section>
<List quotes={this.state.quotes}/>
</section>
);
}
}
// Note: Careful not to confuse state.quotes and props.quote
class List extends PureComponent<{ quotes: Quote[]; }> {
state = {
quotes: this.props.quotes
};
// https://reactjs.org/docs/react-api.html#reactpurecomponent
// React.PureComponent implements it with a shallow prop and state comparison
// "state comparison"
fail() {
for (let i = 0; i < this.state.quotes.length; i++) {
this.state.quotes.pop();
}
this.setState({
quotes: this.state.quotes
});
}
success() {
// This will never work, because `previousState === newState` (both are this.state)
// this.state.quotes = [];
// this.setState(this.state);
this.setState({
quotes: []
});
}
// It work if you change this class to Component
changeChild() {
// if you clicked another button reload the page to get initial state.
if (this.state.quotes.length === 0) {
alert("You clicked another button. Please reload page to test this one.");
}
// previously "I like React."
this.state.quotes[0].text = "StackOverflow save the day";
// NOTICE: Choose only one setState bellow, comment the others.
// Won't work
// this.setState({
// quotes: this.state.quotes
// });
// This will never work, because `previousState === newState` (both are this.state)
// this.state.quotes = [this.state.quotes[0]];
// this.setState(this.state);
// Won't work
this.setState(this.state);
// This will work when <List/> is a PureComponent and <ListElement/> a Component,
// both this.state and this.state.quotes are different
// this.setState({
// quotes: [this.state.quotes[0]]
// });
}
render() {
return (
<div>
<button onClick={this.fail.bind(this)}>Empty the list (FAIL if PureComponent)</button>
<button onClick={this.success.bind(this)}>Empty the list (SUCCESS)</button>
<button onClick={this.changeChild.bind(this)}>Change child (FAIL if PureComponent)</button>
<ul>
{
this.state.quotes.map(
(e, i) => <ListElement key={i} quote={e}/>
)
}
</ul>
</div>
);
}
}
// ATTENTION: This one start as a Component this time, instead of a PureComponent
class ListElement extends Component<{quote: Quote}, {author: string}> {
state = {
author: this.props.quote.author,
};
// Yep this work.
takeOwnership() {
this.setState({
author: "Constantin"
})
}
render() {
return (
<li>
<button onClick={this.takeOwnership.bind(this)}>Take ownership !</button>
<h3>{this.state.author}</h3>
<p>{this.props.quote.text}</p>
</li>
);
}
}
TLDR; I also find it confusing, it seems more like and advice, I recommend not putting too much attention on it, and to use hooks components, where you have more fined grained control, over props/state comparison and shallow/deep comparison.