[Popover + Switch/Checkbox] "flushSync was called from inside a lifecycle method" when a BubbleInput mounts while a Popover is dismissing, inside a <form>
Bug report
Current Behavior
React logs:
flushSync was called from inside a lifecycle method. React cannot flush when
React is already rendering. Consider moving this call to a scheduler task or micro task.when a Switch or Checkbox mounts inside a <form> while an open Popover is being dismissed by the same click.
Stack trace is entirely Radix + React-DOM internals — no frames from application code:
exports.flushSync react-dom
dispatchDiscreteCustomEvent @radix-ui
handleAndDispatchCustomEvent @radix-ui
handleAndDispatchPointerDownOutsideEvent @radix-ui — DismissableLayer outside-click detection
SwitchBubbleInput.useEffect @radix-ui/react-switch
… commitHookEffectListMount / commitPassiveMountOnFiber (×~140)SwitchBubbleInput is the hidden native <input> that lets a Switch participate in real HTML forms. Its mount effect calls into the still-open Popover's outside-click dispatch, which calls flushSync — during a passive-effect commit. That is precisely what React's guard exists to catch.
Dev-only: React strips this diagnostic from production builds.
Expected behavior
No warning. The dismissal dispatch should not run flushSync re-entrantly inside a passive-effect commit — deferring it to a microtask or scheduler task, as React's own message suggests, would presumably be enough.
Reproduction
<form noValidate>
<Popover>
<PopoverTrigger>Open</PopoverTrigger>
<PopoverContent>{/* anything */}</PopoverContent>
</Popover>
<Switch /> {/* or Checkbox */}
</form>- Open the popover.
- Click inside it.
- Click the
Switch/Checkboxwithout closing the popover first, so the click is handled as outside-click dismissal.
What isolates it
Six reproductions, each removing one variable. The deciding pair is 5 vs 6:
| # | Contents | Reproduces |
|---|---|---|
| 1 | Bare Popover + Switch/Checkbox, useState, no form |
No |
| 2 | Same, with a real date-picker calendar inside the popover | No |
| 3 | Same, wired through react-hook-form's Controller |
No |
| 4 | A real 3-field form (date + switch + checkbox) |
Yes |
| 5 | The same field renderers, hand-composed, no <form> |
No |
| 6 | The same renderers in a bare <form noValidate> — no form library, no context, no providers |
Yes |
A native <form> ancestor is the trigger, and nothing else is. Not the date picker (2, 3 used a real calendar), not react-hook-form (3), not tree size or field count, not any form-library context (6 has none).
It also requires the popover to be still open when the click lands — dismiss-by-click, not dismiss-by-Escape. Escape-then-click never reproduced. And it is not Switch-specific: Checkbox behaves identically, which fits, since CheckboxBubbleInput is the same pattern.
Suggested cause
The BubbleInput components are the form-aware part, which is consistent with a <form> ancestor being the trigger — plausibly a closest("form") check or a native reset/submit listener changing effect timing. Stated as a hypothesis, not a finding: we did not read the unminified source to confirm why the form ancestor unlocks it, only that it reliably does.
Not tested
Whether Dialog / DropdownMenu fire it too. All three share DismissableLayer, so they plausibly do — this issue names Popover because that is where we hit it.
Prior art checked
- #2549 "[Checkbox] Controlled Checkbox inside a form will trigger an infinite loop" — same "inside a form" trigger, different symptom (
Maximum update depth exceeded, notflushSync), and closed. - #4014 "[DismissableLayer] Escape handlers go stale on React 19.2" — same subsystem, different failure.
Neither looks like a duplicate, but happy to be pointed at one if this is already tracked.
Context
Found while maintaining a component library built on Radix. There is no fix available on our side: the stack contains none of our code, and the behaviour is internal to the dismissal dispatch. Reporting rather than working around it, since a workaround here would mean reaching into internals with no version guarantee.
Source: radix-ui/primitives