RFClarification: why is `setState` asynchronous? #11527

Closed
mweststrate opened this Issue Nov 11, 2017 · 30 comments

Comments

Projects
None yet
@mweststrate

For quite a while I've tried to understood why setState is asynchronous. And failing to find an answer to it in the past, I came to the conclusion that it was for historical reasons and probably hard to change now. However @gaearon indicated there is a clear reason, so I am curious to find out :)

Anyway, here are the reasons I often hear, but I think they can't be everything as they are too easy to counter

Async setState is required for async rendering

Many initially think it is because of render efficiency. But I don't think that is the reason behind this behavior, because keeping setState sync with async rendering sounds trivial to me, something along the lines of:

Component.prototype.setState = (nextState) => {
  this.state = nextState
  if (!this.renderScheduled)
     setImmediate(this.forceUpdate)
}

In fact, for example mobx-react allows synchronous assignments to observables and still respect the async nature of rendering

Async setState is needed to know which state was rendered

The other argument I hear sometimes is that you want to reason about the state that was rendered, not the state that was requested. But I'm not sure this principle has much merit either. Conceptually it feels strange to me. Rendering is a side effect, state is about facts. Today, I am 32 years old, and next year I will turn 33, regardless whether the owning component manages to re-render this year or not :).

To draw a (probably not to good) parallel: If you wouldn't be able to read your last version of a self written word document until you printed it, that would be pretty awkward. I don't think for example game engines give you feedback on what state of the game was exactly rendered and which frames were dropped either.

An interesting observations: In 2 years mobx-react nobody ever asked me the question: How do I know my observables are rendered? This question just seems not relevant very often.

I did encounter a few cases where knowing which data was rendered was relevant. The case I remember was where I needed to know the pixel dimensions of some data for layout purposes. But that was elegantly solved by using didComponentUpdate and didn't really rely on setState being async either. These cases seem so rare that it hardly justify to design the api primarily around them. If it can be done somehow, it suffices I think


I have no doubt that the React team is aware of the confusion the async nature of setState often introduces, so I suspect there is another very good reason for the current semantics. Tell me more :)

@Kaybarax

This comment has been minimized.

Show comment
Hide comment
@Kaybarax

Kaybarax Nov 11, 2017

We're all waiting @gaearon .

We're all waiting @gaearon .

@mweststrate

This comment has been minimized.

Show comment
Hide comment
@mweststrate

mweststrate Nov 11, 2017

@Kaybarax Hey, it's weekend ;-)

@Kaybarax Hey, it's weekend ;-)

@Kaybarax

This comment has been minimized.

Show comment
Hide comment
@Kaybarax

Kaybarax Nov 11, 2017

@mweststrate Oh! my bad. Cool.

@mweststrate Oh! my bad. Cool.

@milesj

This comment has been minimized.

Show comment
Hide comment
@milesj

milesj Nov 12, 2017

Contributor

I'm gonna go out on a limb here and say it's because of batching multiple setStates in the same tick.

Contributor

milesj commented Nov 12, 2017

I'm gonna go out on a limb here and say it's because of batching multiple setStates in the same tick.

@gaearon

This comment has been minimized.

Show comment
Hide comment
@gaearon

gaearon Nov 12, 2017

Member

I'm going on vacation next week but I'll probably go on Tuesday so I'll try to reply on Monday.

Member

gaearon commented Nov 12, 2017

I'm going on vacation next week but I'll probably go on Tuesday so I'll try to reply on Monday.

@MEDIOCAL

This comment has been minimized.

Show comment
Hide comment
@MEDIOCAL

MEDIOCAL Nov 22, 2017

function enqueueUpdate(component) {
ensureInjected();

// Various parts of our code (such as ReactCompositeComponent's
// _renderValidatedComponent) assume that calls to render aren't nested;
// verify that that's the case. (This is called by each top-level update
// function, like setState, forceUpdate, etc.; creation and
// destruction of top-level components is guarded in ReactMount.)

if (!batchingStrategy.isBatchingUpdates) {
batchingStrategy.batchedUpdates(enqueueUpdate, component);
return;
}

dirtyComponents.push(component);
if (component._updateBatchNumber == null) {
component._updateBatchNumber = updateBatchNumber + 1;
}
}

