diff --git a/packages/react-dom/src/client/ReactDOMFiberInput.js b/packages/react-dom/src/client/ReactDOMFiberInput.js index 8547aa50c1f2a..d8fc9bf32d1ac 100644 --- a/packages/react-dom/src/client/ReactDOMFiberInput.js +++ b/packages/react-dom/src/client/ReactDOMFiberInput.js @@ -14,12 +14,15 @@ import warning from 'shared/warning'; import * as DOMPropertyOperations from './DOMPropertyOperations'; import {getFiberCurrentPropsFromNode} from './ReactDOMComponentTree'; +import {getSafeValue, safeValueToString} from './safeValue'; import ReactControlledValuePropTypes from '../shared/ReactControlledValuePropTypes'; import * as inputValueTracking from './inputValueTracking'; +import type {SafeValue} from './SafeValue'; + type InputWithWrapperState = HTMLInputElement & { _wrapperState: { - initialValue: string, + initialValue: SafeValue, initialChecked: ?boolean, controlled?: boolean, }, @@ -174,13 +177,14 @@ export function updateWrapper(element: Element, props: Object) { if (props.type === 'number') { if ( (value === 0 && node.value === '') || + // We explicitly want to coerce to number here if possible. // eslint-disable-next-line - node.value != value + node.value != (value: any) ) { - node.value = '' + value; + node.value = safeValueToString(value); } - } else if (node.value !== '' + value) { - node.value = '' + value; + } else if (node.value !== safeValueToString(value)) { + node.value = safeValueToString(value); } } @@ -203,7 +207,7 @@ export function postMountWrapper( const node = ((element: any): InputWithWrapperState); if (props.hasOwnProperty('value') || props.hasOwnProperty('defaultValue')) { - const initialValue = '' + node._wrapperState.initialValue; + const initialValue = safeValueToString(node._wrapperState.initialValue); const currentValue = node.value; // Do not assign value if it is already set. This prevents user text input @@ -312,23 +316,9 @@ export function setDefaultValue( node.ownerDocument.activeElement !== node ) { if (value == null) { - node.defaultValue = '' + node._wrapperState.initialValue; - } else if (node.defaultValue !== '' + value) { - node.defaultValue = '' + value; + node.defaultValue = safeValueToString(node._wrapperState.initialValue); + } else if (node.defaultValue !== safeValueToString(value)) { + node.defaultValue = safeValueToString(value); } } } - -function getSafeValue(value: *): * { - switch (typeof value) { - case 'boolean': - case 'number': - case 'object': - case 'string': - case 'undefined': - return value; - default: - // function, symbol are assigned as empty strings - return ''; - } -} diff --git a/packages/react-dom/src/client/SafeValue.js b/packages/react-dom/src/client/SafeValue.js new file mode 100644 index 0000000000000..5dca73eaa1ed2 --- /dev/null +++ b/packages/react-dom/src/client/SafeValue.js @@ -0,0 +1,31 @@ +/** + * Copyright (c) 2013-present, Facebook, Inc. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + * @flow + */ + +export opaque type SafeValue = boolean | number | Object | string | null | void; + +// Flow does not allow string concatenation of most non-string types. To work +// around this limitation, we use an opaque type that can only be obtained by +// passing the value through getSafeValue first. +export function safeValueToString(value: SafeValue): string { + return '' + (value: any); +} + +export function getSafeValue(value: mixed): SafeValue { + switch (typeof value) { + case 'boolean': + case 'number': + case 'object': + case 'string': + case 'undefined': + return value; + default: + // function, symbol are assigned as empty strings + return ''; + } +}