Skip to content

fix: preserve ComboBox selection when a controlled value is applied asynchronously - #10504

Open
starboyvarun wants to merge 1 commit into
adobe:mainfrom
starboyvarun:fix/combobox-async-controlled-selection
Open

fix: preserve ComboBox selection when a controlled value is applied asynchronously#10504
starboyvarun wants to merge 1 commit into
adobe:mainfrom
starboyvarun:fix/combobox-async-controlled-selection

Conversation

@starboyvarun

Copy link
Copy Markdown
Contributor

Closes #4621

🎯 Intent

When a ComboBox is driven by a form library (Formik, react-hook-form), onSelectionChange fires a
spurious extra call — the selected key, then immediately null — and the selection the user just
made is thrown away. The goal is that one user selection produces exactly one onSelectionChange
call, regardless of how quickly the app feeds the controlled value back.

🔍 What was happening

A fully controlled ComboBox (both selectedKey/value and inputValue owned by the app) reads the
selection out of the last rendered props. Form libraries run validation before they apply the new
value, so the update lands in a later render than the one that reported it. In the window in
between, the ComboBox still renders the previous selection.

If the field is committed during that window — blurring or tabbing away right after picking an
option — two things go wrong:

  • commit() doesn't recognise the focused option as selected (the rendered selection hasn't caught
    up), so it re-selects it and fires onSelectionChange a second time with the same key.
  • commitSelection() then reports the rendered selection back to the app to nudge it to re-sync
    inputValue. That rendered selection is stale — null — so it clobbers the pending selection.

Net result for one click: onSelectionChange('2'), onSelectionChange('2'), onSelectionChange(null),
and an empty input. This is the "called twice, first with the selected value and immediately after
with null" from the issue, and why the reported workarounds (setTimeout, disabling validation,
keying the component off the error) all worked — each one removes the deferred update.

🔧 How this fixes it

useComboBoxState now remembers, in pendingValueRef, a selection it has already reported but that
the controlled value hasn't reflected back yet. The ref is set when setValue reports a new key and
cleared as soon as the controlled value changes, so it is only ever set during that in-between window
(and never in multiple selection mode, where the value isn't reported this way).

With that memory in place:

  • commit() treats the focused option as already selected, so it stops re-selecting it.
  • commitSelection() skips reporting the selection back while an update is pending — the app already
    knows about it — and just closes the menu.

Nothing changes when the app applies the value synchronously: the ref is cleared by the time anything
reads it, so the existing behaviour (including the "sync inputValue back on blur" nudge for unmatched
text) is untouched.

✅ Pull Request Checklist:

  • Included link to corresponding React Spectrum GitHub Issue.
  • Added/updated unit tests and storybook for this change (for new code or code which already has tests).
  • Filled out test instructions.
  • Updated documentation (if it already exists for this component).
  • Looked at the Accessibility Practices for this feature - Aria Practices
  • I understand every change in this PR and can explain why it's there.
  • If AI-assisted, I followed our AI contribution guidance and pointed my assistant at CLAUDE.md.

📝 Test Instructions:

New Jest test in packages/react-aria-components/test/ComboBox.test.js: "should not clear the
selection when a fully controlled value is applied asynchronously". It renders a fully controlled
ComboBox whose handler applies selectedKey/inputValue on a timer (standing in for a form
library's validation pass), picks an option, and tabs away before the update lands. Without the fix
it records 3 onSelectionChange calls and an empty input; with it, 1 call and Dog in the input.

To check by hand: wire a ComboBox to Formik or react-hook-form with mode: 'onChange' and a
controlled selectedKey + inputValue, pick an option from the menu, and immediately tab out of the
field. The selection should stick, and onSelectionChange should fire once.

Tested with mouse and keyboard. No visual, RTL, or styling changes — this is state-layer only.

🧢 Your Project:

N/A

…synchronously

Form libraries such as Formik and react-hook-form apply the controlled value after
running validation, so it lands in a later render than the one that reported it.
Committing the field during that window - blurring right after picking an option -
made commit() re-select the focused option and made commitSelection() report the
stale rendered selection back, firing onSelectionChange twice and then with null.

Track a selection that has been reported but not yet reflected back, so commit()
recognises it as already selected and commitSelection() skips reporting it again.

Closes adobe#4621
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.

ComboBox calls onSelectionChange with null value when helpers.setValue is used in Formik

1 participant