function enqueueUpdate(component) {
ensureInjected();

// Various parts of our code (such as ReactCompositeComponent's
// _renderValidatedComponent) assume that calls to render aren't nested;
// verify that that's the case. (This is called by each top-level update
// function, like setState, forceUpdate, etc.; creation and
// destruction of top-level components is guarded in ReactMount.)

if (!batchingStrategy.isBatchingUpdates) {
batchingStrategy.batchedUpdates(enqueueUpdate, component);
return;
}

dirtyComponents.push(component);
if (component._updateBatchNumber == null) {
component._updateBatchNumber = updateBatchNumber + 1;
}
}

@ghost

This comment has been minimized.

Show comment
Hide comment
@ghost

ghost Nov 24, 2017

@mweststrate just 2 cents: that is very valid question.
I am sure we all agree it would be much easier to reason about the state if setState was synchronous.
Whatever were reasons to make setState asynchronous I am not sure react team well compared that to the drawbacks that would introduce, e.g. the difficulty to reason about the state now and the confusion that brings to developers.

ghost commented Nov 24, 2017

@mweststrate just 2 cents: that is very valid question.
I am sure we all agree it would be much easier to reason about the state if setState was synchronous.
Whatever were reasons to make setState asynchronous I am not sure react team well compared that to the drawbacks that would introduce, e.g. the difficulty to reason about the state now and the confusion that brings to developers.

@myitcv

This comment has been minimized.

Show comment
Hide comment
@bradenhs

This comment has been minimized.

Show comment
Hide comment
@bradenhs

bradenhs Dec 19, 2017

I've personally had and seen in other developers confusion on this subject. @gaearon it'd be great to get an explanation for this when you have some time :)

I've personally had and seen in other developers confusion on this subject. @gaearon it'd be great to get an explanation for this when you have some time :)

@gaearon

This comment has been minimized.

Show comment
Hide comment
@gaearon

gaearon Dec 19, 2017

Member

Sorry, it's the end of the year and we've been a bit behind on GitHub etc trying to wrap up everything we've been working on before the holidays.

I do intend to come back to this thread and discuss it. But it's also a bit of a moving target because we're currently working on async React features that directly relate to how and when this.state is updated. I don't want to spend a lot of time writing something up, and then have to rewrite it because the underlying assumptions have changed. So I'd like to keep this open, but I don't know yet when I'll be able to give a definitive answer.

Member

gaearon commented Dec 19, 2017

Sorry, it's the end of the year and we've been a bit behind on GitHub etc trying to wrap up everything we've been working on before the holidays.

I do intend to come back to this thread and discuss it. But it's also a bit of a moving target because we're currently working on async React features that directly relate to how and when this.state is updated. I don't want to spend a lot of time writing something up, and then have to rewrite it because the underlying assumptions have changed. So I'd like to keep this open, but I don't know yet when I'll be able to give a definitive answer.

@gaearon gaearon self-assigned this Dec 19, 2017

@myitcv myitcv referenced this issue in reactjs/rfcs Jan 17, 2018

Merged

async-safe, static lifecycle hooks #6

@gaearon

This comment has been minimized.

Show comment
Hide comment
@gaearon

gaearon Jan 24, 2018

Member

So here’s a few thoughts. This is not a complete response by any means, but maybe this is still more helpful than saying nothing.

First, I think we agree that delaying reconciliation in order to batch updates is beneficial. That is, we agree that setState() re-rendering synchronously would be inefficient in many cases, and it is better to batch updates if we know we’ll likely get several ones.

For example, if we’re inside a browser click handler, and both Child and Parent call setState, we don’t want to re-render the Child twice, and instead prefer to mark them as dirty, and re-render them together before exiting the browser event.

You’re asking: why can’t we do the same exact thing (batching) but write setState updates immediately to this.state without waiting for the end of reconciliation. I don’t think there’s one obvious answer (either solution has tradeoffs) but here’s a few reasons that I can think of.

Guaranteeing Internal Consistency

Even if state is updated synchronously, props are not. (You can’t know props until you re-render the parent component, and if you do this synchronously, batching goes out of the window.)

Right now the objects provided by React (state, props, refs) are internally consistent with each other. This means that if you only use those objects, they are guaranteed to refer to a fully reconciled tree (even if it’s an older version of that tree). Why does this matter?

When you use just the state, if it flushed synchronously (as you proposed), this pattern would work:

console.log(this.state.value) // 0
this.setState({ value: this.state.value + 1 });
console.log(this.state.value) // 1
this.setState({ value: this.state.value + 1 });
console.log(this.state.value) // 2

However, say this state needs to be lifted to be shared across a few components so you move it to a parent:

-this.setState({ value: this.state.value + 1 });
+this.props.onIncrement(); // Does the same thing in a parent

I want to highlight that in typical React apps that rely on setState() this is the single most common kind of React-specific refactoring that you would do on a daily basis.

