-
Notifications
You must be signed in to change notification settings - Fork 1.4k
Description
Provide a general summary of the issue here
There is an inconsistency in useNumberField between how commit()
is called on blur vs Enter keydown.
On blur, commit() is correctly wrapped in flushSync:
// onBlur — correct ✓
let commitAndAnnounce = useCallback(() => {
flushSync(() => {
commit();
});
}, [commit, inputRef]);
On Enter keydown, commit() is called without flushSync:
// onKeyDownEnter — inconsistent ✗
let onKeyDownEnter = useCallback((e) => {
if (e.key === 'Enter') {
commit(); // no flushSync
commitValidation();
// e.continuePropagation() never called
} else {
e.continuePropagation();
}
}, [commit, commitValidation]);
This works fine with native HTML forms because the browser
reads from the hidden input after commit() has updated
state.numberValue. But with controlled form libraries
(TanStack Form, React Hook Form, Formik etc.) that read
React state directly in their onSubmit handlers, the state
hasn't flushed yet and the stale/default value is submitted
silently with no error or warning.
Additionally the Enter event is completely swallowed and never
propagates to the parent, making it impossible for controlled
form libraries to even know Enter was pressed and work around
this themselves.
🤔 Expected Behavior?
When a user types a value and presses Enter, the form should submit the typed value, the same as when the user blurs the field and then submits.commit() on Enter should be wrapped in flushSync, consistent
with how onBlur already handles it:
// consistent with onBlur ✓
if (e.key === 'Enter') {
flushSync(() => {
commit();
});
commitValidation();
e.continuePropagation();
}
This would ensure that any consumer reading React state
immediately after Enter — whether a controlled form library
or custom code — gets the committed value, not the stale one.
😯 Current Behavior
When user types a value in NumberField and presses Enter:
- react-aria intercepts Enter in onKeyDownEnter
- commit() is called WITHOUT flushSync, state update is
scheduled but not flushed to React state yet - Enter event is swallowed, e.continuePropagation() is
never called, parent form never sees the Enter key - Browser natively synthesizes a trusted click on the
form's submit button. - Controlled form library's onSubmit fires and reads
React state before commit() has flushed - Stale default value is submitted silently
Verified with console logs:
- Enter never appears in form's onKeyDown (event swallowed)
- submit button: isTrusted = true (browser synthesized click)
- form state at submit time = defaultValue, not typed value
Works correctly with native HTML forms because the browser
reads from the hidden input AFTER commit() has updated
state.numberValue synchronously. The bug surface only becomes
visible with controlled form libraries.
Potential risk beyond this specific case:
Even in native form setups, this inconsistency is a latent
hazard. In heavy applications with complex React trees,
concurrent rendering, or custom event handling, the lack of
flushSync on Enter could cause unpredictable state ordering
issues that are very difficult to debug. The fact that blur
explicitly uses flushSync suggests the react-aria team already
recognized that commit() needs synchronous flushing, the
same reasoning applies equally to Enter.
💁 Possible Solution
Wrap commit() in flushSync inside onKeyDownEnter, consistent
with onBlur:
if (e.key === 'Enter') {
flushSync(() => {
commit();
});
commitValidation();
e.continuePropagation(); // let parent handle Enter
}
This is a low risk change flushSync is already used
elsewhere in the same file for exactly this reason.
🔦 Context
Discovered while using NumberField with TanStack Form inside
a dialog. Pressing Enter submits the form with the default
value instead of what the user typed. Manually blurring the
field before submitting works correctly, confirming the issue
is purely the missing flushSync on the Enter path.
The inconsistency between blur and Enter is the root cause.
Native HTML forms happen to work around it because they read
from the DOM hidden input rather than React state. Controlled
form libraries have no such escape hatch and are directly
exposed to this race condition.
🖥️ Steps to Reproduce
- Create a NumberField inside a controlled form
(TanStack Form, React Hook Form, Formik etc.) - Set a defaultValue (e.g. 1)
- Type a new value (e.g. 5)
- Press Enter without blurring the field first
- Observe: form submits with defaultValue (1) not typed value (5)
- Repeat but click outside field first, then submit
- Observe: works correctly when field is blurred first
The bug does not reproduce with native HTML form submission
because the browser reads from the hidden input after
commit() has already updated state.numberValue.
Version
1.12.0
What browsers are you seeing the problem on?
Chrome
If other, please specify.
No response
What operating system are you using?
Ubuntu, MacOS
🧢 Your Company/Team
No response
🕷 Tracking Issue
No response