Multiple Async Callbacks Updating State in React

I've been learning React and ran into a problem that's tricky to Google. When my component mounts, I fire off a list of XHR calls to an external service. When each one returns, I want to update the state. Here was my first attempt:

class SomeComponent extends React.Component {  
  componentDidMount() {
    const xhrCalls = ['http://www.example.com','...'];
    xhrCalls.forEach((url) => {
      $.ajax(url, (results) => {
        /*
          this.state.results is an immutable list;
          this.state.results.push yields a new list with 
          the item added.
        */
        let newState = this.state.results.push(result);
        this.setState(newState);
      });
    });
  }
}

What ended up happening was weird and surprising: My state got updated a bunch of times: so far so good. But, the state only had the results of the last XHR request. I was losing all the state updates except the last.

After a lot of messing around, I figured out that setState is asynchronous. If you call this.setState, the value of this.state is not guaranteed to update right away. You can pass a callback as a second argument to setState that will be called once the state has been modified.

This led me down a weird path of thinking about concurrency primitives to assure atomic updates. This is even weirder because JavaScript is not a concurrent programming environment.

Finally, I figured out that setState has another form that takes a function. The function takes a state object and returns a modified version. This is guaranteed to happen atomically.

class SomeComponent extends React.Component {  
  componentDidMount() {
    const xhrCalls = ['http://www.example.com','...'];
    xhrCalls.forEach((url) => {
      $.ajax(url, (results) => {
        this.setState((oldState) => {
          /*
            this.state.results is an immutable list;
            this.state.results.push yields a new list with 
            the item added.
          */
          return oldState.results.push(result);
        });
      });
    });
  };
}