fix: preserve ComboBox selection when a controlled value is applied asynchronously - #10504
Open
starboyvarun wants to merge 1 commit into
Open
fix: preserve ComboBox selection when a controlled value is applied asynchronously#10504starboyvarun wants to merge 1 commit into
starboyvarun wants to merge 1 commit into
Conversation
…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
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.
Closes #4621
🎯 Intent
When a ComboBox is driven by a form library (Formik, react-hook-form),
onSelectionChangefires aspurious extra call — the selected key, then immediately
null— and the selection the user justmade is thrown away. The goal is that one user selection produces exactly one
onSelectionChangecall, regardless of how quickly the app feeds the controlled value back.
🔍 What was happening
A fully controlled ComboBox (both
selectedKey/valueandinputValueowned by the app) reads theselection 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 caughtup), so it re-selects it and fires
onSelectionChangea second time with the same key.commitSelection()then reports the rendered selection back to the app to nudge it to re-syncinputValue. 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
useComboBoxStatenow remembers, inpendingValueRef, a selection it has already reported but thatthe controlled value hasn't reflected back yet. The ref is set when
setValuereports a new key andcleared 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 alreadyknows 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:
📝 Test Instructions:
New Jest test in
packages/react-aria-components/test/ComboBox.test.js: "should not clear theselection when a fully controlled value is applied asynchronously". It renders a fully controlled
ComboBox whose handler applies
selectedKey/inputValueon a timer (standing in for a formlibrary's validation pass), picks an option, and tabs away before the update lands. Without the fix
it records 3
onSelectionChangecalls and an empty input; with it, 1 call andDogin the input.To check by hand: wire a ComboBox to Formik or react-hook-form with
mode: 'onChange'and acontrolled
selectedKey+inputValue, pick an option from the menu, and immediately tab out of thefield. The selection should stick, and
onSelectionChangeshould fire once.Tested with mouse and keyboard. No visual, RTL, or styling changes — this is state-layer only.
🧢 Your Project:
N/A