Skip to content

Instantly share code, notes, and snippets.

@justinwoo
Last active August 29, 2015 14:10
Show Gist options
  • Save justinwoo/79b0aed0d90641726b91 to your computer and use it in GitHub Desktop.
Save justinwoo/79b0aed0d90641726b91 to your computer and use it in GitHub Desktop.
shouldComponentUpdate example with a mock PureRenderMixin implementation. jsbin: http://jsbin.com/cobiyi/1/edit?js,output
/*
The default behavior of React is actually not very fast.
This is especially the case when you have a list or table with lots of elements to represent data.
Most of the data may not be changing, but the portions that do will need to get data passed down
from the parent. But since the parent needs to render all the siblings of the changed element,
having React reuse render() results on the siblings that haven't changed is very useful.
Good React Components are functions of only their props and state, and in those cases you will
inevitably end up going through the same render() calls which are unnecessary and they provide the
same output.
Luckily, with shouldComponentUpdate, you can make React skip wasteful extra render() calls. By
comparing your components' previous and current props/state, you can tell React to skip computation
of entire trees' worth of elements and cut down and cost.
But how do you do a performant comparison between props and state? Doing a true deep comparison
between the previous and current props/state might take just as long as it does to just call render()
again on the component. This is when using immutable data can help.
If we can just do comparisons of object references, that's going to be a lot faster than trying
to do deep compares. But this does require discipline on your part -- you're not longer allowed
to change values when you do this. Instead, every time you make a change, you need to invalidate
the object reference by assigning a new object to replace the old object.
I hope you'll find this interactive demo a good introduction to using shouldComponentUpdate and
immutable data. Once you feel comfortable with the ideas presented here, try using the provided
modules in React:
* React.addons.update (react/lib/update) -- immutability helper for making controlled changes to a
data structure
* React.addons.PureRenderMixin (react/lib/ReactComponentWithPureRenderMixin) -- mixin for doing
shallow data comparisons with shouldComponentUpdate for rendering "pure" components
References:
https://facebook.github.io/react/docs/pure-render-mixin.html
https://facebook.github.io/react/docs/update.html
*/
// mocks the behavior in React's shallowEqual util
// see: https://github.com/facebook/react/blob/5d3b12bb3bd6a092cf00ede07b8255a8399c2e58/src/utils/shallowEqual.js
var shallowEquals = function (a, b) {
if (a === b) return true;
for (var key in a) {
if (a[key] !== b[key]) return false;
}
for (var key in b) {
if (a.hasOwnProperty(key) === false) return false;
}
return true;
}
// use react/lib/Object.assign (to be deprecated in 0.13? depends...)
// or Sindre Sorhus's object-assign
// or lodash's _.assign
// or bring in your own polyfill
// or use react/lib/update aka React.addons.update (useful for nested structures)
function megaFakeObjectAssign(target) {
for (var i = 1; i < arguments.length; i++) {
var other = arguments[i];
for (var key in other) {
target[key] = other[key];
}
}
return target;
}
var wastedCycles = 0;
var ListItem = React.createClass({
shouldComponentUpdate: function (nextProps) {
// or just use mixins: [PureRenderMixin]
// var PureComponentMixin = require('react/lib/ReactComponentWithPureRenderMixin')
if (this.props.smartRender) {
return !shallowEquals(this.props, nextProps);
} else {
return true;
}
},
componentWillMount: function () {
this.renderCount = 0;
this.redundantCount = 0;
},
render: function () {
var style = {};
this.renderCount++;
if (this.renderCount > 1 && this.previousValue === this.props.item.value) {
console.log('wasted cycles!');
wastedCycles++;
this.redundantCount++;
style.background = 'rgba(255,0,0,' + this.redundantCount / 20 + ')';
} else {
this.redundantCount = 0;
}
this.previousValue = this.props.item.value;
return (
<li style={style}>{this.props.item.value} rendered {this.renderCount} times ({this.redundantCount} redundant)</li>
);
}
});
var List = React.createClass({
render: function () {
var useSmartRender = this.props.smartRender;
var items = this.props.items.map(function (item) {
return (
<ListItem
key={item.id}
smartRender={useSmartRender}
item={item}
/>
)
});
return (
<ul>
{items}
</ul>
);
}
});
var App = React.createClass({
getInitialState: function () {
var items = [];
while (items.length < 5) {
var value = (Math.random() * 9999999 | 0).toString(36);
items.push({
id: value,
value: value
});
}
return {
items: items
}
},
addItem: function () {
var value = (Math.random() * 9999999 | 0).toString(36);
var newItems = [].concat(this.state.items, {
id: value,
value: value
});
this.setState({
items: newItems
});
},
removeLast: function () {
var newItems = this.state.items.filter(function (a, i, items) {
return i + 1 !== items.length;
});
this.setState({
items: newItems
});
},
changeLast: function () {
var newItems = this.state.items.map(function (item, i, items) {
if (i + 1 !== items.length) {
return item;
} else {
var newValue = (Math.random() * 9999999 | 0).toString(36);
return megaFakeObjectAssign({}, item, {
value: newValue
});
}
});
this.setState({
items: newItems
});
},
shuffle: function () {
var newItems = this.state.items.slice();
for (var i = 0; i < newItems.length; i++) {
var swap = Math.random() * newItems.length | 0;
var temp = newItems[i];
newItems[i] = newItems[swap];
newItems[swap] = temp;
}
this.setState({
items: newItems
});
},
mutateElements: function () {
var newItems = this.state.items.slice();
for (var i = 0; i < newItems.length; i++) {
var newValue = (Math.random() * 9999999 | 0).toString(36);
newItems[i].value = newValue;
}
this.setState({
items: newItems
});
},
updateElements: function () {
var newItems = this.state.items.slice();
for (var i = 0; i < newItems.length; i++) {
var newValue = (Math.random() * 9999999 | 0).toString(36);
newItems[i] = megaFakeObjectAssign({}, newItems[i], {
value: newValue
});
}
this.setState({
items: newItems
});
},
render: function () {
var buttonStyle = {
margin: '0 5px',
padding: 5
};
return (
<div>
<h3>Wasted cycles: {wastedCycles}</h3>
<p>
<h4>Common operations</h4>
<button style={buttonStyle} onClick={this.addItem}>Add item</button>
<button style={buttonStyle} onClick={this.removeLast}>Remove Last</button>
<button style={buttonStyle} onClick={this.changeLast}>Change Last</button>
<button style={buttonStyle} onClick={this.shuffle}>Shuffle</button>
</p>
<p>
<h4>Less/more common operations (based on your application)</h4>
<button style={buttonStyle} onClick={this.mutateElements}>Mutate elements in place</button>
<button style={buttonStyle} onClick={this.updateElements}>Update elements with new Objects</button>
</p>
<div style={{position: 'absolute', left: 10}}>
<h3>Smart rendering</h3>
<List
items={this.state.items}
smartRender={true}
/>
</div>
<div style={{position: 'absolute', left: 360}}>
<h3>Default Rendering</h3>
<List
items={this.state.items}
smartRender={false}
/>
</div>
</div>
);
}
});
document.addEventListener("DOMContentLoaded", function(event) {
React.render(
<App/>,
document.body
);
});
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment