We always hear that React’s setState is asynchronous, and I’ve been convinced of this until I read the source code of React.
Today, I want to tell you that setState is absolutely synchronous, no asynchronous except you open the Concurrent Mode of React, but it’s still unstable now, so what I talking about is based on Sync Mode of React.
I divide setState into 2 parts, triggered by React event and the others. A little confused? It doesn’t matter, keep up with me and then you will understand.
Hint: This is my first article written in English, there maybe some mistakes of grammar, please forgive me.
SyntheticEvent
At the beginging, I want to explain the concept of SyntheticEvent in React. It owes to SyntheticEvent that we can merge updates and render them only once, we also call it as batch updates. If you aren’t familiar with SyntheticEvent, here is the reference.
Update Processes
However, what’s the connection between setState and SyntheticEvent? here we have a example:
import React from ‘react’;
export default class App extends React.Component {
constructor(props) {
super(props);
this.state = {
clickCount: 0
};
}
handleClickCount = () => {
this.setState(state => ({
clickCount: state.clickCount + 1
}));
console.log(‘# state.clickCount’, this.state.clickCount);
}
render () {
console.log(‘# render start’);
return (
<div>
<p>{this.state.clickCount}</p>
<button onClick={this.handleClickCount}>Increment</button>
</div>
);
}
}
When button is clicked, console will show these infomation:
# state.clickCount 0
# render start
setState was already executed, but the state wasn’t updated, Why?
I guess many developers are familiar with this example, and the conclusion of React’s setState is asynchronous is also based on this.It seems to be asynchronous, but it’s actually synchronous, let me show you the real secret about setState.
When we click button, the first executed function isn’t handleClickCount but the TopLevelEvent which was captured by document, then it marks isBatchingUpdates which is one of global variable in React as true # code line, fn which contains events will be executed, and performSyncWork will start render phase.
Let’s refer to processes of setState, when it encounters requestWork #code line, if isBatchingUpdates is true, it will be returned before perfomSyncWork. That’s why setState is synchronous but state wasn’t updated immediately.
I simplified the whole processes as following:
let isBatchingUpdates = false;
function setState() {
if (isBatchingUpdates) {
return;
}
performSyncWork();
}
function dispatEvent(e) {
isBatchingUpdates = true;
fn(); // execute React events based on e.target
isBatchingUpdates = false;
if (!isBatchingUpdates) {
performSyncWork(); // start render phase
}
}
document.addEventListener(‘click’, dispatEvent, false);
As we can see above, there’s not any asynchronous code. In the event listener, it haven’t executes performSyncWork so state is still old.
The order of execution:
trigger document event
set isBatchingUpdates as true
execute synthetic event
execute setState
returned before performSyncWork(because isBatchingUpdates is true)
log the old state
execute performSyncWork in dispatchEvent
From what I just said, we can draw a conclusion: React’s setState is synchronous, but the order of execution is different between SyntheticEvent and the others.
Examples
Note that we know the principle of setState, let’s make a little change of the example.
I added some code following:
…
handleClickCountLater = () => {
setTimeout(this.handleClickCount, 0);
}
…
// render
…
<button onClick={this.handleClickCountLater}>Increment Later</button>
…
You can guess what will happen before I show you the answer.
The console will show following infomation when you click this button:
# render start
# state.clickCount 1
In the document listener, it pushes setTimeout to WebAPIs. In next event loop, setState will be executed, but isBatchingUpdates is false at this time, so it continues to execute performSyncWork. The result is the same if you replace setTimeout with Promise.resolve.
let’s add a native event to button element.
// constructor
…
this.btnRef = React.createRef();
…
componentDidMount() {
this.btnRef.current.addEventListener(‘click’, this.handleClickCount, false);
}
// render
…
<button ref={this.btnRef}>Increment Ref</button>
…
Due to btnRef’s event isn’t controlled by SyntheticEvent, we will get these infomation at console:
# state.clickCount 1
# render start
Conclusion
All in all, setState is synchronous, but the order of execution is different between SyntheticEvent and the others. In the SyntheticEvent, React will collect all the updates and then update them once, which we call batch updates.
If you want to update state based on the old state, I strongly recommend Functional setState, because it will help you avoid some bugs. To learn more about Functional setState, here is an article maybe help you.