I know that React may perform state updates asynchronously and in batch for performance optimization. Therefore you can never trust the state to be updated after having called setState
. But can you trust React to update the state in the same order as setState
is called for
Consider clicking the button in the following examples:
1. Is there ever a possibility that a is false and b is true for:
class Container extends React.Component {
constructor(props) {
super(props);
this.state = { a: false, b: false };
}
render() {
return <Button onClick={this.handleClick}/>
}
handleClick = () => {
this.setState({ a: true });
this.setState({ b: true });
}
}
2. Is there ever a possibility that a is false and b is true for:
class SuperContainer extends React.Component {
constructor(props) {
super(props);
this.state = { a: false };
}
render() {
return <Container setParentState={this.setState.bind(this)}/>
}
}
class Container extends React.Component {
constructor(props) {
super(props);
this.state = { b: false };
}
render() {
return <Button onClick={this.handleClick}/>
}
handleClick = () => {
this.props.setParentState({ a: true });
this.setState({ b: true });
}
}
Keep in mind that these are extreme simplifications of my use case. I realize that I can do this differently, e.g. updating both state params at the same time in example 1, as well as performing the second state update in a callback to the first state update in example 2. However, this is not my question, and I am only interested in if there is a well defined way that React performs these state updates, nothing else.
Any answer backed up by documentation is greatly appreciated.
I work on React.
TLDR:
But can you trust React to update the state in the same order as setState is called for
- the same component?
Yes.
- different components?
Yes.
The order of updates is always respected. Whether you see an intermediate state "between" them or not depends on whether you're inside in a batch or not.
Currently (React 16 and earlier), only updates inside React event handlers are batched by default. There is an unstable API to force batching outside of event handlers for rare cases when you need it.
In future versions (probably React 17 and later), React will batch all updates by default so you won't have to think about this. As always, we will announce any changes about this on the React blog and in the release notes.
The key to understanding this is that no matter how many setState()
calls in how many components you do inside a React event handler, they will produce only a single re-render at the end of the event. This is crucial for good performance in large applications because if Child
and Parent
each call setState()
when handling a click event, you don't want to re-render the Child
twice.
In both of your examples, setState()
calls happen inside a React event handler. Therefore they are always flushed together at the end of the event (and you don't see the intermediate state).
The updates are always shallowly merged in the order they occur. So if the first update is {a: 10}
, the second is {b: 20}
, and the third is {a: 30}
, the rendered state will be {a: 30, b: 20}
. The more recent update to the same state key (e.g. like a
in my example) always "wins".
The this.state
object is updated when we re-render the UI at the end of the batch. So if you need to update state based on a previous state (such as incrementing a counter), you should use the functional setState(fn)
version that gives you the previous state, instead of reading from this.state
. If you're curious about the reasoning for this, I explained it in depth in this comment.
In your example, we wouldn't see the "intermediate state" because we are inside a React event handler where batching is enabled (because React "knows" when we're exiting that event).
However, both in React 16 and earlier versions, there is yet no batching by default outside of React event handlers. So if in your example we had an AJAX response handler instead of handleClick
, each setState()
would be processed immediately as it happens. In this case, yes, you would see an intermediate state:
promise.then(() => {
// We're not in an event handler, so these are flushed separately.
this.setState({a: true}); // Re-renders with {a: true, b: false }
this.setState({b: true}); // Re-renders with {a: true, b: true }
this.props.setParentState(); // Re-renders the parent
});
We realize it's inconvenient that the behavior is different depending on whether you're in an event handler or not. This will change in a future React version that will batch all updates by default (and provide an opt-in API to flush changes synchronously). Until we switch the default behavior (potentially in React 17), there is an API you can use to force batching:
promise.then(() => {
// Forces batching
ReactDOM.unstable_batchedUpdates(() => {
this.setState({a: true}); // Doesn't re-render yet
this.setState({b: true}); // Doesn't re-render yet
this.props.setParentState(); // Doesn't re-render yet
});
// When we exit unstable_batchedUpdates, re-renders once
});
Internally React event handlers are all being wrapped in unstable_batchedUpdates
which is why they're batched by default. Note that wrapping an update in unstable_batchedUpdates
twice has no effect. The updates are flushed when we exit the outermost unstable_batchedUpdates
call.
That API is "unstable" in the sense that we will remove it when batching is already enabled by default. However, we won't remove it in a minor version, so you can safely rely on it until React 17 if you need to force batching in some cases outside of React event handlers.
To sum up, this is a confusing topic because React only batches inside event handlers by default. This will change in future versions, and the behavior will be more straightforward then. But the solution is not to batch less, it's to batch more by default. That's what we're going to do.