feat: add renderAsync to fix React-Aria useLayoutEffect flushing (#1339)#1463
Open
Ayoub-glitsh wants to merge 4 commits into
Open
feat: add renderAsync to fix React-Aria useLayoutEffect flushing (#1339)#1463Ayoub-glitsh wants to merge 4 commits into
Ayoub-glitsh wants to merge 4 commits into
Conversation
…ting-library#1339) Components like React-Aria ComboBox set ARIA attributes (role, aria-expanded, aria-label, etc.) inside useLayoutEffect and often trigger a setState there, causing a second render cycle. The synchronous render() wraps the initial paint in act() but does not guarantee that the useLayoutEffect → setState → re-render chain completes before returning. This makes the DOM appear half-initialised in consuming-application tests even though the component works correctly at runtime. Changes: - Extract buildRenderResult() to share result-object construction between sync and async render paths - Extract buildRoot() and resolveRenderOptions() to eliminate duplication between render() and renderAsync() - Add renderRootAsync() internal helper that uses await act(async () => {...}) to drain the full microtask queue and all pending layout-effect cycles - Add renderAsync() public API — same signature as render(), returns a Promise that resolves once every effect (including useLayoutEffect chains) has flushed - Add TypeScript overloads for renderAsync in types/index.d.ts - Add test suite src/__tests__/render-async.js covering ARIA attribute presence, second render cycle completion, rerender, unmount, wrapper, and StrictMode Fixes testing-library#1339
|
This pull request is automatically built and testable in CodeSandbox. To see build info of the built libraries, click here or the icon next to each commit SHA. Latest deployment of this branch, based on commit ec4aa85:
|
…ches - Add hydrate test to cover the renderRootAsync hydrate path (L250) - Add istanbul ignore next on legacyRoot guards in render/renderAsync/renderHook These guards are only reachable on React 18 (tested via testGateReact18) and are not executed in the React 19 CI matrix job, matching the same pattern used in the original code before the refactor
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Problem
Closes #1339
Components like React-Aria
ComboBoxset ARIA attributes (role,aria-expanded,aria-label, etc.) insideuseLayoutEffect, and often trigger asetStatethere — causing a second render cycle. The synchronousrender()wraps the initial paint inact()but does not guarantee that theuseLayoutEffect → setState → re-renderchain completes before returning.This makes the DOM appear half-initialised in consuming-application tests:
getByRole('combobox')throws becauserolehasn't been set yetaria-*attributes are missinguserEvent.type()has no effect because the input isn't fully wired upThe component works correctly at runtime and in the component library's own tests, but breaks in any consuming app's test suite.
Solution
Add a new
renderAsyncutility that usesawait act(async () => {...})to fully drain the microtask queue and all pendinguseLayoutEffectcycles before resolving.Usage