Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

DOM is rerender every time when I execute setState after await function #16387

Closed
TroyTae opened this issue Aug 14, 2019 · 10 comments

Comments

@TroyTae
Copy link

commented Aug 14, 2019

Do you want to request a feature or report a bug?

  • bug

What is the current behavior?

  • DOM is rerender every time when I execute setState after await function.

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:

hey1
hey2
update
hey3
update

Is this intended?

What is the expected behavior?

  • I guess that DOM will be updated single time.

Which versions of React, and which browser / OS are affected by this issue? Did this work in previous versions of React?

  • Win/Mac
  • "react": "^16.8.6",
@miraage

This comment has been minimized.

Copy link
Contributor

commented Aug 14, 2019

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"
  });
}
@TroyTae

This comment has been minimized.

Copy link
Author

commented Aug 14, 2019

@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:

hey1
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? 🤔

@nortonwong

This comment has been minimized.

Copy link

commented Aug 14, 2019

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
});
@miraage

This comment has been minimized.

Copy link
Contributor

commented Aug 14, 2019

@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.

@TroyTae

This comment has been minimized.

Copy link
Author

commented Aug 14, 2019

@nortonwong
Thank you!
I wish this feature is released by default in v17 :)

@TroyTae

This comment has been minimized.

Copy link
Author

commented Aug 14, 2019

@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"
  });
hey1
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.

@miraage

This comment has been minimized.

Copy link
Contributor

commented Aug 14, 2019

@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.

updater:

const expirationTime = computeExpirationForFiber(

expirationTime calc:

export function computeExpirationForFiber(

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:

  • a code, fired in the promise is being prioritized differently (totally makes sense due to promises microqueue), OR
  • react has it's own magic for this situation

Maybe we can try summon @acdlite, so he could give us some answers.

@miraage

This comment has been minimized.

Copy link
Contributor

commented Aug 14, 2019

Given the @nortonwong's answer and the SO answer, we may assume that lifecycle methods are also batched.

@miraage

This comment has been minimized.

Copy link
Contributor

commented Aug 14, 2019

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.

@gaearon

This comment has been minimized.

Copy link
Member

commented Aug 15, 2019

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.

@gaearon gaearon closed this Aug 15, 2019

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
4 participants
You can’t perform that action at this time.