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

Bug: State updates are reordered across await in an effect in React 18. #30890

Closed
Dakota-Smith-FL opened this issue Sep 5, 2024 · 5 comments
Closed
Labels
Status: Unconfirmed A potential issue that we haven't yet confirmed as a bug

Comments

@Dakota-Smith-FL
Copy link

Dakota-Smith-FL commented Sep 5, 2024

React version: 18.3.1

Steps To Reproduce

  1. Create an effect of the following form:
useEffect(() => {
    (async () => {
        setStateA("foo");
        await someAsyncFunctionThatReturnsImmediately();
        setStateA("bar");
    })();
});

Link to code example: https://codesandbox.io/p/sandbox/react-dev-forked-9m77xd

The current behavior

React will render with the second state update first ("bar"), and then re-render with the first update ("foo"). This only happens when the awaited function returns immediately. If the awaited promise resolves at a later time, the state is updated in the expected order.

In the example, you can observe this happening by clicking the button and looking at the console ("BUG RENDERED!!!😱😱😱"), or by seeing a brief flash of red on the component as it renders inaccurately. The console message triggers every time, the red flash only occurs occasionally. In our actual app we're seeing the render occur every time.

If you adjust the example to instead call await optionalDelay(true), the state updates will be applied in order and the component will flash green.

The expected behavior

The state updates should be applied in the order as specified in the code. This was the behavior in React 16, which can be tested in the sandbox.

This appears to be fixed in React 19.0.0-rc.0.

Previously reported in:

@Dakota-Smith-FL Dakota-Smith-FL added the Status: Unconfirmed A potential issue that we haven't yet confirmed as a bug label Sep 5, 2024
@Suman-D-R
Copy link

To fix this issue, you can leverage the functional form of setState, which allows React to ensure that state updates are applied in order. Here’s how you can modify your code

useEffect(() => {
  (async () => {
    setStateA((prev) => "foo"); // Using the functional form of setState
    await someAsyncFunctionThatReturnsImmediately();
    setStateA((prev) => "bar"); // Using the functional form of setState
  })();
}, []);

@rickhanlonii
Copy link
Member

rickhanlonii commented Sep 10, 2024

Overview

Thanks for reporting and the detailed sandbox. This is a bug fixed in React 19.

To show what's happening, I updated your sandbox with some profiling markers and delays: https://codesandbox.io/p/sandbox/react-dev-forked-rt7rdr?file=%2FLibraryReports.js%3A8%2C1

React 18 Timeline

Here's the timeline for 18:

Screenshot 2024-09-10 at 1 02 22 PM

Here you can see that the first update is scheduled (as a default priority), then the microtask is scheduled (for the await). The microtask is fired by the browser, which schedules the second update (which is sync priority).

In React 18, we did not batch default and sync lane updates together even though they are both sync updates. Since the sync priority update is given higher priority than the default priority, it is flushed and committed first (that's why you see a flash of this state). Then we render the default priority update. Since it was interrupted by the lower priority update, we rebase it on top of the sync update, and render the result of both state updates together (that's why you see both state updaters run in the last render).

So the final committed state is correct (as long as you're using a reducer / state updater), but there's a flash of incorrect state in between.

React 19 Timeline

Here's the timeline for 19:

Screenshot 2024-09-10 at 1 18 17 PM

Here you can see both updates are scheduled the same as before, but now there is a single render with both updates included. This avoids the flash of intermediate state.

Shout out @tyao1 and @acdlite for working on this with for 19: #25700

@Dakota-Smith-FL
Copy link
Author

Thank you very much for the detailed explanation of what's going on under the hood, and the confirmation that this is indeed fixed in React 19.

Do you happen to know if there will be a React 18 release that includes this fix?

@josephsavona
Copy link
Contributor

josephsavona commented Sep 12, 2024

The changes to fix this are significant and not something we can backport. So unfortunately this fix will be 19 only.

@Dakota-Smith-FL
Copy link
Author

No problem. Thanks for the info.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Status: Unconfirmed A potential issue that we haven't yet confirmed as a bug
Projects
None yet
Development

No branches or pull requests

4 participants