However, this breaks our code!

console.log(this.props.value) // 0
this.props.onIncrement();
console.log(this.props.value) // 0
this.props.onIncrement();
console.log(this.props.value) // 0

This is because, in the model you proposed, this.state would be flushed immediately but this.props wouldn’t. And we can’t immediately flush this.props without re-rendering the parent, which means we would have to give up on batching (which, depending on the case, can degrade the performance very significantly).

There are also more subtle cases of how this can break, e.g. if you’re mixing data from props (not yet flushed) and state (proposed to be flushed immediately) to create a new state: #122 (comment). Refs present the same problem: #122 (comment).

These examples are not at all theoretical. In fact React Redux bindings used to have exactly this kind of problem because they mix React props with non-React state: reduxjs/react-redux#86, reduxjs/react-redux#99, reduxjs/react-redux#292, reduxjs/redux#1415, reduxjs/react-redux#525.

I don’t know why MobX users haven’t bumped into this, but my intuition is that they might be bumping into such scenarios but consider them their own fault. Or maybe they don’t read as much from props and instead read directly from MobX mutable objects instead.

So how does React solve this today? In React, both this.state and this.props update only after the reconciliation and flushing, so you would see 0 being printed both before and after refactoring. This makes lifting state up safe.

Yes, this can be inconvenient in some cases. Especially for folks coming from more OO backgrounds who just want to mutate state several times instead of thinking how to represent a complete state update in a single place. I can empathize with that, although I do think that keeping state updates concentrated is clearer from a debugging perspective: #122 (comment).

Still, you have the option of moving the state that you want to read immediately into some sideways mutable object, especially if you don’t use it as a source of truth for rendering. Which is pretty much what MobX lets you do 🙂.

You also have an option to flush the entire tree if you know what you’re doing. The API is called ReactDOM.flushSync(fn). I don’t think we have documented it yet, but we definitely will do so at some point during the 16.x release cycle. Note that it actually forces complete re-rendering for updates that happen inside of the call, so you should use it very sparingly. This way it doesn’t break the guarantee of internal consistency between props, state, and refs.

To sum up, the React model doesn’t always lead to the most concise code, but it is internally consistent and ensures lifting state up is safe.

Enabling Concurrent Updates

Conceptually, React behaves as if it had a single update queue per component. This is why the discussion makes sense at all: we discuss whether to apply updates to this.state immediately or not because we have no doubts the updates will be applied in that exact order. However, that needn’t be the case (haha).

Recently, we’ve been talking about “async rendering” a lot. I admit we haven’t done a very good job at communicating what that means, but that’s the nature of R&D: you go after an idea that seems conceptually promising, but you really understand its implications only after having spent enough time with it.

One way we’ve been explaining “async rendering” is that React could assign different priorities to setState() calls depending on where they’re coming from: an event handler, a network response, an animation, etc.

For example, if you are typing a message, setState() calls in the TextBox component need to be flushed immediately. However, if you receive a new message while you’re typing, it is probably better to delay rendering of the new MessageBubble up to a certain threshold (e.g. a second) than to let the typing stutter due to blocking the thread.

If we let certain updates have “lower priority”, we could split their rendering into small chunks of a few milliseconds so they wouldn’t be noticeable to the user.

I know performance optimizations like this might not sound very exciting or convincing. You could say: “we don’t need this with MobX, our update tracking is fast enough to just avoid re-renders”. I don’t think it’s true in all cases (e.g. no matter how fast MobX is, you still have to create DOM nodes and do the rendering for newly mounted views). Still, if it were true, and if you consciously decided that you’re okay with always wrapping objects into a specific JavaScript library that tracks reads and writes, maybe you don’t benefit from these optimizations as much.

But asynchronous rendering is not just about performance optimizations. We think it is a fundamental shift in what the React component model can do.

For example, consider the case where you’re navigating from one screen to another. Typically you’d show a spinner while the new screen is rendering.

However, if the navigation is fast enough (within a second or so), flashing and immediately hiding a spinner causes a degraded user experience. Worse, if you have multiple levels of components with different async dependencies (data, code, images), you end up with a cascade of spinners that briefly flash one by one. This is both visually unpleasant and makes your app slower in practice because of all the DOM reflows. It is also the source of much boilerplate code.

