On the async nature of `setState` in React

All right, this one's going to be a longer post. So there's only a couple of words to say to this...

React Meme

Bottom line - I can't withstand a good GOT meme. Back to the article now. Spoiler alert: it does include code.

The state of setState

setState is probably the most React-ish thing you are using right now. Its fame and high usage can be attributed to the fact that it is the recommended way of updating your DOM in React applications (directly manipulating the state is a bad idea). Don't go outside of React world (more on this later) and you're safe.

Quoting someone that helped writing the React docs (and that hopefully works for Facebook or worked at that time):

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.

And:

There is no guarantee of synchronous operation of calls to setState and calls may be batched for performance gains.

Sounds about right. In short, looks like setState is asynchronous. React tries to be clever under the hood, and, based on its updating strategies, it will batch multiple calls so it can squeeze as much perf as it can.

When I first read the React docs, I didn't pay too much attention to the way setState is explained. It seemed obvious to me it's async so I moved on. However, it took some failing tests from a TDD workshop I'm currently working on (more on this in a later blog post) and a very smart guy (@BenNadel) to make me question how this method works under the hood.

Ben pointed out a very simple thing. If setState is truly async, how come it can only potentially return the existing value. It should always return the existing value! Take the following code for example:


class Test extends React.Component {
  constructor(props) {
    super(props);
    this.state = {
      counter: 0
    };
  }

  render() {
    return (
      <button onClick={ this._btnClickHandler.bind(this) }>
        Click me
      </button>
    );
  }

  _btnClickHandler() {
    this.setState({ counter: 1 }); // this is async - takes a while
    console.log(this.state.counter); // should log 0
  }
}

When you log this.state.counter in _btnClickHandler, it should always be 0. Not potentially. If you don't understand why, stop now and go learn JavaScript. Logical conclusion: immediately after you call setState, if indeed it's async, you should still be operating on the old state.

Ok, but as it turns out, this is not exactly true...

How setState actually works

TL;DR - I made this repo on GitHub where I'm testing different scenarios (React known events, events added via addEventListener, setTimeout and AJAX calls). Open up the console and be amazed. Turns out setState is either sync or async, depending on the context.

