Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions packages/react-dom-bindings/src/client/ReactDOMComponent.js
Original file line number Diff line number Diff line change
Expand Up @@ -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';

Expand Down Expand Up @@ -99,6 +100,7 @@ function validatePropertiesInDevelopment(type: string, props: any) {
registrationNameDependencies,
possibleRegistrationNames,
});
validateTitleProperties(type, props);
if (
props.contentEditable &&
!props.suppressContentEditableWarning &&
Expand Down
47 changes: 2 additions & 45 deletions packages/react-dom-bindings/src/server/ReactFizzConfigDOM.js
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -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 <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}</title>. 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: <title>{`hello ${nameOfUser}`}</title>.',
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 <title> 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 (
Expand Down
64 changes: 64 additions & 0 deletions packages/react-dom-bindings/src/shared/ReactDOMTitle.js
Original file line number Diff line number Diff line change
@@ -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}</title>. 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: <title>{`hello ${nameOfUser}`}</title>.',
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 <title> 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>.',
);
}
}
}
}
}
18 changes: 18 additions & 0 deletions packages/react-dom/src/__tests__/ReactDOMComponent-test.js
Original file line number Diff line number Diff line change
Expand Up @@ -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}</title>. 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: <title>{`hello ${nameOfUser}`}</title>.\n' +
' in title (at **)',
]);
});

it('should warn about incorrect casing on properties', async () => {
const container = document.createElement('div');
const root = ReactDOMClient.createRoot(container);
Expand Down
41 changes: 41 additions & 0 deletions packages/react-dom/src/__tests__/ReactDOMFizzServer-test.js
Original file line number Diff line number Diff line change
Expand Up @@ -5926,6 +5926,21 @@ 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 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}</title>. ' +
'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: ' +
'<title>{`hello ${nameOfUser}`}</title>.',
],
{withoutStack: true},
);
expect(errors).toEqual([]);
// with float, the title doesn't render on the client or on the server
expect(getVisibleChildren(document.head)).toEqual(<title />);
Expand Down Expand Up @@ -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(
Expand Down Expand Up @@ -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(
Expand Down