Skip to content

feat: add renderAsync to fix React-Aria useLayoutEffect flushing (#1339)#1463

Open
Ayoub-glitsh wants to merge 4 commits into
testing-library:mainfrom
Ayoub-glitsh:fix/render-async-aria-effects
Open

feat: add renderAsync to fix React-Aria useLayoutEffect flushing (#1339)#1463
Ayoub-glitsh wants to merge 4 commits into
testing-library:mainfrom
Ayoub-glitsh:fix/render-async-aria-effects

Conversation

@Ayoub-glitsh
Copy link
Copy Markdown

Problem

Closes #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:

  • getByRole('combobox') throws because role hasn't been set yet
  • aria-* attributes are missing
  • userEvent.type() has no effect because the input isn't fully wired up

The 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 renderAsync utility that uses await act(async () => {...}) to fully drain the microtask queue and all pending useLayoutEffect cycles before resolving.

Usage

// Before (broken with React-Aria):
const { getByPlaceholderText } = render(<MyComboBox />)
const input = getByPlaceholderText('Type to Start Searching...')
// role / aria-* attributes not set yet ❌

// After (fixed):
const { getByRole } = await renderAsync(<MyComboBox />)
const input = getByRole('combobox') // fully initialised ✓
await userEvent.type(input, 'racc')
await waitFor(() => expect(fetchMock).toHaveBeenCalledWith('/api/MYAPI?term=racc', ...))

…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
@codesandbox-ci
Copy link
Copy Markdown

codesandbox-ci Bot commented May 18, 2026

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:

Sandbox Source
react-testing-library-examples Configuration

…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
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

React-Aria Component Combobox is not Fully rendering in Consuming Application Tests

1 participant