Wouldn’t it be nice if when you do a simple setState() that renders a different view, we could “start” rendering the updated view “in background”? Imagine that without any writing any coordination code yourself, you could choose to show a spinner if the update took more than a certain threshold (e.g. a second), and otherwise let React perform a seamless transition when the async dependencies of the whole new subtree are satisfied. Moreover, while we’re “waiting”, the “old screen” stays interactive (e.g. so you can choose a different item to transition to), and React enforces that if it takes too long, you have to show a spinner.

It turns out that, with current React model and some adjustments to lifecycles, we actually can implement this! @acdlite has been working on this feature for the past few weeks, and will post an RFC for it soon.

Note that this is only possible because this.state is not flushed immediately. If it were flushed immediately, we’d have no way to start rendering a “new version” of the view in background while the “old version” is still visible and interactive. Their independent state updates would clash.

I don’t want to steal the thunder from @acdlite with regards to announcing all of this but I hope this does sound at least a bit exciting. I understand this still might sound like vaporware, or like we don’t really know what we’re doing. I hope we can convince you otherwise in the coming months, and that you’ll appreciate the flexibility of the React model. And as far as I understand, at least in part this flexibility is possible thanks to not flushing state updates immediately.

Member

gaearon commented Jan 24, 2018

So here’s a few thoughts. This is not a complete response by any means, but maybe this is still more helpful than saying nothing.

First, I think we agree that delaying reconciliation in order to batch updates is beneficial. That is, we agree that setState() re-rendering synchronously would be inefficient in many cases, and it is better to batch updates if we know we’ll likely get several ones.

For example, if we’re inside a browser click handler, and both Child and Parent call setState, we don’t want to re-render the Child twice, and instead prefer to mark them as dirty, and re-render them together before exiting the browser event.

You’re asking: why can’t we do the same exact thing (batching) but write setState updates immediately to this.state without waiting for the end of reconciliation. I don’t think there’s one obvious answer (either solution has tradeoffs) but here’s a few reasons that I can think of.

Guaranteeing Internal Consistency

Even if state is updated synchronously, props are not. (You can’t know props until you re-render the parent component, and if you do this synchronously, batching goes out of the window.)

Right now the objects provided by React (state, props, refs) are internally consistent with each other. This means that if you only use those objects, they are guaranteed to refer to a fully reconciled tree (even if it’s an older version of that tree). Why does this matter?

When you use just the state, if it flushed synchronously (as you proposed), this pattern would work:

console.log(this.state.value) // 0
this.setState({ value: this.state.value + 1 });
console.log(this.state.value) // 1
this.setState({ value: this.state.value + 1 });
console.log(this.state.value) // 2

However, say this state needs to be lifted to be shared across a few components so you move it to a parent:

-this.setState({ value: this.state.value + 1 });
+this.props.onIncrement(); // Does the same thing in a parent

I want to highlight that in typical React apps that rely on setState() this is the single most common kind of React-specific refactoring that you would do on a daily basis.

However, this breaks our code!

console.log(this.props.value) // 0
this.props.onIncrement();
console.log(this.props.value) // 0
this.props.onIncrement();
console.log(this.props.value) // 0

This is because, in the model you proposed, this.state would be flushed immediately but this.props wouldn’t. And we can’t immediately flush this.props without re-rendering the parent, which means we would have to give up on batching (which, depending on the case, can degrade the performance very significantly).

There are also more subtle cases of how this can break, e.g. if you’re mixing data from props (not yet flushed) and state (proposed to be flushed immediately) to create a new state: #122 (comment). Refs present the same problem: #122 (comment).

These examples are not at all theoretical. In fact React Redux bindings used to have exactly this kind of problem because they mix React props with non-React state: reduxjs/react-redux#86, reduxjs/react-redux#99, reduxjs/react-redux#292, reduxjs/redux#1415, reduxjs/react-redux#525.

I don’t know why MobX users haven’t bumped into this, but my intuition is that they might be bumping into such scenarios but consider them their own fault. Or maybe they don’t read as much from props and instead read directly from MobX mutable objects instead.

So how does React solve this today? In React, both this.state and this.props update only after the reconciliation and flushing, so you would see 0 being printed both before and after refactoring. This makes lifting state up safe.

Yes, this can be inconvenient in some cases. Especially for folks coming from more OO backgrounds who just want to mutate state several times instead of thinking how to represent a complete state update in a single place. I can empathize with that, although I do think that keeping state updates concentrated is clearer from a debugging perspective: #122 (comment).

Still, you have the option of moving the state that you want to read immediately into some sideways mutable object, especially if you don’t use it as a source of truth for rendering. Which is pretty much what MobX lets you do 🙂.

