Beware: React setState is asynchronous!
I recently fixed a bug in one of my applications whose root cause was that I was calling setState multiple times during a single update cycle. Because setState is asynchronous, subsequent calls in the same update cycle will overwrite previous updates, and the previous changes will be lost.
Consider the following line of code. When you read it, the intentions are clear: set foo to 42 and keep the other properties untouched. But the change may never take effect if somebody else calls setState during the same update cycle and sets a different property!
this.setState({ ...this.state, foo: 42 });
It’s uncommon to call setState multiple times from the same block of code (e.g. an event callback), though even there I can imagine situations where that would not look completely out of place:
this.setState({ ...this.state, foo: 42 });
if (condition) {
this.setState({ ...this.state, isBar: true });
}
But there are far less obvious situations which lead to multiple calls to setState. For example: combination of componentWillReceiveProps and an input onChange event handler, like I had in my application.
Invalidating state changes is one issue here. Another is that you may be acting on an outdated view of the current state. Any time you access this.state you can not be sure that it’s the most recent version. This is especially important if you do state changes such as incrementing or appending:
// Increment foo
this.setState({ ...this.state, foo: this.state.foo + 1 });// Append an item to an array
this.setState({ ...this.state, foo: [].concat(this.state.foo, 1) });
These state changes must be done atomically and you must be sure that you are basing your decisions and actions on the current view of the state.
Considering the implications — innocently looking code which doesn’t behave as expected — I firmly believe that using this particular setState API should be avoided.
Alternative setState calling convention
It’s relatively easy to avoid these problems: setState supports an alternative calling convention where instead of giving it the new state directly, you give it a function which atomically transforms the current state and props into a new state.
this.setState((previousState, currentProps) => {
return { ...previousState, foo: currentProps.bar };
});
Writing the callback inline like that is convenient, but it doesn’t completely prevent the problem from happening. It is still possible to write the callback such that it returns the new state based on a potentially outdated view of the current state, namely when you capture values from the state outside of the callback:
// Capturing values from the state outside of the setState callback.
let previousFoo = this.state.foo;this.setState(function incrementFoo(previousState) {
// BAD! Setting `foo` based on a potentially outdated
// view of its current value: `foo` may have been updated
// in the meantime by another call to `setState`. return { ...previousState, foo: previousFoo + 10 };
});
For best results move the callback to a place where it doesn’t have access to the component instance (this.state). The following code makes it unambiguous which external values the transformation depends on (in this case the delta), and leaves very little opportunity to act on an outdated view of the state.
function incrementFooBy(delta) {
return (previousState, currentProps) => {
return { ...previousState, foo: previousState.foo + delta };
};
}class MyComponent extends React.Component {
onClick = () => {
this.setState(incrementFooBy(42));
} render() {
return <button onClick={onClick}>click me</button>;
}
}
If you squint your eyes, that pattern looks strikingly close to how Redux works: setState is the Redux dispatch function to which you pass descriptions how you would like the state to be transformed. In React the transformation function itself, in Redux an abstract description of the transformation (in the form of a plain JavaScript object) which is interpreted by the reducer.