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
(becauseisBatchingUpdates
is true) - log the old state
- execute
performSyncWork
indispatchEvent
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.