Async Nature Of setState()

On the Async nature of setState()

Gist:

React batches updates and flushes it out once per frame (perf optimization) However, in some cases React has no control over batching, hence updates are made synchronously eg. eventListeners, Ajax, setTimeout and similar Web APIs

Main Idea

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. There is no guarantee of synchronous operation of calls to setState and calls may be batched for performance gains.

Run the below code and you will make the following observations:

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.

So why does React synchronously update 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 performance 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.

class TestComponent extends React.Component {
  constructor(...args) {
    super(...args);
    this.state = {
      dollars: 10
    };
    this._saveButtonRef = (btn => { this._btnRef = btn });
    [
      '_onTimeoutHandler',
      '_onMouseLeaveHandler',
      '_onClickHandler',
      '_onAjaxCallback',
    ].forEach(propToBind => {
      this[propToBind] = this[propToBind].bind(this);
    });
  }

  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://reactjs.org/docs/events.html
    this._btnRef.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
    fetch('https://api.github.com/users')
      .then(this._onAjaxCallback);
  }

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

    return (
       <button
         ref={this._saveButtonRef}
         onClick={this._onClickHandler}>
         'Click me'
      </button>
    );
  }

  _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() {
    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() {
    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(response) {
    if (response.status !== 200) {
      console.log('Error in AJAX call: ' + response.statusText);
      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(
  <TestComponent />,
  document.getElementById('app')
);

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));
   });
}

A note on the async nature of setstate

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.

results matching ""

    No results matching ""