You also have an option to flush the entire tree if you know what you’re doing. The API is called ReactDOM.flushSync(fn). I don’t think we have documented it yet, but we definitely will do so at some point during the 16.x release cycle. Note that it actually forces complete re-rendering for updates that happen inside of the call, so you should use it very sparingly. This way it doesn’t break the guarantee of internal consistency between props, state, and refs.

To sum up, the React model doesn’t always lead to the most concise code, but it is internally consistent and ensures lifting state up is safe.

Enabling Concurrent Updates

Conceptually, React behaves as if it had a single update queue per component. This is why the discussion makes sense at all: we discuss whether to apply updates to this.state immediately or not because we have no doubts the updates will be applied in that exact order. However, that needn’t be the case (haha).

Recently, we’ve been talking about “async rendering” a lot. I admit we haven’t done a very good job at communicating what that means, but that’s the nature of R&D: you go after an idea that seems conceptually promising, but you really understand its implications only after having spent enough time with it.

One way we’ve been explaining “async rendering” is that React could assign different priorities to setState() calls depending on where they’re coming from: an event handler, a network response, an animation, etc.

For example, if you are typing a message, setState() calls in the TextBox component need to be flushed immediately. However, if you receive a new message while you’re typing, it is probably better to delay rendering of the new MessageBubble up to a certain threshold (e.g. a second) than to let the typing stutter due to blocking the thread.

If we let certain updates have “lower priority”, we could split their rendering into small chunks of a few milliseconds so they wouldn’t be noticeable to the user.

I know performance optimizations like this might not sound very exciting or convincing. You could say: “we don’t need this with MobX, our update tracking is fast enough to just avoid re-renders”. I don’t think it’s true in all cases (e.g. no matter how fast MobX is, you still have to create DOM nodes and do the rendering for newly mounted views). Still, if it were true, and if you consciously decided that you’re okay with always wrapping objects into a specific JavaScript library that tracks reads and writes, maybe you don’t benefit from these optimizations as much.

But asynchronous rendering is not just about performance optimizations. We think it is a fundamental shift in what the React component model can do.

For example, consider the case where you’re navigating from one screen to another. Typically you’d show a spinner while the new screen is rendering.

However, if the navigation is fast enough (within a second or so), flashing and immediately hiding a spinner causes a degraded user experience. Worse, if you have multiple levels of components with different async dependencies (data, code, images), you end up with a cascade of spinners that briefly flash one by one. This is both visually unpleasant and makes your app slower in practice because of all the DOM reflows. It is also the source of much boilerplate code.

Wouldn’t it be nice if when you do a simple setState() that renders a different view, we could “start” rendering the updated view “in background”? Imagine that without any writing any coordination code yourself, you could choose to show a spinner if the update took more than a certain threshold (e.g. a second), and otherwise let React perform a seamless transition when the async dependencies of the whole new subtree are satisfied. Moreover, while we’re “waiting”, the “old screen” stays interactive (e.g. so you can choose a different item to transition to), and React enforces that if it takes too long, you have to show a spinner.

It turns out that, with current React model and some adjustments to lifecycles, we actually can implement this! @acdlite has been working on this feature for the past few weeks, and will post an RFC for it soon.

Note that this is only possible because this.state is not flushed immediately. If it were flushed immediately, we’d have no way to start rendering a “new version” of the view in background while the “old version” is still visible and interactive. Their independent state updates would clash.

I don’t want to steal the thunder from @acdlite with regards to announcing all of this but I hope this does sound at least a bit exciting. I understand this still might sound like vaporware, or like we don’t really know what we’re doing. I hope we can convince you otherwise in the coming months, and that you’ll appreciate the flexibility of the React model. And as far as I understand, at least in part this flexibility is possible thanks to not flushing state updates immediately.

@Kaybarax

This comment has been minimized.

Show comment
Hide comment
@Kaybarax

Kaybarax Jan 24, 2018

Wonderful in depth explanation to the decisions behind the architecture of React. Thanks.

Wonderful in depth explanation to the decisions behind the architecture of React. Thanks.

@pod4g

This comment has been minimized.

Show comment
Hide comment

pod4g commented Jan 25, 2018

mark

@xJkit

This comment has been minimized.

Show comment
Hide comment
@xJkit

xJkit Jan 25, 2018

Thank you, Dan.

xJkit commented Jan 25, 2018

Thank you, Dan.

@solkimicreb

This comment has been minimized.

Show comment
Hide comment
@solkimicreb

solkimicreb Jan 25, 2018

I ❤️ this issue. Awesome question and awesome answer. I always thought that this was a bad design decision, now I have to rethink 😄

