Skip to main content
Ben Nadel at Endless Sunshine 2017 (Portland, OR) with: Landon Lewis and Brian Blocker
Ben Nadel at Endless Sunshine 2017 (Portland, OR) with: Landon Lewis ( @landonlewis ) Brian Blocker ( @brianblocker )

setState() State Mutation Operation May Be Synchronous In ReactJS

By on

As I've been getting into ReactJS, I've run into a few situations in which it would be convenient to call the setState() component method several times within a single function. This is often to apply different bits of logic to different parts of the state. When doing this, however, it's been unclear in my mind as to what the state will be in between these calls to setState(). So, I decided to dig a little deeper into the mechanics and have found that calls to setState() may or may not be synchronous depending on how the mutation was originally triggered.

Run this demo in my JavaScript Demos project on GitHub.

The ReactJS documentation on the setState() method is a bit confusing. In a big, bold, red warning, it states:

setState() does not immediately mutate this.state but creates a pending state transition. Accessing this.state after calling this method can potentially return the existing value.

These two sentences seem to be in opposition. On the one hand, it says it won't immediately mutate the state; but then, on the other hand, subsequently accessing the state my only "potentially" return the current value. Here, the use of "potentially" conflicts with the statement, "does not immediately mutate."

Digging into the source code (which is not the easiest to understand), it seems that the ability to defer state mutation depends on how the mutation event was triggered. If it was triggered by some event that ReactJS can monitor, such as an onClick props-based handler, then ReactJS can batch the updates, to the state, at the end of the event. If the state change was triggered by a timer or some other developer-orchestrated event handler, however, then ReactJS can't batch the updates and has to mutate the state immediately.

To see this divergence in action, I've created a demo that mutates the state using a single component method. But, I am invoking that method using several different bindings:

<!doctype html>
<html>
<head>
	<meta charset="utf-8" />

	<title>
		setState() State Mutation Operation May Be Synchronous In ReactJS
	</title>

	<link rel="stylesheet" type="text/css" href="./demo.css"></link>
</head>
<body>

	<h1>
		setState() State Mutation Operation May Be Synchronous In ReactJS
	</h1>

	<div id="content">
		<!-- This content will be replaced with the React rendering. -->
	</div>


	<!-- Load scripts. -->
	<script type="text/javascript" src="../../vendor/reactjs/react-0.13.3.js"></script>
	<script type="text/javascript">

		// I manage the Demo widget.
		var Demo = React.createClass({

			// I initialize the state of the component, including instance variables.
			getInitialState: function() {

				return({
					counter: 0
				});

			},


			// ---
			// PUBLIC METHODS.
			// ---


			// I get called once, on the client, when the component has been rendered.
			componentDidMount: function() {

				// Automatically update the state every 3 seconds.
				setInterval( this.updateState, 3000 );

				// Update the state on mouse-down.
				// --
				// NOTE: We are implementing our own event binding here - not using the
				// React Element props to manage the event handler.
				React.findDOMNode( this )
					.addEventListener( "mousedown", this.updateState )
				;

			},


			// I render the component based on the current state.
			render: function() {

				return(
					React.DOM.span(
						{
							onClick: this.updateState,
							className: "button"
						},
						( "Counter at " + this.state.counter )
					)
				);

			},


			// I update the state and log the existing state value on either side of
			// the operation in an attempt to see if the state mutation operation is
			// synchronous or asynchronous.
			updateState: function( event ) {

				console.log( "= = = = = = = = = = = =" );
				console.log( "EVENT:", ( event ? event.type : "timer" ) );
				console.log( "Pre-setState:", this.state.counter );

				this.setState({
					counter: ( this.state.counter + 1 )
				});

				console.log( "Post-setState:", this.state.counter );

			}

		});


		// --------------------------------------------------------------------------- //
		// --------------------------------------------------------------------------- //


		// Render the root Demo and mount it inside the given element.
		React.render(
			React.createElement( Demo ),
			document.getElementById( "content" )
		);

	</script>

</body>
</html>

As you can see, the updateState() method is called in three different ways:

  • setTimeout() - not managed by ReactJS.
  • mousedown - not managed by ReactJS.
  • onClick - managed by ReactJS.

Since only one of the three triggers is managed by ReactJS, only one of the three actions will result in an asynchronous state mutation. And, when I run this code and interact with the page, I get the following output:

setState() timing in ReactJS may update state synchronously or asynchronously.

As you can see, only the onClick trigger had the same state value both before and after the call to setState(). This is because ReactJS was able to batch the state mutation at the end of the event.

Since the actual change to the state may or may not be synchronous, it's best to just assume that it will always be indeterminate and to not reference the state using any assumption one or the other. And, even if you are very confident in how it works, it seems that the whole approach to batching mutations is driven by an injected "strategy component" which may change in the future. So, again, probably best not to make any assumptions one or other other.

Want to use code from this post? Check out the license.

Reader Comments

1 Comments

Thank you for this analysis. I got bit by this on premature re-render having incorrectly concluded setState was always a batched/deferred update.

I believe in love. I believe in compassion. I believe in human rights. I believe that we can afford to give more of these gifts to the world around us because it costs us nothing to be decent and kind and understanding. And, I want you to know that when you land on this site, you are accepted for who you are, no matter how you identify, what truths you live, or whatever kind of goofy shit makes you feel alive! Rock on with your bad self!
Ben Nadel