;
}
}
if (this.state.shouldThrow) {
diff --git a/packages/react-debug-tools/src/__tests__/ReactHooksInspectionIntegration-test.js b/packages/react-debug-tools/src/__tests__/ReactHooksInspectionIntegration-test.js
index b13bec22d213c..013caecb11ba0 100644
--- a/packages/react-debug-tools/src/__tests__/ReactHooksInspectionIntegration-test.js
+++ b/packages/react-debug-tools/src/__tests__/ReactHooksInspectionIntegration-test.js
@@ -615,7 +615,7 @@ describe('ReactHooksInspectionIntegration', () => {
expect(tree[0].id).toEqual(0);
expect(tree[0].isStateEditable).toEqual(false);
expect(tree[0].name).toEqual('OpaqueIdentifier');
- expect((tree[0].value + '').startsWith('c_')).toBe(true);
+ expect(String(tree[0].value).startsWith('c_')).toBe(true);
expect(tree[1]).toEqual({
id: 1,
@@ -646,7 +646,7 @@ describe('ReactHooksInspectionIntegration', () => {
expect(tree[0].id).toEqual(0);
expect(tree[0].isStateEditable).toEqual(false);
expect(tree[0].name).toEqual('OpaqueIdentifier');
- expect((tree[0].value + '').startsWith('c_')).toBe(true);
+ expect(String(tree[0].value).startsWith('c_')).toBe(true);
expect(tree[1]).toEqual({
id: 1,
diff --git a/packages/react-devtools-extensions/src/main.js b/packages/react-devtools-extensions/src/main.js
index 9d5b4da02e409..eb093fc51b785 100644
--- a/packages/react-devtools-extensions/src/main.js
+++ b/packages/react-devtools-extensions/src/main.js
@@ -94,7 +94,7 @@ function createPanelIfReactLoaded() {
function initBridgeAndStore() {
const port = chrome.runtime.connect({
- name: '' + tabId,
+ name: String(tabId),
});
// Looks like `port.onDisconnect` does not trigger on in-tab navigation like new URL or back/forward navigation,
// so it makes no sense to handle it here.
diff --git a/packages/react-devtools-shared/src/backend/legacy/renderer.js b/packages/react-devtools-shared/src/backend/legacy/renderer.js
index 24f30b87bc78a..1f0aed404087a 100644
--- a/packages/react-devtools-shared/src/backend/legacy/renderer.js
+++ b/packages/react-devtools-shared/src/backend/legacy/renderer.js
@@ -63,7 +63,7 @@ function getData(internalInstance: InternalInstance) {
// != used deliberately here to catch undefined and null
if (internalInstance._currentElement != null) {
if (internalInstance._currentElement.key) {
- key = '' + internalInstance._currentElement.key;
+ key = String(internalInstance._currentElement.key);
}
const elementType = internalInstance._currentElement.type;
diff --git a/packages/react-devtools-shared/src/backend/renderer.js b/packages/react-devtools-shared/src/backend/renderer.js
index 78953e72979a4..24a00583e7e68 100644
--- a/packages/react-devtools-shared/src/backend/renderer.js
+++ b/packages/react-devtools-shared/src/backend/renderer.js
@@ -1848,7 +1848,7 @@ export function attach(
// This check is a guard to handle a React element that has been modified
// in such a way as to bypass the default stringification of the "key" property.
- const keyString = key === null ? null : '' + key;
+ const keyString = key === null ? null : String(key);
const keyStringID = getStringID(keyString);
pushOperation(TREE_OPERATION_ADD);
diff --git a/packages/react-devtools-shared/src/backend/utils.js b/packages/react-devtools-shared/src/backend/utils.js
index 4b40645595d1c..09f821f4e1877 100644
--- a/packages/react-devtools-shared/src/backend/utils.js
+++ b/packages/react-devtools-shared/src/backend/utils.js
@@ -166,11 +166,7 @@ export function format(
): string {
const args = inputArgs.slice();
- // Symbols cannot be concatenated with Strings.
- let formatted: string =
- typeof maybeMessage === 'symbol'
- ? maybeMessage.toString()
- : '' + maybeMessage;
+ let formatted: string = String(maybeMessage);
// If the first argument is a string, check for substitutions.
if (typeof maybeMessage === 'string') {
@@ -203,17 +199,14 @@ export function format(
// Arguments that remain after formatting.
if (args.length) {
for (let i = 0; i < args.length; i++) {
- const arg = args[i];
-
- // Symbols cannot be concatenated with Strings.
- formatted += ' ' + (typeof arg === 'symbol' ? arg.toString() : arg);
+ formatted += ' ' + String(args[i]);
}
}
// Update escaped %% values.
formatted = formatted.replace(/%{2,2}/g, '%');
- return '' + formatted;
+ return String(formatted);
}
export function isSynchronousXHRSupported(): boolean {
diff --git a/packages/react-devtools-shared/src/devtools/views/ErrorBoundary/ErrorBoundary.js b/packages/react-devtools-shared/src/devtools/views/ErrorBoundary/ErrorBoundary.js
index c7aa20dcd5902..ea4f54b1a9b8b 100644
--- a/packages/react-devtools-shared/src/devtools/views/ErrorBoundary/ErrorBoundary.js
+++ b/packages/react-devtools-shared/src/devtools/views/ErrorBoundary/ErrorBoundary.js
@@ -46,7 +46,7 @@ export default class ErrorBoundary extends Component {
error !== null &&
error.hasOwnProperty('message')
? error.message
- : '' + error;
+ : String(error);
const callStack =
typeof error === 'object' &&
diff --git a/packages/react-devtools-shared/src/devtools/views/utils.js b/packages/react-devtools-shared/src/devtools/views/utils.js
index b326996daa05a..035d9f83a3bc5 100644
--- a/packages/react-devtools-shared/src/devtools/views/utils.js
+++ b/packages/react-devtools-shared/src/devtools/views/utils.js
@@ -19,8 +19,8 @@ export function alphaSortEntries(
): number {
const a = entryA[0];
const b = entryB[0];
- if ('' + +a === a) {
- if ('' + +b !== b) {
+ if (String(+a) === a) {
+ if (String(+b) !== b) {
return -1;
}
return +a < +b ? -1 : 1;
diff --git a/packages/react-devtools-shared/src/hook.js b/packages/react-devtools-shared/src/hook.js
index 152b1dce8f8e4..6f34f86132ab6 100644
--- a/packages/react-devtools-shared/src/hook.js
+++ b/packages/react-devtools-shared/src/hook.js
@@ -180,10 +180,7 @@ export function installHook(target: any): DevToolsHook | null {
const args = inputArgs.slice();
// Symbols cannot be concatenated with Strings.
- let formatted: string =
- typeof maybeMessage === 'symbol'
- ? maybeMessage.toString()
- : '' + maybeMessage;
+ let formatted = String(maybeMessage);
// If the first argument is a string, check for substitutions.
if (typeof maybeMessage === 'string') {
@@ -216,17 +213,14 @@ export function installHook(target: any): DevToolsHook | null {
// Arguments that remain after formatting.
if (args.length) {
for (let i = 0; i < args.length; i++) {
- const arg = args[i];
-
- // Symbols cannot be concatenated with Strings.
- formatted += ' ' + (typeof arg === 'symbol' ? arg.toString() : arg);
+ formatted += ' ' + String(args[i]);
}
}
// Update escaped %% values.
formatted = formatted.replace(/%{2,2}/g, '%');
- return '' + formatted;
+ return String(formatted);
}
let unpatchFn = null;
diff --git a/packages/react-devtools-shared/src/hooks/SourceMapMetadataConsumer.js b/packages/react-devtools-shared/src/hooks/SourceMapMetadataConsumer.js
index 250192c7fa4b3..4882afba9a9fb 100644
--- a/packages/react-devtools-shared/src/hooks/SourceMapMetadataConsumer.js
+++ b/packages/react-devtools-shared/src/hooks/SourceMapMetadataConsumer.js
@@ -38,7 +38,6 @@ function normalizeSourcePath(
const {sourceRoot} = map;
let source = sourceInput;
- // eslint-disable-next-line react-internal/no-primitive-constructors
source = String(source);
// Some source maps produce relative source paths like "./foo.js" instead of
// "foo.js". Normalize these first so that future comparisons will succeed.
diff --git a/packages/react-devtools-shared/src/utils.js b/packages/react-devtools-shared/src/utils.js
index 7c9e2fbd25f8a..9ec19e3afde41 100644
--- a/packages/react-devtools-shared/src/utils.js
+++ b/packages/react-devtools-shared/src/utils.js
@@ -834,7 +834,7 @@ export function formatDataForPreview(
return data;
default:
try {
- return truncateForDisplay('' + data);
+ return truncateForDisplay(String(data));
} catch (error) {
return 'unserializable';
}
diff --git a/packages/react-dom/src/__tests__/ReactDOMAttribute-test.js b/packages/react-dom/src/__tests__/ReactDOMAttribute-test.js
index aa91857b48009..e240708667f21 100644
--- a/packages/react-dom/src/__tests__/ReactDOMAttribute-test.js
+++ b/packages/react-dom/src/__tests__/ReactDOMAttribute-test.js
@@ -89,6 +89,27 @@ describe('ReactDOM unknown attribute', () => {
testUnknownAttributeAssignment(lol, 'lol');
});
+ it('throws with Temporal-like objects', () => {
+ class TemporalLike {
+ valueOf() {
+ // Throwing here is the behavior of ECMAScript "Temporal" date/time API.
+ // See https://tc39.es/proposal-temporal/docs/plaindate.html#valueOf
+ throw new TypeError('prod message');
+ }
+ toString() {
+ return '2020-01-01';
+ }
+ }
+ const test = () =>
+ testUnknownAttributeAssignment(new TemporalLike(), null);
+ expect(() =>
+ expect(test).toThrowError(new TypeError('prod message')),
+ ).toErrorDev(
+ 'Warning: The provided `unknown` attribute is an unsupported type TemporalLike.' +
+ ' This value must be coerced to a string before before using it here.',
+ );
+ });
+
it('removes symbols and warns', () => {
expect(() => testUnknownAttributeRemoval(Symbol('foo'))).toErrorDev(
'Warning: Invalid value for prop `unknown` on
tag. Either remove it ' +
diff --git a/packages/react-dom/src/__tests__/ReactDOMComponent-test.js b/packages/react-dom/src/__tests__/ReactDOMComponent-test.js
index 2911099e8e932..fb7d7cf41ada3 100644
--- a/packages/react-dom/src/__tests__/ReactDOMComponent-test.js
+++ b/packages/react-dom/src/__tests__/ReactDOMComponent-test.js
@@ -254,6 +254,28 @@ describe('ReactDOMComponent', () => {
ReactDOM.render(, div);
});
+ it('throws with Temporal-like objects as style values', () => {
+ class TemporalLike {
+ valueOf() {
+ // Throwing here is the behavior of ECMAScript "Temporal" date/time API.
+ // See https://tc39.es/proposal-temporal/docs/plaindate.html#valueOf
+ throw new TypeError('prod message');
+ }
+ toString() {
+ return '2020-01-01';
+ }
+ }
+ const style = {fontSize: new TemporalLike()};
+ const div = document.createElement('div');
+ const test = () => ReactDOM.render(, div);
+ expect(() =>
+ expect(test).toThrowError(new TypeError('prod message')),
+ ).toErrorDev(
+ 'Warning: The provided `fontSize` CSS property is an unsupported type TemporalLike.' +
+ ' This value must be coerced to a string before before using it here.',
+ );
+ });
+
it('should update styles if initially null', () => {
let styles = null;
const container = document.createElement('div');
@@ -1130,7 +1152,7 @@ describe('ReactDOMComponent', () => {
describe('createOpenTagMarkup', () => {
function quoteRegexp(str) {
- return (str + '').replace(/([.?*+\^$\[\]\\(){}|-])/g, '\\$1');
+ return String(str).replace(/([.?*+\^$\[\]\\(){}|-])/g, '\\$1');
}
function expectToHaveAttribute(actual, expected) {
@@ -1164,7 +1186,7 @@ describe('ReactDOMComponent', () => {
describe('createContentMarkup', () => {
function quoteRegexp(str) {
- return (str + '').replace(/([.?*+\^$\[\]\\(){}|-])/g, '\\$1');
+ return String(str).replace(/([.?*+\^$\[\]\\(){}|-])/g, '\\$1');
}
function genMarkup(props) {
@@ -2412,6 +2434,28 @@ describe('ReactDOMComponent', () => {
expect(el.getAttribute('whatever')).toBe('[object Object]');
});
+ it('allows Temporal-like objects as HTML (they are not coerced to strings first)', function() {
+ class TemporalLike {
+ valueOf() {
+ // Throwing here is the behavior of ECMAScript "Temporal" date/time API.
+ // See https://tc39.es/proposal-temporal/docs/plaindate.html#valueOf
+ throw new TypeError('prod message');
+ }
+ toString() {
+ return '2020-01-01';
+ }
+ }
+
+ // `dangerouslySetInnerHTML` is never coerced to a string, so won't throw
+ // even with a Temporal-like object.
+ const container = document.createElement('div');
+ ReactDOM.render(
+ ,
+ container,
+ );
+ expect(container.firstChild.innerHTML).toEqual('2020-01-01');
+ });
+
it('allows cased data attributes', function() {
let el;
expect(() => {
diff --git a/packages/react-dom/src/__tests__/ReactDOMInput-test.js b/packages/react-dom/src/__tests__/ReactDOMInput-test.js
index 018dd14697f08..fb2c4bbc380c5 100644
--- a/packages/react-dom/src/__tests__/ReactDOMInput-test.js
+++ b/packages/react-dom/src/__tests__/ReactDOMInput-test.js
@@ -544,6 +544,102 @@ describe('ReactDOMInput', () => {
expect(node.value).toBe('foobar');
});
+ it('should throw for date inputs if `defaultValue` is an object where valueOf() throws', () => {
+ class TemporalLike {
+ valueOf() {
+ // Throwing here is the behavior of ECMAScript "Temporal" date/time API.
+ // See https://tc39.es/proposal-temporal/docs/plaindate.html#valueOf
+ throw new TypeError('prod message');
+ }
+ toString() {
+ return '2020-01-01';
+ }
+ }
+ const test = () =>
+ ReactDOM.render(
+ ,
+ container,
+ );
+ expect(() =>
+ expect(test).toThrowError(new TypeError('prod message')),
+ ).toErrorDev(
+ 'Form field values (value, checked, defaultValue, or defaultChecked props) must be ' +
+ 'strings, not TemporalLike. This value must be coerced to a string before before using it here.',
+ );
+ });
+
+ it('should throw for text inputs if `defaultValue` is an object where valueOf() throws', () => {
+ class TemporalLike {
+ valueOf() {
+ // Throwing here is the behavior of ECMAScript "Temporal" date/time API.
+ // See https://tc39.es/proposal-temporal/docs/plaindate.html#valueOf
+ throw new TypeError('prod message');
+ }
+ toString() {
+ return '2020-01-01';
+ }
+ }
+ const test = () =>
+ ReactDOM.render(
+ ,
+ container,
+ );
+ expect(() =>
+ expect(test).toThrowError(new TypeError('prod message')),
+ ).toErrorDev(
+ 'Form field values (value, checked, defaultValue, or defaultChecked props) must be ' +
+ 'strings, not TemporalLike. This value must be coerced to a string before before using it here.',
+ );
+ });
+
+ it('should throw for date inputs if `value` is an object where valueOf() throws', () => {
+ class TemporalLike {
+ valueOf() {
+ // Throwing here is the behavior of ECMAScript "Temporal" date/time API.
+ // See https://tc39.es/proposal-temporal/docs/plaindate.html#valueOf
+ throw new TypeError('prod message');
+ }
+ toString() {
+ return '2020-01-01';
+ }
+ }
+ const test = () =>
+ ReactDOM.render(
+ {}} />,
+ container,
+ );
+ expect(() =>
+ expect(test).toThrowError(new TypeError('prod message')),
+ ).toErrorDev(
+ 'Form field values (value, checked, defaultValue, or defaultChecked props) must be ' +
+ 'strings, not TemporalLike. This value must be coerced to a string before before using it here.',
+ );
+ });
+
+ it('should throw for text inputs if `value` is an object where valueOf() throws', () => {
+ class TemporalLike {
+ valueOf() {
+ // Throwing here is the behavior of ECMAScript "Temporal" date/time API.
+ // See https://tc39.es/proposal-temporal/docs/plaindate.html#valueOf
+ throw new TypeError('prod message');
+ }
+ toString() {
+ return '2020-01-01';
+ }
+ }
+ const test = () =>
+ ReactDOM.render(
+ {}} />,
+ container,
+ );
+ expect(() =>
+ expect(test).toThrowError(new TypeError('prod message')),
+ ).toErrorDev(
+ 'Form field values (value, checked, defaultValue, or defaultChecked props) must be ' +
+ 'strings, not TemporalLike. This value must be coerced to a string before before using it here.',
+ );
+ });
+
it('should display `value` of number 0', () => {
const stub = ;
const node = ReactDOM.render(stub, container);
@@ -1575,7 +1671,7 @@ describe('ReactDOMInput', () => {
return value;
},
set: function(val) {
- value = '' + val;
+ value = String(val);
log.push('set property value');
},
});
diff --git a/packages/react-dom/src/__tests__/ReactDOMSelect-test.js b/packages/react-dom/src/__tests__/ReactDOMSelect-test.js
index 3368f09f585ac..08353e11da70c 100644
--- a/packages/react-dom/src/__tests__/ReactDOMSelect-test.js
+++ b/packages/react-dom/src/__tests__/ReactDOMSelect-test.js
@@ -1019,4 +1019,268 @@ describe('ReactDOMSelect', () => {
expect(node.value).toBe('');
});
});
+
+ describe('When given a Temporal.PlainDate-like value', () => {
+ class TemporalLike {
+ valueOf() {
+ // Throwing here is the behavior of ECMAScript "Temporal" date/time API.
+ // See https://tc39.es/proposal-temporal/docs/plaindate.html#valueOf
+ throw new TypeError('prod message');
+ }
+ toString() {
+ return '2020-01-01';
+ }
+ }
+
+ it('throws when given a Temporal.PlainDate-like value (select)', () => {
+ const test = () => {
+ ReactTestUtils.renderIntoDocument(
+ ,
+ );
+ };
+ expect(() =>
+ expect(test).toThrowError(new TypeError('prod message')),
+ ).toErrorDev(
+ 'Form field values (value, checked, defaultValue, or defaultChecked props)' +
+ ' must be strings, not TemporalLike. ' +
+ 'This value must be coerced to a string before before using it here.',
+ );
+ });
+
+ it('throws when given a Temporal.PlainDate-like value (option)', () => {
+ const test = () => {
+ ReactTestUtils.renderIntoDocument(
+ ,
+ );
+ };
+ expect(() =>
+ expect(test).toThrowError(new TypeError('prod message')),
+ ).toErrorDev(
+ 'The provided `value` attribute is an unsupported type TemporalLike.' +
+ ' This value must be coerced to a string before before using it here.',
+ );
+ });
+
+ it('throws when given a Temporal.PlainDate-like value (both)', () => {
+ const test = () => {
+ ReactTestUtils.renderIntoDocument(
+ ,
+ );
+ };
+ expect(() =>
+ expect(test).toThrowError(new TypeError('prod message')),
+ ).toErrorDev(
+ 'The provided `value` attribute is an unsupported type TemporalLike.' +
+ ' This value must be coerced to a string before before using it here.',
+ );
+ });
+
+ it('throws with updated Temporal.PlainDate-like value (select)', () => {
+ ReactTestUtils.renderIntoDocument(
+ ,
+ );
+ const test = () => {
+ ReactTestUtils.renderIntoDocument(
+ ,
+ );
+ };
+ expect(() =>
+ expect(test).toThrowError(new TypeError('prod message')),
+ ).toErrorDev(
+ 'Form field values (value, checked, defaultValue, or defaultChecked props)' +
+ ' must be strings, not TemporalLike. ' +
+ 'This value must be coerced to a string before before using it here.',
+ );
+ });
+
+ it('throws with updated Temporal.PlainDate-like value (option)', () => {
+ ReactTestUtils.renderIntoDocument(
+ ,
+ );
+ const test = () => {
+ ReactTestUtils.renderIntoDocument(
+ ,
+ );
+ };
+ expect(() =>
+ expect(test).toThrowError(new TypeError('prod message')),
+ ).toErrorDev(
+ 'The provided `value` attribute is an unsupported type TemporalLike.' +
+ ' This value must be coerced to a string before before using it here.',
+ );
+ });
+
+ it('throws with updated Temporal.PlainDate-like value (both)', () => {
+ ReactTestUtils.renderIntoDocument(
+ ,
+ );
+ const test = () => {
+ ReactTestUtils.renderIntoDocument(
+ ,
+ );
+ };
+ expect(() =>
+ expect(test).toThrowError(new TypeError('prod message')),
+ ).toErrorDev(
+ 'The provided `value` attribute is an unsupported type TemporalLike.' +
+ ' This value must be coerced to a string before before using it here.',
+ );
+ });
+
+ it('throws when given a Temporal.PlainDate-like defaultValue (select)', () => {
+ const test = () => {
+ ReactTestUtils.renderIntoDocument(
+ ,
+ );
+ };
+ expect(() =>
+ expect(test).toThrowError(new TypeError('prod message')),
+ ).toErrorDev(
+ 'Form field values (value, checked, defaultValue, or defaultChecked props)' +
+ ' must be strings, not TemporalLike. ' +
+ 'This value must be coerced to a string before before using it here.',
+ );
+ });
+
+ it('throws when given a Temporal.PlainDate-like defaultValue (option)', () => {
+ const test = () => {
+ ReactTestUtils.renderIntoDocument(
+ ,
+ );
+ };
+ expect(() =>
+ expect(test).toThrowError(new TypeError('prod message')),
+ ).toErrorDev(
+ 'The provided `value` attribute is an unsupported type TemporalLike.' +
+ ' This value must be coerced to a string before before using it here.',
+ );
+ });
+
+ it('throws when given a Temporal.PlainDate-like value (both)', () => {
+ const test = () => {
+ ReactTestUtils.renderIntoDocument(
+ ,
+ );
+ };
+ expect(() =>
+ expect(test).toThrowError(new TypeError('prod message')),
+ ).toErrorDev(
+ 'The provided `value` attribute is an unsupported type TemporalLike.' +
+ ' This value must be coerced to a string before before using it here.',
+ );
+ });
+
+ it('throws with updated Temporal.PlainDate-like defaultValue (select)', () => {
+ ReactTestUtils.renderIntoDocument(
+ ,
+ );
+ const test = () => {
+ ReactTestUtils.renderIntoDocument(
+ ,
+ );
+ };
+ expect(() =>
+ expect(test).toThrowError(new TypeError('prod message')),
+ ).toErrorDev(
+ 'Form field values (value, checked, defaultValue, or defaultChecked props)' +
+ ' must be strings, not TemporalLike. ' +
+ 'This value must be coerced to a string before before using it here.',
+ );
+ });
+
+ it('throws with updated Temporal.PlainDate-like defaultValue (both)', () => {
+ ReactTestUtils.renderIntoDocument(
+ ,
+ );
+ const test = () => {
+ ReactTestUtils.renderIntoDocument(
+ ,
+ );
+ };
+ expect(() =>
+ expect(test).toThrowError(new TypeError('prod message')),
+ ).toErrorDev(
+ 'The provided `value` attribute is an unsupported type TemporalLike.' +
+ ' This value must be coerced to a string before before using it here.',
+ );
+ });
+ });
});
diff --git a/packages/react-dom/src/__tests__/ReactDOMServerIntegrationHooks-test.js b/packages/react-dom/src/__tests__/ReactDOMServerIntegrationHooks-test.js
index 53a34b25ffd55..a27954785f7ea 100644
--- a/packages/react-dom/src/__tests__/ReactDOMServerIntegrationHooks-test.js
+++ b/packages/react-dom/src/__tests__/ReactDOMServerIntegrationHooks-test.js
@@ -1745,7 +1745,7 @@ describe('ReactDOMServerHooks', () => {
it('useOpaqueIdentifier warns when there is a hydration error and we are using ID as a string', async () => {
function Child({appId}) {
- return ;
+ return ;
}
function App() {
const id = useOpaqueIdentifier();
@@ -1769,7 +1769,7 @@ describe('ReactDOMServerHooks', () => {
it('useOpaqueIdentifier warns when there is a hydration error and we are using ID as a string', async () => {
function Child({appId}) {
- return ;
+ return ;
}
function App() {
const id = useOpaqueIdentifier();
@@ -1793,7 +1793,7 @@ describe('ReactDOMServerHooks', () => {
it('useOpaqueIdentifier warns if you try to use the result as a string in a child component', async () => {
function Child({appId}) {
- return ;
+ return ;
}
function App() {
const id = useOpaqueIdentifier();
@@ -1817,7 +1817,7 @@ describe('ReactDOMServerHooks', () => {
it('useOpaqueIdentifier warns if you try to use the result as a string', async () => {
function App() {
const id = useOpaqueIdentifier();
- return ;
+ return ;
}
const container = document.createElement('div');
@@ -1836,7 +1836,7 @@ describe('ReactDOMServerHooks', () => {
it('useOpaqueIdentifier warns if you try to use the result as a string in a child component wrapped in a Suspense', async () => {
function Child({appId}) {
- return ;
+ return ;
}
function App() {
const id = useOpaqueIdentifier();
diff --git a/packages/react-dom/src/__tests__/ReactDOMServerIntegrationUntrustedURL-test.internal.js b/packages/react-dom/src/__tests__/ReactDOMServerIntegrationUntrustedURL-test.internal.js
index 9fb48b3bce24c..72291c9959ee0 100644
--- a/packages/react-dom/src/__tests__/ReactDOMServerIntegrationUntrustedURL-test.internal.js
+++ b/packages/react-dom/src/__tests__/ReactDOMServerIntegrationUntrustedURL-test.internal.js
@@ -242,6 +242,11 @@ describe('ReactDOMServerIntegration - Untrusted URLs - disableJavaScriptURLs', (
// consistency but the code structure makes that hard right now.
expectedToStringCalls = 2;
}
+ if (__DEV__) {
+ // Checking for string coercion problems results in double the
+ // toString calls in DEV
+ expectedToStringCalls *= 2;
+ }
let toStringCalls = 0;
const firstIsSafe = {
diff --git a/packages/react-dom/src/__tests__/ReactDOMTextComponent-test.js b/packages/react-dom/src/__tests__/ReactDOMTextComponent-test.js
index ddc3a979597e3..24ff139085d96 100644
--- a/packages/react-dom/src/__tests__/ReactDOMTextComponent-test.js
+++ b/packages/react-dom/src/__tests__/ReactDOMTextComponent-test.js
@@ -284,4 +284,26 @@ describe('ReactDOMTextComponent', () => {
ReactDOM.render(, el);
expect(el.innerHTML).toBe('');
});
+
+ it('throws for Temporal-like text nodes', () => {
+ const el = document.createElement('div');
+ class TemporalLike {
+ valueOf() {
+ // Throwing here is the behavior of ECMAScript "Temporal" date/time API.
+ // See https://tc39.es/proposal-temporal/docs/plaindate.html#valueOf
+ throw new TypeError('prod message');
+ }
+ toString() {
+ return '2020-01-01';
+ }
+ }
+ expect(() =>
+ ReactDOM.render(
{new TemporalLike()}
, el),
+ ).toThrowError(
+ new Error(
+ 'Objects are not valid as a React child (found: object with keys {}).' +
+ ' If you meant to render a collection of children, use an array instead.',
+ ),
+ );
+ });
});
diff --git a/packages/react-dom/src/__tests__/ReactDOMTextarea-test.js b/packages/react-dom/src/__tests__/ReactDOMTextarea-test.js
index 4d56f53d9e40c..f8bec6c4ba97d 100644
--- a/packages/react-dom/src/__tests__/ReactDOMTextarea-test.js
+++ b/packages/react-dom/src/__tests__/ReactDOMTextarea-test.js
@@ -142,7 +142,7 @@ describe('ReactDOMTextarea', () => {
return value;
},
set: function(val) {
- value = '' + val;
+ value = String(val);
counter++;
},
});
@@ -219,6 +219,36 @@ describe('ReactDOMTextarea', () => {
expect(node.value).toEqual('foo');
});
+ it('should throw when value is set to a Temporal-like object', () => {
+ class TemporalLike {
+ valueOf() {
+ // Throwing here is the behavior of ECMAScript "Temporal" date/time API.
+ // See https://tc39.es/proposal-temporal/docs/plaindate.html#valueOf
+ throw new TypeError('prod message');
+ }
+ toString() {
+ return '2020-01-01';
+ }
+ }
+ const container = document.createElement('div');
+ const stub = ;
+ const node = renderTextarea(stub, container);
+
+ expect(node.value).toBe('giraffe');
+
+ const test = () =>
+ ReactDOM.render(
+ ,
+ container,
+ );
+ expect(() =>
+ expect(test).toThrowError(new TypeError('prod message')),
+ ).toErrorDev(
+ 'Form field values (value, checked, defaultValue, or defaultChecked props) must be ' +
+ 'strings, not TemporalLike. This value must be coerced to a string before before using it here.',
+ );
+ });
+
it('should take updates to `defaultValue` for uncontrolled textarea', () => {
const container = document.createElement('div');
diff --git a/packages/react-dom/src/__tests__/ReactIdentity-test.js b/packages/react-dom/src/__tests__/ReactIdentity-test.js
index cce0c227b6392..97283d8c392db 100644
--- a/packages/react-dom/src/__tests__/ReactIdentity-test.js
+++ b/packages/react-dom/src/__tests__/ReactIdentity-test.js
@@ -262,4 +262,33 @@ describe('ReactIdentity', () => {
ReactTestUtils.renderIntoDocument(component);
}).not.toThrow();
});
+
+ it('should throw if key is a Temporal-like object', () => {
+ class TemporalLike {
+ valueOf() {
+ // Throwing here is the behavior of ECMAScript "Temporal" date/time API.
+ // See https://tc39.es/proposal-temporal/docs/plaindate.html#valueOf
+ throw new TypeError('prod message');
+ }
+ toString() {
+ return '2020-01-01';
+ }
+ }
+
+ const el = document.createElement('div');
+ const test = () =>
+ ReactDOM.render(
+
+
+
,
+ el,
+ );
+ expect(() =>
+ expect(test).toThrowError(new TypeError('prod message')),
+ ).toErrorDev(
+ 'The provided key is an unsupported type TemporalLike.' +
+ ' This value must be coerced to a string before before using it here.',
+ {withoutStack: true},
+ );
+ });
});
diff --git a/packages/react-dom/src/__tests__/ReactMultiChildText-test.js b/packages/react-dom/src/__tests__/ReactMultiChildText-test.js
index 467cbf59c2e06..2e99344289a7a 100644
--- a/packages/react-dom/src/__tests__/ReactMultiChildText-test.js
+++ b/packages/react-dom/src/__tests__/ReactMultiChildText-test.js
@@ -44,7 +44,7 @@ const expectChildren = function(container, children) {
} else {
expect(textNode != null).toBe(true);
expect(textNode.nodeType).toBe(3);
- expect(textNode.data).toBe('' + children);
+ expect(textNode.data).toBe(String(children));
}
} else {
let mountIndex = 0;
@@ -55,7 +55,7 @@ const expectChildren = function(container, children) {
if (typeof child === 'string') {
textNode = outerNode.childNodes[mountIndex];
expect(textNode.nodeType).toBe(3);
- expect(textNode.data).toBe('' + child);
+ expect(textNode.data).toBe(child);
mountIndex++;
} else {
const elementDOMNode = outerNode.childNodes[mountIndex];
diff --git a/packages/react-dom/src/client/DOMPropertyOperations.js b/packages/react-dom/src/client/DOMPropertyOperations.js
index 2dcb1ace8ab9f..30fed05aed609 100644
--- a/packages/react-dom/src/client/DOMPropertyOperations.js
+++ b/packages/react-dom/src/client/DOMPropertyOperations.js
@@ -20,6 +20,7 @@ import {
disableJavaScriptURLs,
enableTrustedTypesIntegration,
} from 'shared/ReactFeatureFlags';
+import {checkAttributeStringCoercion} from 'shared/CheckStringCoercion';
import {isOpaqueHydratingObject} from './ReactDOMHostConfig';
import type {PropertyInfo} from '../shared/DOMProperty';
@@ -40,10 +41,18 @@ export function getValueForProperty(
const {propertyName} = propertyInfo;
return (node: any)[propertyName];
} else {
+ // This check protects multiple uses of `expected`, which is why the
+ // react-internal/safe-string-coercion rule is disabled in several spots
+ // below.
+ if (__DEV__) {
+ checkAttributeStringCoercion(expected, name);
+ }
+
if (!disableJavaScriptURLs && propertyInfo.sanitizeURL) {
// If we haven't fully disabled javascript: URLs, and if
// the hydration is successful of a javascript: URL, we
// still want to warn on the client.
+ // eslint-disable-next-line react-internal/safe-string-coercion
sanitizeURL('' + (expected: any));
}
@@ -60,6 +69,7 @@ export function getValueForProperty(
if (shouldRemoveAttribute(name, expected, propertyInfo, false)) {
return value;
}
+ // eslint-disable-next-line react-internal/safe-string-coercion
if (value === '' + (expected: any)) {
return expected;
}
@@ -85,6 +95,7 @@ export function getValueForProperty(
if (shouldRemoveAttribute(name, expected, propertyInfo, false)) {
return stringValue === null ? expected : stringValue;
+ // eslint-disable-next-line react-internal/safe-string-coercion
} else if (stringValue === '' + (expected: any)) {
return expected;
} else {
@@ -119,6 +130,9 @@ export function getValueForAttribute(
return expected === undefined ? undefined : null;
}
const value = node.getAttribute(name);
+ if (__DEV__) {
+ checkAttributeStringCoercion(expected, name);
+ }
if (value === '' + (expected: any)) {
return expected;
}
@@ -153,6 +167,9 @@ export function setValueForProperty(
if (value === null) {
node.removeAttribute(attributeName);
} else {
+ if (__DEV__) {
+ checkAttributeStringCoercion(value, name);
+ }
node.setAttribute(
attributeName,
enableTrustedTypesIntegration ? (value: any) : '' + (value: any),
@@ -191,6 +208,9 @@ export function setValueForProperty(
if (enableTrustedTypesIntegration) {
attributeValue = (value: any);
} else {
+ if (__DEV__) {
+ checkAttributeStringCoercion(value, attributeName);
+ }
attributeValue = '' + (value: any);
}
if (propertyInfo.sanitizeURL) {
diff --git a/packages/react-dom/src/client/ReactDOMComponent.js b/packages/react-dom/src/client/ReactDOMComponent.js
index 7820978957107..2d347639eba45 100644
--- a/packages/react-dom/src/client/ReactDOMComponent.js
+++ b/packages/react-dom/src/client/ReactDOMComponent.js
@@ -14,6 +14,7 @@ import {
import {canUseDOM} from 'shared/ExecutionEnvironment';
import hasOwnProperty from 'shared/hasOwnProperty';
+import {checkHtmlStringCoercion} from 'shared/CheckStringCoercion';
import {
getValueForAttribute,
@@ -139,6 +140,9 @@ if (__DEV__) {
const NORMALIZE_NULL_AND_REPLACEMENT_REGEX = /\u0000|\uFFFD/g;
normalizeMarkupForTextOrAttribute = function(markup: mixed): string {
+ if (__DEV__) {
+ checkHtmlStringCoercion(markup);
+ }
const markupString =
typeof markup === 'string' ? markup : '' + (markup: any);
return markupString
diff --git a/packages/react-dom/src/client/ReactDOMInput.js b/packages/react-dom/src/client/ReactDOMInput.js
index c1afd277e3e93..4fabc1fb9ada9 100644
--- a/packages/react-dom/src/client/ReactDOMInput.js
+++ b/packages/react-dom/src/client/ReactDOMInput.js
@@ -18,6 +18,7 @@ import {checkControlledValueProps} from '../shared/ReactControlledValuePropTypes
import {updateValueIfChanged} from './inputValueTracking';
import getActiveElement from './getActiveElement';
import {disableInputAttributeSyncing} from 'shared/ReactFeatureFlags';
+import {checkAttributeStringCoercion} from 'shared/CheckStringCoercion';
import type {ToStringValue} from './ToStringValue';
@@ -365,6 +366,9 @@ function updateNamedCousins(rootNode, props) {
// the input might not even be in a form. It might not even be in the
// document. Let's just use the local `querySelectorAll` to ensure we don't
// miss anything.
+ if (__DEV__) {
+ checkAttributeStringCoercion(name, 'name');
+ }
const group = queryRoot.querySelectorAll(
'input[name=' + JSON.stringify('' + name) + '][type="radio"]',
);
diff --git a/packages/react-dom/src/client/ToStringValue.js b/packages/react-dom/src/client/ToStringValue.js
index 41826cd404e05..508e8a9029957 100644
--- a/packages/react-dom/src/client/ToStringValue.js
+++ b/packages/react-dom/src/client/ToStringValue.js
@@ -7,6 +7,8 @@
* @flow
*/
+import {checkFormFieldValueStringCoercion} from 'shared/CheckStringCoercion';
+
export opaque type ToStringValue =
| boolean
| number
@@ -19,6 +21,8 @@ export opaque type ToStringValue =
// around this limitation, we use an opaque type that can only be obtained by
// passing the value through getToStringValue first.
export function toString(value: ToStringValue): string {
+ // The coercion safety check is performed in getToStringValue().
+ // eslint-disable-next-line react-internal/safe-string-coercion
return '' + (value: any);
}
@@ -26,10 +30,14 @@ export function getToStringValue(value: mixed): ToStringValue {
switch (typeof value) {
case 'boolean':
case 'number':
- case 'object':
case 'string':
case 'undefined':
return value;
+ case 'object':
+ if (__DEV__) {
+ checkFormFieldValueStringCoercion(value);
+ }
+ return value;
default:
// function, symbol are assigned as empty strings
return '';
diff --git a/packages/react-dom/src/client/inputValueTracking.js b/packages/react-dom/src/client/inputValueTracking.js
index 54c850662ab2b..499c9aa97ecc6 100644
--- a/packages/react-dom/src/client/inputValueTracking.js
+++ b/packages/react-dom/src/client/inputValueTracking.js
@@ -7,6 +7,8 @@
* @flow
*/
+import {checkFormFieldValueStringCoercion} from 'shared/CheckStringCoercion';
+
type ValueTracker = {|
getValue(): string,
setValue(value: string): void,
@@ -55,6 +57,9 @@ function trackValueOnNode(node: any): ?ValueTracker {
valueField,
);
+ if (__DEV__) {
+ checkFormFieldValueStringCoercion(node[valueField]);
+ }
let currentValue = '' + node[valueField];
// if someone has already defined a value or Safari, then bail
@@ -76,6 +81,9 @@ function trackValueOnNode(node: any): ?ValueTracker {
return get.call(this);
},
set: function(value) {
+ if (__DEV__) {
+ checkFormFieldValueStringCoercion(value);
+ }
currentValue = '' + value;
set.call(this, value);
},
@@ -93,6 +101,9 @@ function trackValueOnNode(node: any): ?ValueTracker {
return currentValue;
},
setValue(value) {
+ if (__DEV__) {
+ checkFormFieldValueStringCoercion(value);
+ }
currentValue = '' + value;
},
stopTracking() {
diff --git a/packages/react-dom/src/server/DOMMarkupOperations.js b/packages/react-dom/src/server/DOMMarkupOperations.js
index 014e635531926..dda7ceab721fb 100644
--- a/packages/react-dom/src/server/DOMMarkupOperations.js
+++ b/packages/react-dom/src/server/DOMMarkupOperations.js
@@ -16,6 +16,7 @@ import {
shouldRemoveAttribute,
} from '../shared/DOMProperty';
import sanitizeURL from '../shared/sanitizeURL';
+import {checkAttributeStringCoercion} from 'shared/CheckStringCoercion';
import quoteAttributeValueForBrowser from './quoteAttributeValueForBrowser';
/**
@@ -44,6 +45,9 @@ export function createMarkupForProperty(name: string, value: mixed): string {
return attributeName + '=""';
} else {
if (propertyInfo.sanitizeURL) {
+ if (__DEV__) {
+ checkAttributeStringCoercion(value, attributeName);
+ }
value = '' + (value: any);
sanitizeURL(value);
}
diff --git a/packages/react-dom/src/server/ReactDOMServerFormatConfig.js b/packages/react-dom/src/server/ReactDOMServerFormatConfig.js
index 78e909cff610c..954cc1ef689f5 100644
--- a/packages/react-dom/src/server/ReactDOMServerFormatConfig.js
+++ b/packages/react-dom/src/server/ReactDOMServerFormatConfig.js
@@ -9,6 +9,12 @@
import type {ReactNodeList} from 'shared/ReactTypes';
+import {
+ checkHtmlStringCoercion,
+ checkCSSPropertyStringCoercion,
+ checkAttributeStringCoercion,
+} from 'shared/CheckStringCoercion';
+
import {Children} from 'react';
import {enableFilterEmptyStringAttributesDOM} from 'shared/ReactFeatureFlags';
@@ -272,6 +278,9 @@ function pushStyle(
const isCustomProperty = styleName.indexOf('--') === 0;
if (isCustomProperty) {
nameChunk = stringToChunk(escapeTextForBrowser(styleName));
+ if (__DEV__) {
+ checkCSSPropertyStringCoercion(styleValue, styleName);
+ }
valueChunk = stringToChunk(
escapeTextForBrowser(('' + styleValue).trim()),
);
@@ -291,6 +300,9 @@ function pushStyle(
valueChunk = stringToChunk('' + styleValue);
}
} else {
+ if (__DEV__) {
+ checkCSSPropertyStringCoercion(styleValue, styleName);
+ }
valueChunk = stringToChunk(
escapeTextForBrowser(('' + styleValue).trim()),
);
@@ -439,6 +451,9 @@ function pushAttribute(
break;
default:
if (propertyInfo.sanitizeURL) {
+ if (__DEV__) {
+ checkAttributeStringCoercion(value, attributeName);
+ }
value = '' + (value: any);
sanitizeURL(value);
}
@@ -496,6 +511,9 @@ function pushInnerHTML(
);
const html = innerHTML.__html;
if (html !== null && html !== undefined) {
+ if (__DEV__) {
+ checkHtmlStringCoercion(html);
+ }
target.push(stringToChunk('' + html));
}
}
@@ -679,6 +697,9 @@ function pushStartOption(
if (selectedValue !== null) {
let stringValue;
if (value !== null) {
+ if (__DEV__) {
+ checkAttributeStringCoercion(value, 'value');
+ }
stringValue = '' + value;
} else {
if (__DEV__) {
@@ -697,6 +718,9 @@ function pushStartOption(
if (isArray(selectedValue)) {
// multiple
for (let i = 0; i < selectedValue.length; i++) {
+ if (__DEV__) {
+ checkAttributeStringCoercion(selectedValue[i], 'value');
+ }
const v = '' + selectedValue[i];
if (v === stringValue) {
target.push(selectedMarkerAttribute);
@@ -895,8 +919,16 @@ function pushStartTextArea(
children.length <= 1,
'