I ❤️ this issue. Awesome question and awesome answer. I always thought that this was a bad design decision, now I have to rethink 😄

@wxyyxc1992

This comment has been minimized.

Show comment
Hide comment
@wxyyxc1992

wxyyxc1992 Jan 25, 2018

Thank you, Dan.

Thank you, Dan.

@Dajust

This comment has been minimized.

Show comment
Hide comment
@Dajust

Dajust Jan 25, 2018

I call it asyncAwesome setState 😄

Dajust commented Jan 25, 2018

I call it asyncAwesome setState 😄

@nicowilliams

This comment has been minimized.

Show comment
Hide comment
@nicowilliams

nicowilliams Jan 25, 2018

I tend to think that everything should be implemented async first, and if you find a need for a sync operation, well, wrap the async operation with a wait for completion. It's much easier to make sync code out of async code (all you need is a wrapper) than the reverse (which basically requires a complete rewrite, unless you reach for threading, which is not at all light-weight).

I tend to think that everything should be implemented async first, and if you find a need for a sync operation, well, wrap the async operation with a wait for completion. It's much easier to make sync code out of async code (all you need is a wrapper) than the reverse (which basically requires a complete rewrite, unless you reach for threading, which is not at all light-weight).

@mweststrate

This comment has been minimized.

Show comment
Hide comment
@mweststrate

mweststrate Jan 25, 2018

@gaearon thanks for the extensive explanation! It has been nagging me for a long time ("there must be a good reason, but nobody can tell which one"). But now it makes total sense and I see how this is a really conscious decision :). Thanks a lot for the extensive answer, really appreciate it!

Or maybe they don’t read as much from props and instead read directly from MobX mutable objects instead.

I think this is quite true indeed, in MobX props are typically used as just component configuration, and the domain data is typically not captured in props, but in domain entities that are passed around between components.

Again, thanks a lot!

mweststrate commented Jan 25, 2018

@gaearon thanks for the extensive explanation! It has been nagging me for a long time ("there must be a good reason, but nobody can tell which one"). But now it makes total sense and I see how this is a really conscious decision :). Thanks a lot for the extensive answer, really appreciate it!

Or maybe they don’t read as much from props and instead read directly from MobX mutable objects instead.

I think this is quite true indeed, in MobX props are typically used as just component configuration, and the domain data is typically not captured in props, but in domain entities that are passed around between components.

Again, thanks a lot!

@LPGhatguy LPGhatguy referenced this issue in Roblox/roact Jan 25, 2018

Open

Asynchronous Rendering #18

@sag1v

This comment has been minimized.

Show comment
Hide comment
@sag1v

sag1v Jan 26, 2018

@gaearon Thanks for the detailed and great explanation.
Though there is still something missing here which i think i understand but want to be sure.

When the event is registered "Outside React", that means maybe through addEventListener on a ref for example. Then there are no batching taking place.
Consider this code:

class App extends React.Component {
  constructor(props) {
    super(props);
    this.state = {
      count: 0
    };
  }

  componentDidMount() {
    this.refBtn.addEventListener("click", this.onClick);
  }

  componentWillUnmount() {
    this.refBtn.removeEventListener("click", this.onClick);
  }

  onClick = () => {
    console.log("before setState", this.state.count);
    this.setState(state => ({ count: state.count + 1 }));
    console.log("after setState", this.state.count);
  };

  render() {
    return (
      <div>
        <button onClick={this.onClick}>React Event</button>
        <button ref={ref => (this.refBtn = ref)}>Direct DOM event</button>
      </div>
    );
  }
}

When we will click on the button "React Event", we will see in the console:
"before setState" 0
"after setState" 0
When the other button "Direct DOM event" will get clicked, we will see in the console:
"before setState" 0
"after setState" 1

After some small research and browsing through the source code, i think i know why this happens.
react can't fully control the flow of the event and can't be sure on when and how the next event will fire, so as a "panic-mode" it will just trigger the state change immediately.

What are your thoughts about this? 🤔

sag1v commented Jan 26, 2018

@gaearon Thanks for the detailed and great explanation.
Though there is still something missing here which i think i understand but want to be sure.

When the event is registered "Outside React", that means maybe through addEventListener on a ref for example. Then there are no batching taking place.
Consider this code:

class App extends React.Component {
  constructor(props) {
    super(props);
    this.state = {
      count: 0
    };
  }

  componentDidMount() {
    this.refBtn.addEventListener("click", this.onClick);
  }

  componentWillUnmount() {
    this.refBtn.removeEventListener("click", this.onClick);
  }