Now consider the following code (yeah, it's a lot):


var TestComponent = React.createClass({
  displayName: 'TestComponent',

  getInitialState: function getInitialState() {
    return {
      dollars: 10
    };
  },

  componentDidMount: function componentDidMount() {
    // Add custom event via `addEventListener`
    //
    // The list of supported React events does include `mouseleave`
    // via `onMouseLeave` prop
    //
    // However, we are not adding the event the `React way` - this will have
    // effects on how state mutates
    //
    // Check the list here - https://facebook.github.io/react/docs/events.html
    this.refs.btn.addEventListener('mouseleave', this._onMouseLeaveHandler);

    // Add JS timeout
    //
    // Again,outside React `world` - this will also have effects on how state
    // mutates
    setTimeout(this._onTimeoutHandler, 10000);

    // Make AJAX request
    superagent
      .get('https://api.github.com/users')
      .end(this._onAjaxCallback);
  },

  render: function render() {
    console.log('State in render: ' + JSON.stringify(this.state));

    return React.createElement(
      'button',
      {
        ref: 'btn',
        onClick: this._onClickHandler
      },
      'Click me'
    );
  },

  _onClickHandler: function _onClickHandler() {
    console.log('State before (_onClickHandler): ' + JSON.stringify(this.state));
    this.setState({
      dollars: this.state.dollars + 10
    });
    console.log('State after (_onClickHandler): ' + JSON.stringify(this.state));
  },

  _onMouseLeaveHandler: function _onMouseLeaveHandler() {
    console.log('State before (mouseleave): ' + JSON.stringify(this.state));
    this.setState({
      dollars: this.state.dollars + 20
    });
    console.log('State after (mouseleave): ' + JSON.stringify(this.state));
  },

  _onTimeoutHandler: function _onTimeoutHandler() {
    console.log('State before (timeout): ' + JSON.stringify(this.state));
    this.setState({
      dollars: this.state.dollars + 30
    });
    console.log('State after (timeout): ' + JSON.stringify(this.state));
  },

  _onAjaxCallback: function _onAjaxCallback(err, res) {
    if (err) {
      console.log('Error in AJAX call: ' + JSON.stringify(err));
      return;
    }

    console.log('State before (AJAX call): ' + JSON.stringify(this.state));
    this.setState({
      dollars: this.state.dollars + 40
    });
    console.log('State after (AJAX call): ' + JSON.stringify(this.state));
  }
});


// Render to DOM
ReactDOM.render(
  React.createElement(TestComponent),
  document.getElementById('app')
);

Nothing too fancy, just a simple component. I'm not using JSX for simplicity's sake. And superagent is used for AJAX calls. Check it out here if you're not familiar with it.

Ok, let's take the first example. Probably one of the most common ones. Adding an event listener via React known events that will change the state (this can potentially do more complicated stuff like dispatching an action in a Redux architecture, etc.)

The _onClickHandler method logs the state, calls setState which increases the cash amount and then logs the state again. In this case, setState is asynchronous as you would expect. Looking at the console, we should get something like this (values may differ, depending on how many actions you did):

State example one

Note that render was actually batched after the second log. And the state was the same before and after setState call. Logical conclusion: async. Thanks, Captain Obvious. But it's important to understand why React is doing this. We're updating the state in a callback function for a context that React understands - the onClick handler (which is a convention made by React that knows how to deal with the click event in JavaScript). The fact that the state manipulation happens inside a method that React is aware of, puts the library in control. Thus, it is able to employ a different updating strategy by batching multiple calls to setState in order to squeeze as much perf as it can. Everything lives in the React world.

However, if you inspect the other logs, you'll see something like this:

State example two

So what's happening here? You can see that in every situation (addEventListener, setTimeout or AJAX call) the state before and the state after are different. And that render was called immediately after triggering the setState method. But why is that? Well, it turns out React does not understand and thus cannot control code that doesn't live inside the library. Timeouts or AJAX calls for example, are developer authored code that executes outside of the context of React.

Got another one for you. Especially for those of you who worked with Angular. Remember the need to call $scope.$apply() in AJAX callbacks or when doing third party plugins integration? Values were not being updated otherwise, because that code was being executed outside of Angular's world and the framework was not aware of it. It's the same with React.

So why does React synchronously updated the state in these cases? Well, because it's trying to be as defensive as possible. Not being in control means it's not able to do any perf optimisations so it's better to update the state on spot and make sure the code that follows has access to the latest information available.

I think knowing how the library deals with each case is important and can sometimes get you out of subtle bugs.

Possible solution?

We're used to calling setState with one parameter only, but actually, the method's signature support two. The second argument that you can pass in is a callback function that will always be executed after the state has been updated (whether it's inside React's known context or outside of it).

An example might be:


_onClickHandler: function _onClickHandler() {
  console.log('State before (_onClickHandler): ' + JSON.stringify(this.state));
  this.setState({
    dollars: this.state.dollars + 10
  }, () => {
    console.log('Here state will always be updated to latest version!');
    console.log('State after (_onClickHandler): ' + JSON.stringify(this.state));
  });
}

I'll be honest with you, I haven't used/I'm not using this technique very much. But other than verbosity and multiple params, I see no issue with it (if anyone knows something, please let us know in the comments). So I guess I should take a second look :).

A note on the async nature of setState

I'd like to clear one thing up. If it's to be politically correct, setState, as a method, is always synchronous. It's just a function that calls something behind the scenes - enqueueState or enqueueCallback on updater.

In fact, here's setState taken directly from React source code:


ReactComponent.prototype.setState = function(partialState, callback) {
  invariant(
    typeof partialState === 'object' ||
    typeof partialState === 'function' ||
    partialState == null,
    'setState(...): takes an object of state variables to update or a ' +
    'function which returns an object of state variables.'
  );
  this.updater.enqueueSetState(this, partialState);
  if (callback) {
    this.updater.enqueueCallback(this, callback, 'setState');
  }
};

What's actually sync or async are the effects of calling setState in a React application - the reconciliation algorithm, doing the VDOM comparisons and calling render to update the real DOM.

Thus this article becomes a very simple idea. There is no such thing as asynchronous setState. And to celebrate that, I'm gonna put here a cactus emoji - 🌵. Just because I can and because I have a strange affiliation for it.

`Njoy setting states responsibly from now on!