Historic reasons behind setState not being immediately visible


(Paul Jolly) #1

As covered in the docs:

and many posts here, a call to .setState() is not guaranteed to immediately reflect via .state.

For example if I call:

this.setState({x: 5})

the result of this.state.x is undefined (not the Javascript value undefined, rather there is no guarantees of what the value will be)

This point is clear to me.

But I’m interested in why this decision was taken.

I understand the desire to batch renders for performance reasons, but this does not seem to explain why the effects of .setState() cannot be synchronous; the two requirements are orthogonal to my understanding.

Please can someone point me towards some reading material?

Thanks


(Jed Fox) #2

@gaearon replied to an issue about this:

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: https://github.com/facebook/react/issues/122#issuecomment-81856416. Refs present the same problem: https://github.com/facebook/react/issues/122#issuecomment-22659651.

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: https://github.com/reactjs/react-redux/issues/86, https://github.com/reactjs/react-redux/pull/99, https://github.com/reactjs/react-redux/issues/292, https://github.com/reactjs/redux/issues/1415, https://github.com/reactjs/react-redux/issues/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 (https://github.com/facebook/react/issues/122#issuecomment-19888472).

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 :slightly_smiling_face:.

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 yet, but we definitely will document it 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 a concept 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, a new message is received 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.