  onClick = () => {
    console.log("before setState", this.state.count);
    this.setState(state => ({ count: state.count + 1 }));
    console.log("after setState", this.state.count);
  };

  render() {
    return (
      <div>
        <button onClick={this.onClick}>React Event</button>
        <button ref={ref => (this.refBtn = ref)}>Direct DOM event</button>
      </div>
    );
  }
}

When we will click on the button "React Event", we will see in the console:
"before setState" 0
"after setState" 0
When the other button "Direct DOM event" will get clicked, we will see in the console:
"before setState" 0
"after setState" 1

After some small research and browsing through the source code, i think i know why this happens.
react can't fully control the flow of the event and can't be sure on when and how the next event will fire, so as a "panic-mode" it will just trigger the state change immediately.

What are your thoughts about this? 🤔

@mweststrate

This comment has been minimized.

Show comment
Hide comment
@mweststrate

mweststrate Jan 26, 2018

@sag1v although a bit related, it is probably clearer to open a new issue for new questions. Just use #11527 somewhere in the description to link it to this one.

@sag1v although a bit related, it is probably clearer to open a new issue for new questions. Just use #11527 somewhere in the description to link it to this one.

@Kaybarax

This comment has been minimized.

Show comment
Hide comment
@Kaybarax

Kaybarax Jan 26, 2018

@sag1v @gaearon had given me a very terse reply here https://twitter.com/dan_abramov/status/949992957180104704 . I think his take on this would also answer me more concretely.

@sag1v @gaearon had given me a very terse reply here https://twitter.com/dan_abramov/status/949992957180104704 . I think his take on this would also answer me more concretely.

@sag1v

This comment has been minimized.

Show comment
Hide comment
@sag1v

sag1v Jan 26, 2018

@mweststrate I thought of opening a new issue but then i realized that this is directly related to your question "why is setState asynchronous?".
As this discussion is about the decisions made on making setState "async", well i thought to add in when and why making it "sync".
I don't mind opening a new issue if i didn't convince you that my post is related to this issue 😉

sag1v commented Jan 26, 2018

@mweststrate I thought of opening a new issue but then i realized that this is directly related to your question "why is setState asynchronous?".
As this discussion is about the decisions made on making setState "async", well i thought to add in when and why making it "sync".
I don't mind opening a new issue if i didn't convince you that my post is related to this issue 😉

@sag1v

This comment has been minimized.

Show comment
Hide comment
@sag1v

sag1v Jan 26, 2018

@Kaybarax That becuase your question was "When is it sync" and not "Why is it sync"?.
As i mentioned in my post, i think i know the why, but i want to be sure and get the Official answer hehe. 😄

sag1v commented Jan 26, 2018

@Kaybarax That becuase your question was "When is it sync" and not "Why is it sync"?.
As i mentioned in my post, i think i know the why, but i want to be sure and get the Official answer hehe. 😄

@gaearon

This comment has been minimized.

Show comment
Hide comment
@gaearon

gaearon Jan 26, 2018

Member

react can't fully control the flow of the event and can't be sure on when and how the next event will fire, so as a "panic-mode" it will just trigger the state change immediately

Sort of. Although this is not exactly related to the question about updating this.state.

What you’re asking is in which cases does React enable batching. React currently batches updates inside event handlers managed by React because React “sits” at the top call stack frame and knows when all React event handlers have run. At that point it flushes the update.

If the event handler isn’t set up by React, currently it makes the update synchronous. Because it doesn’t know if it’s safe to wait or not, and if other updates will happen soon.

In the future versions of React this behavior will change. The plan is to treat updates as low priority by default so they end up coalesced and batched together (e.g. within a second), with an opt-in to flush them immediately. You can read more here: #11171 (comment).

Member

gaearon commented Jan 26, 2018

react can't fully control the flow of the event and can't be sure on when and how the next event will fire, so as a "panic-mode" it will just trigger the state change immediately

Sort of. Although this is not exactly related to the question about updating this.state.

What you’re asking is in which cases does React enable batching. React currently batches updates inside event handlers managed by React because React “sits” at the top call stack frame and knows when all React event handlers have run. At that point it flushes the update.

If the event handler isn’t set up by React, currently it makes the update synchronous. Because it doesn’t know if it’s safe to wait or not, and if other updates will happen soon.

In the future versions of React this behavior will change. The plan is to treat updates as low priority by default so they end up coalesced and batched together (e.g. within a second), with an opt-in to flush them immediately. You can read more here: #11171 (comment).

@antzshrek

