[Fiber] Update input.defaultValue for type=number too (#36980)
We used to avoid updating .defaultValue until blur on number inputs because of a Chrome bug, but the relevant Chromium behavior was fixed in 2020 in M85 https://issues.chromium.org/issues/40124871 and likewise in WebKit https://bugs.webkit.org/show_bug.cgi?id=217156 so I think we can probably just remove this special-cased logic. We do still need some special logic for number inputs because it's the only type where we let users specify a non-string value that has multiple string representations and need to be careful not to clobber the .value if it has a slightly different string than what you'd get from casting a number to string. Fixes #29862. Tested with the number inputs fixture in current Chrome, Safari, and Firefox.
Sophie Alpert committed
Jul 9, 2026 at 19:58 UTC
c0c39a6b3907eaab35f43074949e2957a2a734c1
3 files changed
+25
-56
packages/react-dom-bindings/src/client/ReactDOMInput.js
+23
-26
@@ -13,7 +13,6 @@ import {getCurrentFiberOwnerNameInDevOrNull} from 'react-reconciler/src/ReactCur
13
import {getFiberCurrentPropsFromNode} from './ReactDOMComponentTree';
14
import {getToStringValue, toString} from './ToStringValue';
15
import {track, trackHydrated, updateValueIfChanged} from './inputValueTracking';
16
-import getActiveElement from './getActiveElement';
16
import {
17
disableInputAttributeSyncing,
18
enableHydrationChangeEvent,
@@ -121,10 +120,13 @@ export function updateInput(
120
if (value != null) {
121
if (type === 'number') {
122
if (
123
+ // "" == 0, so a cleared field wouldn't otherwise be restored to 0.
124
// $FlowFixMe[incompatible-type]
125
// $FlowFixMe[invalid-compare]
126
(value === 0 && node.value === '') ||
127
- // We explicitly want to coerce to number here if possible.
127
+ // We explicitly want to coerce to number here if possible, so that
128
+ // other spellings of the same number (e.g. "0.0" mid-edit) aren't
129
+ // clobbered while the user types.
130
// eslint-disable-next-line
131
node.value != (value as any)
132
) {
@@ -144,7 +146,7 @@ export function updateInput(
146
// whenever the defaultValue React prop has changed. When not present,
147
// React does nothing
148
if (defaultValue != null) {
147
- setDefaultValue(node, type, getToStringValue(defaultValue));
149
+ setDefaultValue(node, getToStringValue(defaultValue));
150
} else if (lastDefaultValue != null) {
151
node.removeAttribute('value');
152
}
@@ -155,9 +157,22 @@ export function updateInput(
157
// 2. The defaultValue React property
158
// 3. Otherwise there should be no change
159
if (value != null) {
158
- setDefaultValue(node, type, getToStringValue(value));
160
+ if (
161
+ type === 'number' &&
162
+ // We explicitly want to coerce to number here if possible.
163
+ // eslint-disable-next-line
164
+ node.value == (value as any)
165
+ ) {
166
+ // node.value may be a different spelling of the same number (e.g.
167
+ // "0.0" for 0). Mirror what's displayed, like the value setter does.
168
+ // Not redundant with the assignment above: browsers sanitize invalid
169
+ // assigned values to "", in which case we sync the React value below.
170
+ setDefaultValue(node, getToStringValue(node.value));
171
+ } else {
172
+ setDefaultValue(node, getToStringValue(value));
173
+ }
174
} else if (defaultValue != null) {
160
- setDefaultValue(node, type, getToStringValue(defaultValue));
175
+ setDefaultValue(node, getToStringValue(defaultValue));
176
} else if (lastDefaultValue != null) {
177
node.removeAttribute('value');
178
}
@@ -462,26 +477,8 @@ export function restoreControlledInputState(element: Element, props: Object) {
477
}
478
}
479
465
-// In Chrome, assigning defaultValue to certain input types triggers input validation.
466
-// For number inputs, the display value loses trailing decimal points. For email inputs,
467
-// Chrome raises "The specified value <x> is not a valid email address".
468
-//
469
-// Here we check to see if the defaultValue has actually changed, avoiding these problems
470
-// when the user is inputting text
471
-//
472
-// https://github.com/facebook/react/issues/7253
473
-export function setDefaultValue(
474
- node: HTMLInputElement,
475
- type: ?string,
476
- value: ToStringValue,
477
-) {
478
- if (
479
- // Focused number inputs synchronize on blur. See ChangeEventPlugin.js
480
- type !== 'number' ||
481
- getActiveElement(node.ownerDocument) !== node
482
- ) {
483
- if (node.defaultValue !== toString(value)) {
484
- node.defaultValue = toString(value);
485
- }
480
+function setDefaultValue(node: HTMLInputElement, value: ToStringValue) {
481
+ if (node.defaultValue !== toString(value)) {
482
+ node.defaultValue = toString(value);
483
}
484
}
packages/react-dom-bindings/src/events/plugins/ChangeEventPlugin.js
-25
@@ -23,10 +23,8 @@ import getEventTarget from '../getEventTarget';
23
import isEventSupported from '../isEventSupported';
24
import {getNodeFromInstance} from '../../client/ReactDOMComponentTree';
25
import {updateValueIfChanged} from '../../client/inputValueTracking';
26
-import {setDefaultValue} from '../../client/ReactDOMInput';
26
import {enqueueStateRestore} from '../ReactDOMControlledComponent';
27
29
-import {disableInputAttributeSyncing} from 'shared/ReactFeatureFlags';
28
import {batchedUpdates} from '../ReactDOMUpdateBatching';
29
import {
30
processDispatchQueue,
@@ -260,20 +258,6 @@ function getTargetInstForInputOrChangeEvent(
258
}
259
}
260
263
-function handleControlledInputBlur(node: HTMLInputElement, props: any) {
264
- if (node.type !== 'number') {
265
- return;
266
- }
267
-
268
- if (!disableInputAttributeSyncing) {
269
- const isControlled = props.value != null;
270
- if (isControlled) {
271
- // If controlled, assign the value attribute to the current value on blur
272
- setDefaultValue(node as any, 'number', (node as any).value);
273
- }
274
- }
275
-}
276
-
261
/**
262
* This plugin creates an `onChange` event that normalizes change events
263
* across form elements. This event fires at a time when it's possible to
@@ -330,15 +314,6 @@ function extractEvents(
314
if (handleEventFunc) {
315
handleEventFunc(domEventName, targetNode, targetInst);
316
}
333
-
334
- // When blurring, set the value attribute for number inputs
335
- if (domEventName === 'focusout' && targetInst) {
336
- // These props aren't necessarily the most current but we warn for changing
337
- // between controlled and uncontrolled, so it doesn't matter and the previous
338
- // code was also broken for changes.
339
- const props = targetInst.memoizedProps;
340
- handleControlledInputBlur(targetNode as any as HTMLInputElement, props);
341
- }
317
}
318
319
export {registerEvents, extractEvents};
packages/react-dom/src/__tests__/ReactDOMInput-test.js
+2
-5
@@ -1035,9 +1035,6 @@ describe('ReactDOMInput', () => {
1035
expect(node.value).toBe('0.0');
1036
expect(node.hasAttribute('value')).toBe(false);
1037
} else {
1038
- dispatchEventOnNode(node, 'blur');
1039
- dispatchEventOnNode(node, 'focusout');
1040
-
1038
expect(node.value).toBe('0.0');
1039
expect(node.getAttribute('value')).toBe('0.0');
1040
}
@@ -2664,7 +2661,7 @@ describe('ReactDOMInput', () => {
2661
}
2662
});
2663
2667
- it('does not set the value attribute on number inputs if focused', async () => {
2664
+ it('sets the value attribute on number inputs even when focused', async () => {
2665
const Input = getTestInput();
2666
await act(() => {
2667
root.render(<Input type="number" value="1" />);
@@ -2681,7 +2678,7 @@ describe('ReactDOMInput', () => {
2678
if (disableInputAttributeSyncing) {
2679
expect(node.hasAttribute('value')).toBe(false);
2680
} else {
2684
- expect(node.getAttribute('value')).toBe('1');
2681
+ expect(node.getAttribute('value')).toBe('2');
2682
}
2683
});
2684