Do you want to request a feature or report a bug?
What is the current behavior?
If the current behavior is a bug, please provide the steps to reproduce and if possible a minimal demo of the problem. Your bug will get fixed much faster if we can run your code and it doesn't have dependencies other than React. Paste the link to your JSFiddle (https://jsfiddle.net/Luktwrdm/) or CodeSandbox (https://codesandbox.io/s/new) example below:
Code:
class TestComponent extends React.Component {
async componentDidMount() {
this.setState({});
console.log('hey1');
this.setState({});
console.log('hey2');
await this.setState({});
console.log('hey3');
this.setState({});
}
componentDidUpdate() {
console.log('update');
}
render() {
return <div></div>;
}
}
Console:
hey2
update
hey3
update
Is this intended?
What is the expected behavior?
Which versions of React, and which browser / OS are affected by this issue? Did this work in previous versions of React?
I would say this is intended. Let's see how it works if we rewrite async/await to promises.
Everything below is my assumption on how React works. Would be cool to hear feedback from the core team.
Note: setState does not return any value, so you can not rely on it like it was a promise
If "ticks" and "loops" make absolutely no sense, please read https://developer.mozilla.org/en-US/docs/Web/JavaScript/EventLoop
componentDidMount() {
this.setState({}); // enqueued to the end of "the current tick"
console.log('hey1'); // sync call to console
this.setState({}); // enqueued to the end of "the current tick"
console.log('hey2'); // sync call to console
// this is where the current tick ends
// then() will be run in another "event loop"
Promise.resolve(this.setState({})).then(() => {
// we are in the new tick, previous tick had been already flushed and rendered
this.setState({}); // enqueued to the end of "the current tick"
console.log('hey3'); // sync call to console
this.setState({}); // enqueued to the end of "the current tick"
});
}
@miraage
Hmmm Let me show you other code :)
class TestComponent extends React.Component {
async componentDidMount() {
this.setState({});
console.log('hey1');
this.setState({});
console.log('hey2');
await this.setState({});
console.log('hey3');
this.setState({});
console.log('hey4');
this.setState({});
}
componentDidUpdate() {
console.log('update');
}
render() {
return <div></div>;
}
}
Console:
hey2
update
hey3
update
hey4
update
The point I confused is that re-render happens on every setState after executing await function.
If this issue comes from event loop, update will print (count of await keyword + 1) times.
Isn't it? 🤔
Yes, this is the current behavior. See this answer from a React maintainer: In depth: When and why are setState() calls batched?
When you run an async function, only the code from the start of the function up to the first await or return executes synchronously. Later calls to setState happen outside of the lifecycle method and cannot be automatically batched in React 16. The Stack Overflow answer describes how to re-enable batching:
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 });
@TroyTae I described the very simple example. What could happen in real life: each setState is enqueued. Let's imagine it as "react component asks to enqueue an update". All of those updates could be applied either as batch or individually given a certain condition.
Or update could be actually paused and resumed later (this is what Fiber was designed for).
I highly recommend you to watch React Fiber Deep Dive, so you can have a better idea how on React works under the hood.
@nortonwong
Thank you!
I wish this feature is released by default in v17 :)
@miraage
I'm so sorry. Can you explain your code?
According to your example, my code can rewrite like this. right?
// this is where the current tick ends
// then() will be run in another "event loop"
Promise.resolve(this.setState({})).then(() => {
// we are in the new tick, previous tick had been already flushed and rendered
this.setState({}); // enqueued to the end of "the current tick"
console.log('hey3'); // sync call to console
this.setState({}); // enqueued to the end of "the current tick"
console.log('hey4'); // sync call to console
this.setState({}); // enqueued to the end of "the current tick"
});
hey2
update
hey3
update
hey4
update
According your comment, three setState are enqueued.
But in the console, setState is executed and make re-render immediately.
@TroyTae yep, this is how I see React working on a basic level. I can not explain why there are 2 updates inside an async function and a single update outside.
What I have digged so far:
When we enqueue a state update for a class component, it gets an expirationTime. I believe it is a time window before "I render no matter what". It depends on a priority from the scheduler.
expirationTime calc: https://github.com/facebook/react/blob/62b04cfa753076d5ffb1d74b855f8f8db36f5186/packages/react-reconciler/src/ReactFiberWorkLoop.js#L290
scheduler (tons of "magic", won't even try to understand 😁 ): https://github.com/facebook/react/blob/62b04cfa753076d5ffb1d74b855f8f8db36f5186/packages/scheduler/src/Scheduler.js
So, my assumption would be:
Maybe we can try summon @acdlite, so he could give us some answers.
Given the @nortonwong's answer and the SO answer, we may assume that lifecycle methods are also batched.
https://reactjs.org/docs/implementation-notes.html#updating-host-components
The reconciler also implements support for setState() in composite components. Multiple updates inside event handlers get batched into a single update.
Lifecycle methods that are called after the DOM is ready, such as componentDidMount() and componentDidUpdate(), get collected into “callback queues” and are executed in a single batch.
This is expected behavior because we currently only batch updates inside scopes known to React (e.g. during a synchronous lifecycle method, or during an event handler). You can work around this with unstable_batchedUpdates as mentioned above.
In the future batching will be on by default everywhere.
Most helpful comment
This is expected behavior because we currently only batch updates inside scopes known to React (e.g. during a synchronous lifecycle method, or during an event handler). You can work around this with
unstable_batchedUpdatesas mentioned above.In the future batching will be on by default everywhere.