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: createRoot().render() is not fully synchronous unless wrapped in flushSync, unlike legacy ReactDOM.render #32811

Closed
jukben opened this issue Apr 3, 2025 · 2 comments · Fixed by reactjs/react.dev#7717
Labels
Status: Unconfirmed A potential issue that we haven't yet confirmed as a bug

Comments

@jukben
Copy link

jukben commented Apr 3, 2025

React version:
18.2.0 (reproduced in latest 19.1 as well)


Summary

There is an open issue proposing documentation updates around the use of flushSync with createRoot().render() to enforce synchronous rendering. However, since that issue has gone without feedback for several months, and given the core behavior observed, I suspect this may be an actual bug in React rather than just a documentation gap. I'm opening this issue here in the main React repository to clarify whether this behavior is intentional or not.


Steps To Reproduce

  1. Render a basic React app using createRoot(...).render(<App />).
  2. Inside <App />, log messages from useLayoutEffect and useEffect.
  3. Log messages before and after root.render() call.
  4. Compare behavior between:
    • createRoot(...).render(...) (modern API)
    • flushSync(() => root.render(...))
    • Legacy ReactDOM.render(...)

Link to code example: CodeSandbox


The current behavior

Using createRoot(...).render(...) without wrapping in flushSync, we observe that:

  • The log inside useLayoutEffect is emitted after console.log("Post root.render"), indicating the effect happens after the render call resolves.
  • In contrast, using flushSync(() => root.render(...)) or legacy ReactDOM.render(...), useLayoutEffect is called before the post-render log.

This suggests the modern root API allows post-render behavior to interleave with layout effects, even outside of explicitly opted-in concurrent features. This might violate expectations around synchronous behavior of the initial render.


The expected behavior

As @rickhanlonii noted in a related discussion, the initial render should be synchronous by default. If that’s the case, this behavior seems inconsistent.

  • createRoot(...).render(...) should preserve layout effect timing consistency with legacy ReactDOM.render(...), without needing to wrap in flushSync.
  • If the current behavior is intentional, it would help to explicitly document that createRoot(...).render(...) may be async under certain conditions—even without concurrent features being enabled.
@jukben jukben added the Status: Unconfirmed A potential issue that we haven't yet confirmed as a bug label Apr 3, 2025
@eps1lon
Copy link
Collaborator

eps1lon commented Apr 3, 2025

Once we start rendering, it's synchronous. But the actual render() call is not and always runs in the next microtask. It's the same as setState in that regard.

We can document in caveats under https://react.dev/reference/react-dom/client/createRoot#root-render. Could you file a PR making these changes? Similar for hydrateRoot

@jukben
Copy link
Author

jukben commented Apr 4, 2025

Hey @eps1lon thanks a lot for clarifying! I put together this draft, could you take a look reactjs/react.dev#7717

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

Successfully merging a pull request may close this issue.

2 participants