diff --git a/packages/react-dom-bindings/src/client/ReactDOMComponent.js b/packages/react-dom-bindings/src/client/ReactDOMComponent.js index 1b25e3727023..628ee56454e5 100644 --- a/packages/react-dom-bindings/src/client/ReactDOMComponent.js +++ b/packages/react-dom-bindings/src/client/ReactDOMComponent.js @@ -60,6 +60,7 @@ import getAttributeAlias from '../shared/getAttributeAlias'; import possibleStandardNames from '../shared/possibleStandardNames'; import {validateProperties as validateARIAProperties} from '../shared/ReactDOMInvalidARIAHook'; import {validateProperties as validateInputProperties} from '../shared/ReactDOMNullInputValuePropHook'; +import {validateProperties as validateTitleProperties} from '../shared/ReactDOMTitle'; import {validateProperties as validateUnknownProperties} from '../shared/ReactDOMUnknownPropertyHook'; import sanitizeURL from '../shared/sanitizeURL'; @@ -99,6 +100,7 @@ function validatePropertiesInDevelopment(type: string, props: any) { registrationNameDependencies, possibleRegistrationNames, }); + validateTitleProperties(type, props); if ( props.contentEditable && !props.suppressContentEditableWarning && diff --git a/packages/react-dom-bindings/src/server/ReactFizzConfigDOM.js b/packages/react-dom-bindings/src/server/ReactFizzConfigDOM.js index 73ce8d3dd29f..ba1b6c109d4f 100644 --- a/packages/react-dom-bindings/src/server/ReactFizzConfigDOM.js +++ b/packages/react-dom-bindings/src/server/ReactFizzConfigDOM.js @@ -67,6 +67,7 @@ import getAttributeAlias from '../shared/getAttributeAlias'; import {checkControlledValueProps} from '../shared/ReactControlledValuePropTypes'; import {validateProperties as validateARIAProperties} from '../shared/ReactDOMInvalidARIAHook'; import {validateProperties as validateInputProperties} from '../shared/ReactDOMNullInputValuePropHook'; +import {validateProperties as validateTitleProperties} from '../shared/ReactDOMTitle'; import {validateProperties as validateUnknownProperties} from '../shared/ReactDOMUnknownPropertyHook'; import warnValidStyle from '../shared/warnValidStyle'; import {getCrossOriginString} from '../shared/crossOriginStrings'; @@ -3535,51 +3536,7 @@ function pushTitle( const noscriptTagInScope = formatContext.tagScope & NOSCRIPT_SCOPE; const isFallback = formatContext.tagScope & FALLBACK_SCOPE; if (__DEV__) { - if (hasOwnProperty.call(props, 'children')) { - const children = props.children; - - const child = Array.isArray(children) - ? children.length < 2 - ? children[0] - : null - : children; - - if (Array.isArray(children) && children.length > 1) { - console.error( - 'React expects the `children` prop of tags to be a string, number, bigint, or object with a novel `toString` method but found an Array with length %s instead.' + - ' Browsers treat all child Nodes of <title> tags as Text content and React expects to be able to convert `children` of <title> tags to a single string value' + - ' which is why Arrays of length greater than 1 are not supported. When using JSX it can be common to combine text nodes and value nodes.' + - ' For example: <title>hello {nameOfUser}. While not immediately apparent, `children` in this case is an Array with length 2. If your `children` prop' + - ' is using this form try rewriting it using a template string: {`hello ${nameOfUser}`}.', - children.length, - ); - } else if (typeof child === 'function' || typeof child === 'symbol') { - const childType = - typeof child === 'function' ? 'a Function' : 'a Sybmol'; - console.error( - 'React expect children of tags to be a string, number, bigint, or object with a novel `toString` method but found %s instead.' + - ' Browsers treat all child Nodes of <title> tags as Text content and React expects to be able to convert children of <title>' + - ' tags to a single string value.', - childType, - ); - } else if (child && child.toString === {}.toString) { - if (child.$$typeof != null) { - console.error( - 'React expects the `children` prop of <title> tags to be a string, number, bigint, or object with a novel `toString` method but found an object that appears to be' + - ' a React element which never implements a suitable `toString` method. Browsers treat all child Nodes of <title> tags as Text content and React expects to' + - ' be able to convert children of <title> tags to a single string value which is why rendering React elements is not supported. If the `children` of <title> is' + - ' a React Component try moving the <title> tag into that component. If the `children` of <title> is some HTML markup change it to be Text only to be valid HTML.', - ); - } else { - console.error( - 'React expects the `children` prop of <title> tags to be a string, number, bigint, or object with a novel `toString` method but found an object that does not implement' + - ' a suitable `toString` method. Browsers treat all child Nodes of <title> tags as Text content and React expects to be able to convert children of <title> tags' + - ' to a single string value. Using the default `toString` method available on every object is almost certainly an error. Consider whether the `children` of this <title>' + - ' is an object in error and change it to a string or number value if so. Otherwise implement a `toString` method that React can use to produce a valid <title>.', - ); - } - } - } + validateTitleProperties('title', props); } if ( diff --git a/packages/react-dom-bindings/src/shared/ReactDOMTitle.js b/packages/react-dom-bindings/src/shared/ReactDOMTitle.js new file mode 100644 index 000000000000..ca1ad2738f5b --- /dev/null +++ b/packages/react-dom-bindings/src/shared/ReactDOMTitle.js @@ -0,0 +1,64 @@ +/** + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + * @flow + */ + +const hasOwnProperty = Object.prototype.hasOwnProperty; + +export function validateProperties(type: string, props: Object) { + if (type !== 'title') { + return; + } + + if (__DEV__) { + if (hasOwnProperty.call(props, 'children')) { + const children = props.children; + + const child = Array.isArray(children) + ? children.length < 2 + ? children[0] + : null + : children; + + if (Array.isArray(children) && children.length > 1) { + console.error( + 'React expects the `children` prop of <title> tags to be a string, number, bigint, or object with a novel `toString` method but found an Array with length %s instead.' + + ' Browsers treat all child Nodes of <title> tags as Text content and React expects to be able to convert `children` of <title> tags to a single string value' + + ' which is why Arrays of length greater than 1 are not supported. When using JSX it can be common to combine text nodes and value nodes.' + + ' For example: <title>hello {nameOfUser}. While not immediately apparent, `children` in this case is an Array with length 2. If your `children` prop' + + ' is using this form try rewriting it using a template string: {`hello ${nameOfUser}`}.', + children.length, + ); + } else if (typeof child === 'function' || typeof child === 'symbol') { + const childType = + typeof child === 'function' ? 'a Function' : 'a Symbol'; + console.error( + 'React expect children of tags to be a string, number, bigint, or object with a novel `toString` method but found %s instead.' + + ' Browsers treat all child Nodes of <title> tags as Text content and React expects to be able to convert children of <title>' + + ' tags to a single string value.', + childType, + ); + } else if (child && child.toString === {}.toString) { + if (child.$$typeof != null) { + console.error( + 'React expects the `children` prop of <title> tags to be a string, number, bigint, or object with a novel `toString` method but found an object that appears to be' + + ' a React element which never implements a suitable `toString` method. Browsers treat all child Nodes of <title> tags as Text content and React expects to' + + ' be able to convert children of <title> tags to a single string value which is why rendering React elements is not supported. If the `children` of <title> is' + + ' a React Component try moving the <title> tag into that component. If the `children` of <title> is some HTML markup change it to be Text only to be valid HTML.', + ); + } else { + console.error( + 'React expects the `children` prop of <title> tags to be a string, number, bigint, or object with a novel `toString` method but found an object that does not implement' + + ' a suitable `toString` method. Browsers treat all child Nodes of <title> tags as Text content and React expects to be able to convert children of <title> tags' + + ' to a single string value. Using the default `toString` method available on every object is almost certainly an error. Consider whether the `children` of this <title>' + + ' is an object in error and change it to a string or number value if so. Otherwise implement a `toString` method that React can use to produce a valid <title>.', + ); + } + } + } + } +} diff --git a/packages/react-dom/src/__tests__/ReactDOMComponent-test.js b/packages/react-dom/src/__tests__/ReactDOMComponent-test.js index 0f0986dde8e3..7cbdf575b3ad 100644 --- a/packages/react-dom/src/__tests__/ReactDOMComponent-test.js +++ b/packages/react-dom/src/__tests__/ReactDOMComponent-test.js @@ -2700,6 +2700,24 @@ describe('ReactDOMComponent', () => { // without access to the event system (which we don't bundle). }); + it('should warn when <title> has an Array of children (client)', async () => { + const container = document.createElement('div'); + const root = ReactDOMClient.createRoot(container); + await act(() => { + root.render( + React.createElement('title', null, 'My App', ' | Home'), + ); + }); + assertConsoleErrorDev([ + 'React expects the `children` prop of <title> tags to be a string, number, bigint, or object with a novel `toString` method but found an Array with length 2 instead.' + + ' Browsers treat all child Nodes of <title> tags as Text content and React expects to be able to convert `children` of <title> tags to a single string value' + + ' which is why Arrays of length greater than 1 are not supported. When using JSX it can be common to combine text nodes and value nodes.' + + ' For example: <title>hello {nameOfUser}. While not immediately apparent, `children` in this case is an Array with length 2. If your `children` prop' + + ' is using this form try rewriting it using a template string: {`hello ${nameOfUser}`}.\n' + + ' in title (at **)', + ]); + }); + it('should warn about incorrect casing on properties', async () => { const container = document.createElement('div'); const root = ReactDOMClient.createRoot(container); diff --git a/packages/react-dom/src/__tests__/ReactDOMFizzServer-test.js b/packages/react-dom/src/__tests__/ReactDOMFizzServer-test.js index 6ff107786f3b..223369b02792 100644 --- a/packages/react-dom/src/__tests__/ReactDOMFizzServer-test.js +++ b/packages/react-dom/src/__tests__/ReactDOMFizzServer-test.js @@ -5926,6 +5926,21 @@ describe('ReactDOMFizzServer', () => { }, }); await waitForAll([]); + assertConsoleErrorDev( + [ + 'React expects the `children` prop of tags to be a string, number, bigint, ' + + 'or object with a novel `toString` method but found an Array with length 2 instead. ' + + 'Browsers treat all child Nodes of <title> tags as Text content and React expects ' + + 'to be able to convert `children` of <title> tags to a single string value which is why ' + + 'Arrays of length greater than 1 are not supported. ' + + 'When using JSX it can be common to combine text nodes and value nodes. ' + + 'For example: <title>hello {nameOfUser}. ' + + 'While not immediately apparent, `children` in this case is an Array with length 2. ' + + 'If your `children` prop is using this form try rewriting it using a template string: ' + + '{`hello ${nameOfUser}`}.', + ], + {withoutStack: true}, + ); expect(errors).toEqual([]); // with float, the title doesn't render on the client or on the server expect(getVisibleChildren(document.head)).toEqual(); @@ -5973,6 +5988,19 @@ describe('ReactDOMFizzServer', () => { }, }); await waitForAll([]); + assertConsoleErrorDev( + [ + 'React expects the `children` prop of <title> tags to be a string, number, bigint, ' + + 'or object with a novel `toString` method but found an object that appears to be a ' + + 'React element which never implements a suitable `toString` method. ' + + 'Browsers treat all child Nodes of <title> tags as Text content and React expects ' + + 'to be able to convert children of <title> tags to a single string value which is ' + + 'why rendering React elements is not supported. If the `children` of <title> is a ' + + 'React Component try moving the <title> tag into that component. ' + + 'If the `children` of <title> is some HTML markup change it to be Text only to be valid HTML.', + ], + {withoutStack: true}, + ); expect(errors).toEqual([]); // object titles are toStringed when float is on expect(getVisibleChildren(document.head)).toEqual( @@ -6016,6 +6044,19 @@ describe('ReactDOMFizzServer', () => { }, }); await waitForAll([]); + assertConsoleErrorDev( + [ + 'React expects the `children` prop of <title> tags to be a string, number, bigint, ' + + 'or object with a novel `toString` method but found an object that does not implement a ' + + 'suitable `toString` method. Browsers treat all child Nodes of <title> tags as Text ' + + 'content and React expects to be able to convert children of <title> tags to a single string value. ' + + 'Using the default `toString` method available on every object is almost certainly an error. ' + + 'Consider whether the `children` of this <title> is an object in error and change it to a ' + + 'string or number value if so. Otherwise implement a `toString` method that React can ' + + 'use to produce a valid <title>.', + ], + {withoutStack: true}, + ); expect(errors).toEqual([]); // object titles are toStringed when float is on expect(getVisibleChildren(document.head)).toEqual(