#4121·primitives

[Popover + Switch/Checkbox] "flushSync was called from inside a lifecycle method" when a BubbleInput mounts while a Popover is dismissing, inside a <form>

Author: ViktorBjornCreated Aug 17, 2026Updated Aug 20, 2026

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

typescript
<form noValidate>
  <Popover>
    <PopoverTrigger>Open</PopoverTrigger>
    <PopoverContent>{/* anything */}</PopoverContent>
  </Popover>
  <Switch />   {/* or Checkbox */}
</form>
  1. Open the popover.
  2. Click inside it.
  3. Click the Switch/Checkbox without 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, not flushSync), 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.