shouldComponentUpdate() Will Short-Circuit An Entire Subtree Of Components In ReactJS
ReactJS is a very fast rendering engine. It's even a bit faster than AngularJS when it comes to rendering larger data sets. But, that doesn't mean performance is never a concern. As such, ReactJS provides a .shouldComponentUpdate() life-cycle method for each component. By default, this method returns true (ie, should update). But, if you want to prevent the virtual DOM (Document Object Model) from being recalculated, you can override this method and return false. What might not be obvious, however, is that doing so will short-circuit the update for the entire sub-tree of components, not just the current component.
Run this demo in my JavaScript Demos project on GitHub.
Demonstrating this is fairly easy. To do so, I've set up a demo that has two instances of a Tree component - one that is told to allow updates and one that is told to prevent updates. Each Tree component renders a recursive set of SubTree components that define their virtual DOM based on dynamic data. As we update the dynamic data, we can see if and how each subtree of components update to reflect the new data.
<!doctype html>
<html>
<head>
<meta charset="utf-8" />
<title>
shouldComponentUpdate() Will Short-Circuit An Entire Subtree Of Components In ReactJS
</title>
<link rel="stylesheet" type="text/css" href="./demo.css"></link>
</head>
<body>
<h1>
shouldComponentUpdate() Will Short-Circuit An Entire Subtree Of Components In ReactJS
</h1>
<div id="content">
<!-- App will be rendered here. -->
</div>
<!-- Load scripts. -->
<script type="text/javascript" src="../../vendor/reactjs/react-0.13.3.js"></script>
<script type="text/javascript" src="../../vendor/reactjs/JSXTransformer-0.13.3.js"></script>
<script type="text/jsx">
// I render the demo with two trees - one of which employs updates, the other of
// which does not. This will allow us to see how changes in values are propagated
// down through different sub-trees with different update settings.
var Demo = React.createClass({
// I return the initial state of the component.
getInitialState: function() {
return({
count: 0
});
},
// ---
// PUBLIC METHODS.
// ---
// I handle a click to update the count.
handleCountClick: function( event ) {
this.setState({
count: ( this.state.count + 1 )
});
},
// I return the virtual DOM based on the current component state.
// --
// NOTE: One of the trees has update=TRUE, the other has update=FALSE.
render: function() {
return(
<div>
<p>
<a onClick={ this.handleCountClick }>Increase Count</a>
</p>
<Tree update={ true } count={ this.state.count } />
<Tree update={ false } count={ this.state.count } />
</div>
);
}
});
// --------------------------------------------------------------------------- //
// --------------------------------------------------------------------------- //
// I output a tree of nested nodes. However, after the first render, any
// subsequent requests to render may be short-circuited by the "update" prop
// that is passed-in. If the update prop is set to False, the rendering will
// be prevented at the top of the tree, thereby preventing all the nested
// SubTree nodes from rendering as well.
var Tree = React.createClass({
// I return the virtual DOM based on the current component state.
render: function() {
return(
<div className="tree">
<strong>
Will Update: { this.props.update ? "True" : "False" }
</strong>
<SubTree
level={ 1 }
count={ this.props.count }>
</SubTree>
</div>
);
},
// I determine if the virtual DOM should be recalculated.
// --
// NOTE: If I return false, recalculation for the entire subtree of this
// component will be halted / short-circuited.
shouldComponentUpdate: function( newProps, newState ) {
return( this.props.update );
}
});
// --------------------------------------------------------------------------- //
// --------------------------------------------------------------------------- //
// I recursively render a set of nested nodes, each of which outputs the count
// value that is passed-in.
// --
// NOTE: This React class doesn't use the .shouldComponentUpdate() method. The
// point here is to demonstrate that this node will not update if its parent node
// is not going to update.
var SubTree = React.createClass({
// I return the virtual DOM based on the current component state.
render: function() {
// If we've reached our max-depth, stop recursing.
if ( this.props.level === 5 ) {
return( null );
}
return(
<div className="tree-node">
Count: { this.props.count }
<SubTree
level={ this.props.level + 1 }
count={ this.props.count }>
</SubTree>
</div>
);
}
});
// --------------------------------------------------------------------------- //
// --------------------------------------------------------------------------- //
// Render the root Demo and mount it inside the given element.
React.render( <Demo />, document.getElementById( "content" ) );
</script>
</body>
</html>
Other than the update={false}, everything about the two Trees is the same. However, when we update the root-level count value, only one of the DOM trees is updated:
As you can see, while the SubTree components do not explicitly prevent updates, the override of .shouldComponentUpdate(), in the top-level Tree component, is enough to prevent the entire subtree of components from recalculating their virtual DOMs.
How Immutable Data Dove-Tails With .shouldComponentUpdate()
In my post yesterday, I talked about how immutable is consumed in ReactJS. While it may have had a negative undertone, it was not a post against immutable data; rather, it was a post in favor of understanding it holistically. ReactJS doesn't care about immutable data - it only cares about the virtual DOM. It's up to you, as the developer, to tie immutable data back to the virtual DOM. And, the .shouldComponentUpdate() method is one such opportunity.
Consider, for a moment, a two-dimensional array that is used to render a data table. If the array is mutable, it means that any value at any indicie can be altred without changing the top-level array reference. From a ReactJS standpoint, this means that we have to recalculate the entire virtual DOM associated with the table because any element within that table may need to be re-rendered.
But, if the two-dimensional array were treated as immutable, it's a whole other story. With immutable data, you can't change arbitrary values - you have to create a completely new two-dimensional array that aggregates both the old and the new values. This means that you don't have to dig into the array to see if the values have changed - you simply need to compare the current array reference to the previous array reference. If the references are the same, you can be confident that the data has not changed. And, if they are different, you can be confident that at least one value within the array has changed.
This is very powerful. Circling back to the .shouldComponentUpdate() method, this concept of immutable data means that we can quickly and accurately short-circuit rendering for an entire subtree of components based on nothing more than variable references. The .shouldComponentUpdate() method is provided with the "new state" and the "new props" objects. With immutable data, you can compare something in the "new state" with something in this.state (ie, the "old state") and, if the references are the same, you can return(false), thereby preventing the entire subtree of components from having to recalculate the virtual DOM. And, depending on the size and complexity of the data, this can be a win.
In AngularJS, there's no way to terminate the digest at an arbitrary location within the Scope tree; so, it wasn't immediately obvious to me that updates, in ReactJS, could be short-circuited. It's a very interesting feature of ReactJS. And, combined with immutable data, it seems like it could be quite powerful.
Want to use code from this post? Check out the license.
Reader Comments
React in its Advance performance sections talks about about shouldComponentUpdate function.
It allows us to prevent component render when return false.
With react redux where container components subscribe for a store update, we generally define shouldComponentUpdate to avoid rerender if not required.
So my query is isn't it the same thing what angular watchers does automatically. Digest cycle goes through all watchers and compare the expressions and render changes only if required.
We used to say digest loop is slow.