This comment has been minimized.

Show comment
Hide comment

Awesome!

@SagiMedina

This comment has been minimized.

Show comment
Hide comment
@SagiMedina

SagiMedina Jan 27, 2018

That question & answer should be documented somewhere more reachable. Thank you guys enlightening us.

That question & answer should be documented somewhere more reachable. Thank you guys enlightening us.

@decadef20

This comment has been minimized.

Show comment
Hide comment
@decadef20

decadef20 Jan 30, 2018

Learned a lot . Thanks

Learned a lot . Thanks

@iugo iugo referenced this issue in zsqk/zsqk Feb 1, 2018

Open

为什么 `setState` 要是异步的? #78

@justjavac justjavac referenced this issue in justjavac/justjavac.github.com Feb 5, 2018

Open

React 是否保持 state 更新的顺序? #9

@jiyinyiyong

This comment has been minimized.

Show comment
Hide comment
@jiyinyiyong

jiyinyiyong Feb 16, 2018

Trying to add my point of view to the thread. I work on an app based on MobX some months, I have been exploring ClojureScript for years and made my own React alternative(called Respo), I tried Redux in the early days although very short time, and I'm betting on ReasonML.

The core idea in combining React and Functional Programming(FP) is you got a piece of data you can render it into a view with whatever skills you have that obeys the laws in FP. You got no side-effects if you only use pure functions.

React is not pure functional. By embracing local states inside components, React has the power of interacting with various libraries related to DOM and other browser APIs(also friendly to MobX), which meanwhile makes React impure. However, I tried in ClojureScript, if React is pure, it could be disaster since it's really hard to interact with so many existing libraries that has side-effects in them.

So in Respo(my own solution), I had two goals that seem conflicted, 1) view = f(store) so no local state is expected; 2) I don't like to program all component UI states in global reducers since that could be hard for maintaining. At end, I figured out I need a syntax sugar, which helps me maintain components states in a global store with paths, meanwhile I write state updates inside component with Clojure macros.

So what I learnt: local states is a developer experience feature, underneath we want global states which enables our engines to perform optimisations in deep levels. So, MobX people prefer OOP, is that for developer experience, or for engines?

Trying to add my point of view to the thread. I work on an app based on MobX some months, I have been exploring ClojureScript for years and made my own React alternative(called Respo), I tried Redux in the early days although very short time, and I'm betting on ReasonML.

The core idea in combining React and Functional Programming(FP) is you got a piece of data you can render it into a view with whatever skills you have that obeys the laws in FP. You got no side-effects if you only use pure functions.

React is not pure functional. By embracing local states inside components, React has the power of interacting with various libraries related to DOM and other browser APIs(also friendly to MobX), which meanwhile makes React impure. However, I tried in ClojureScript, if React is pure, it could be disaster since it's really hard to interact with so many existing libraries that has side-effects in them.

So in Respo(my own solution), I had two goals that seem conflicted, 1) view = f(store) so no local state is expected; 2) I don't like to program all component UI states in global reducers since that could be hard for maintaining. At end, I figured out I need a syntax sugar, which helps me maintain components states in a global store with paths, meanwhile I write state updates inside component with Clojure macros.

So what I learnt: local states is a developer experience feature, underneath we want global states which enables our engines to perform optimisations in deep levels. So, MobX people prefer OOP, is that for developer experience, or for engines?

@gaearon

This comment has been minimized.

Show comment
Hide comment
@gaearon

gaearon Mar 27, 2018

Member

By the way I gave a talk about the future of React and its async features, in case you missed it:
https://reactjs.org/blog/2018/03/01/sneak-peek-beyond-react-16.html

Member

gaearon commented Mar 27, 2018

By the way I gave a talk about the future of React and its async features, in case you missed it:
https://reactjs.org/blog/2018/03/01/sneak-peek-beyond-react-16.html

@su6838354 su6838354 referenced this issue in sfm-rc/blog Mar 28, 2018

Open

react setState 异步的原因 #8

@chrisrichard chrisrichard referenced this issue in mobxjs/mobx-react May 4, 2018

Closed

Unexpected rendering behaviour after update #187

@kdnk kdnk referenced this issue in wantedly/frontend_night May 17, 2018

Closed

frontend night #3 #3

@ghost ghost referenced this issue in bcherny/undux Jun 2, 2018

Closed

Objects are not valid as a React child #40

@peterlxb peterlxb referenced this issue in peterlxb/Frontend-react Jul 18, 2018

Open

04-React 组件的生命周期及其使用场景 #7

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment