diff --git a/packages/react-dom-bindings/src/client/ReactDOMInput.js b/packages/react-dom-bindings/src/client/ReactDOMInput.js index 1416df94ebdc9..a1805c9e65a64 100644 --- a/packages/react-dom-bindings/src/client/ReactDOMInput.js +++ b/packages/react-dom-bindings/src/client/ReactDOMInput.js @@ -92,38 +92,17 @@ export function updateInputChecked(element: Element, props: Object) { export function updateInput(element: Element, props: Object) { const node: HTMLInputElement = (element: any); - updateInputChecked(element, props); - const value = getToStringValue(props.value); const type = props.type; - if (value != null) { - if (type === 'number') { - if ( - // $FlowFixMe[incompatible-type] - (value === 0 && node.value === '') || - // We explicitly want to coerce to number here if possible. - // eslint-disable-next-line - node.value != (value: any) - ) { - node.value = toString((value: any)); - } - } else if (node.value !== toString((value: any))) { - node.value = toString((value: any)); - } - } else if (type === 'submit' || type === 'reset') { - // Submit/reset inputs need the attribute removed completely to avoid - // blank-text buttons. - node.removeAttribute('value'); - return; - } - if (disableInputAttributeSyncing) { // When not syncing the value attribute, React only assigns a new value // whenever the defaultValue React prop has changed. When not present, // React does nothing if (props.defaultValue != null) { setDefaultValue(node, props.type, getToStringValue(props.defaultValue)); + } else { + node.removeAttribute('value'); } } else { // When syncing the value attribute, the value comes from a cascade of @@ -135,6 +114,8 @@ export function updateInput(element: Element, props: Object) { setDefaultValue(node, props.type, value); } else if (props.defaultValue != null) { setDefaultValue(node, props.type, getToStringValue(props.defaultValue)); + } else { + node.removeAttribute('value'); } } @@ -154,6 +135,29 @@ export function updateInput(element: Element, props: Object) { node.defaultChecked = !!props.defaultChecked; } } + + updateInputChecked(element, props); + + if (value != null) { + if (type === 'number') { + if ( + // $FlowFixMe[incompatible-type] + (value === 0 && node.value === '') || + // We explicitly want to coerce to number here if possible. + // eslint-disable-next-line + node.value != (value: any) + ) { + node.value = toString((value: any)); + } + } else if (node.value !== toString((value: any))) { + node.value = toString((value: any)); + } + } else if (type === 'submit' || type === 'reset') { + // Submit/reset inputs need the attribute removed completely to avoid + // blank-text buttons. + node.removeAttribute('value'); + return; + } } export function postInitInput( diff --git a/packages/react-dom/src/__tests__/DOMPropertyOperations-test.js b/packages/react-dom/src/__tests__/DOMPropertyOperations-test.js index bf523c4494e81..67d6f7bd465ab 100644 --- a/packages/react-dom/src/__tests__/DOMPropertyOperations-test.js +++ b/packages/react-dom/src/__tests__/DOMPropertyOperations-test.js @@ -1166,11 +1166,7 @@ describe('DOMPropertyOperations', () => { ).toErrorDev( 'A component is changing a controlled input to be uncontrolled', ); - if (disableInputAttributeSyncing) { - expect(container.firstChild.hasAttribute('value')).toBe(false); - } else { - expect(container.firstChild.getAttribute('value')).toBe('foo'); - } + expect(container.firstChild.hasAttribute('value')).toBe(false); expect(container.firstChild.value).toBe('foo'); }); diff --git a/packages/react-dom/src/__tests__/ReactDOMInput-test.js b/packages/react-dom/src/__tests__/ReactDOMInput-test.js index f829865efa5ba..47fca9522fac8 100644 --- a/packages/react-dom/src/__tests__/ReactDOMInput-test.js +++ b/packages/react-dom/src/__tests__/ReactDOMInput-test.js @@ -1952,11 +1952,7 @@ describe('ReactDOMInput', () => { expect(renderInputWithStringThenWithUndefined).toErrorDev( 'A component is changing a controlled input to be uncontrolled.', ); - if (disableInputAttributeSyncing) { - expect(input.getAttribute('value')).toBe(null); - } else { - expect(input.getAttribute('value')).toBe('latest'); - } + expect(input.getAttribute('value')).toBe(null); }); it('preserves the value property', () => { @@ -2002,11 +1998,7 @@ describe('ReactDOMInput', () => { 'or `undefined` for uncontrolled components.', 'A component is changing a controlled input to be uncontrolled.', ]); - if (disableInputAttributeSyncing) { - expect(input.hasAttribute('value')).toBe(false); - } else { - expect(input.getAttribute('value')).toBe('latest'); - } + expect(input.hasAttribute('value')).toBe(false); }); it('preserves the value property', () => { @@ -2165,4 +2157,30 @@ describe('ReactDOMInput', () => { expect(node.hasAttribute('value')).toBe(false); }); }); + + it('should remove previous `defaultValue`', () => { + const node = ReactDOM.render( + , + container, + ); + + expect(node.value).toBe('0'); + expect(node.defaultValue).toBe('0'); + + ReactDOM.render(, container); + expect(node.defaultValue).toBe(''); + }); + + it('should treat `defaultValue={null}` as missing', () => { + const node = ReactDOM.render( + , + container, + ); + + expect(node.value).toBe('0'); + expect(node.defaultValue).toBe('0'); + + ReactDOM.render(, container); + expect(node.defaultValue).toBe(''); + }); });