setState(), shouldComponentUpdate(), And render() Timing In ReactJS
The other day, when I was noodling on immutable data in ReactJS, we saw that the shouldComponentUpdate() method provides us with the incoming state (and props) object. But, in another post, we also saw that calls to setState() may or may not be synchronous depending on whether or not the context can be managed by ReactJS' queuing mechanism. So, this got me thinking - how does setState(), shouldComponentUpdate(), and render() interact in a context in which ReactJS cannot queue and flush the state mutation asynchronously.
Run this demo in my JavaScript Demos project on GitHub.
When a call to setState() is made from within something like an onClick handler or an onMouseMove handler, it is being made in the middle of the event-delegation workflow that is managed by ReactJS. As such, ReactJS understands the context and can safely enqueue the state changes and then flush them after the event-delegation is complete.
When a call to setState() is made from within something like a setTimeout() callback, ReactJS doesn't have hooks into the surrounding context. As such, all calls to setState() have to be processed immediately since ReactJS has no way of knowing when it would be safe to dequeue said state changes.
Once the state is changed, ReactJS has to reconcile the change-in-state with the virtual DOM (Document Object Model) and, subsequently, with the rendered DOM. And, if the change in state cannot be queued and flushed asynchronously, it turns out, neither can any of the post-change events.
To see this in action, I've set up a simple demo in which we're making multiples calls to setState() from within two different contexts: a setTimeout() callback and an onClick handler. As these state changes are executed, we can see, from the console-logging, when the shouldComponentUpdate() and render() methods get called:
<!doctype html>
<html>
<head>
<meta charset="utf-8" />
<title>
setState(), shouldComponentUpdate(), and render() Timing In ReactJS
</title>
<link rel="stylesheet" type="text/css" href="./demo.css"></link>
</head>
<body>
<h1>
setState(), shouldComponentUpdate(), and render() Timing 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.min.js"></script>
<script type="text/javascript">
// I manage the root application component.
var Demo = React.createClass({
// I return the initial state of the component.
getInitialState: function() {
return({
status: "Pre-timeout",
count: 0
});
},
// ---
// PUBLIC METHODS.
// ---
// I get called once, on the client, when the component has been rendered
// in the DOM.
componentDidMount: function() {
setTimeout( this.handleTimeout, 1000 );
},
// I handle the click event.
// --
// CAUTION: Since this is powered by the onClick prop of the link element,
// it is managed through ReactJS' event delegation and allows state updates
// to be queued and flushed asynchronously.
handleClick: function( event ) {
console.warn( "handleClick()." );
console.log( "handleClick() - BEFORE setState()." );
var currentCount = this.state.count;
this.setState({
count: ++currentCount
});
this.setState({
count: ++currentCount
});
console.log( "handleClick() - AFTER setState()." );
},
// I handle the timeout event (callback).
// --
// CAUTION: Since this is powered by the setTimeout() method, it cannot be
// managed by ReactJS' event delegation system. As such, the state changes
// cannot be queued and must be applied synchronously.
handleTimeout: function() {
console.warn( "handleTimeout()." );
console.log( "handleTimeout() - BEFORE setState()." );
this.setState({
status: "Post-timeout"
});
this.setState({
count: ( this.state.count + 1 )
})
console.log( "handleTimeout() - AFTER setState()." );
},
// I return the virtual DOM based on the current component state.
render: function() {
console.log( "render()." );
return(
React.DOM.div(
null,
React.DOM.p(
null,
React.DOM.strong( null, "Status:" ),
" ",
this.state.status
),
React.DOM.p(
null,
React.DOM.strong( null, "Count:" ),
" ",
this.state.count
),
React.DOM.p(
null,
React.DOM.a(
{
onClick: this.handleClick
},
"Increment count"
)
)
)
);
},
// I determine if the virtual DOM should be recalculated based on the
// delta in the incoming state and props collections.
shouldComponentUpdate: function( newProps, newState ) {
console.log( "shouldComponentUpdate()." );
console.log(
". . . shouldComponentUpdate() - Status: new( %s ) vs. current( %s ).",
newState.status,
this.state.status
);
console.log(
". . . shouldComponentUpdate() - Count: new( %s ) vs. current( %s ).",
newState.count,
this.state.count
);
return( true );
}
});
// --------------------------------------------------------------------------- //
// --------------------------------------------------------------------------- //
// Render the root Demo and mount it inside the given element.
React.render(
React.createElement( Demo ),
document.getElementById( "content" )
);
</script>
</body>
</html>
When we run this code, the setTimeout() will execute automatically. Then, if I click on the "Increment count" link to execute the setState() from within the onClick handler, we get the following console output:
As you can see, each individual call to setState(), from within the setTimeout() callback, precipitated a individual call to shouldComponentUpdate() and render(). However, the same workflow, from within the onClick handler, resulted in only a single call to shouldComponentUpdate() and render().
I'm not sure that there's any significant take-away from this exploration other than a better mental model and a stronger understanding of how ReactJS actually reconciles setState() calls with the virtual DOM (and then with the actual DOM). Perhaps, also, an understanding that render() is likely being called more often than you think and that shouldComponentUpdate() may be called mid-workflow.
Want to use code from this post? Check out the license.
Reader Comments
hi
So, what is the solution to this?