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