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
19 changes: 16 additions & 3 deletions packages/react-dom-bindings/src/client/ReactDOMComponent.js
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,10 @@ import {
restoreControlledTextareaState,
} from './ReactDOMTextarea';
import {setSrcObject} from './ReactDOMSrcObject';
import {validateTextNesting} from './validateDOMNesting';
import {
validateTextNesting,
getHostContextNamespaceForDomNamespace,
} from './validateDOMNesting';
import setTextContent from './setTextContent';
import {
createDangerousStringForStyles,
Expand Down Expand Up @@ -393,7 +396,12 @@ function setProp(
case 'children': {
if (typeof value === 'string') {
if (__DEV__) {
validateTextNesting(value, tag, false);
validateTextNesting(
value,
tag,
false,
getHostContextNamespaceForDomNamespace(domElement.namespaceURI),
);
}
// Avoid setting initial textContent when the text is empty. In IE11 setting
// textContent on a <textarea> will cause the placeholder to not
Expand All @@ -407,7 +415,12 @@ function setProp(
} else if (typeof value === 'number' || typeof value === 'bigint') {
if (__DEV__) {
// $FlowFixMe[unsafe-addition] Flow doesn't want us to use `+` operator with string and bigint
validateTextNesting('' + value, tag, false);
validateTextNesting(
'' + value,
tag,
false,
getHostContextNamespaceForDomNamespace(domElement.namespaceURI),
);
}
const canSetTextContent = tag !== 'body';
if (canSetTextContent) {
Expand Down
8 changes: 5 additions & 3 deletions packages/react-dom-bindings/src/client/ReactFiberConfigDOM.js
Original file line number Diff line number Diff line change
Expand Up @@ -532,7 +532,7 @@ export function createInstance(
if (__DEV__) {
// TODO: take namespace into account when validating.
const hostContextDev: HostContextDev = (hostContext: any);
validateDOMNesting(type, hostContextDev.ancestorInfo);
validateDOMNesting(type, hostContextDev.ancestorInfo, hostContextDev.context);
hostContextProd = hostContextDev.context;
} else {
hostContextProd = (hostContext: any);
Expand Down Expand Up @@ -758,6 +758,7 @@ export function createTextInstance(
text,
ancestor.tag,
hostContextDev.ancestorInfo.implicitRootScope,
hostContextDev.context,
);
}
}
Expand Down Expand Up @@ -4249,7 +4250,7 @@ export function validateHydratableInstance(
if (__DEV__) {
// TODO: take namespace into account when validating.
const hostContextDev: HostContextDev = (hostContext: any);
return validateDOMNesting(type, hostContextDev.ancestorInfo);
return validateDOMNesting(type, hostContextDev.ancestorInfo, hostContextDev.context);
}
return true;
}
Expand Down Expand Up @@ -4291,6 +4292,7 @@ export function validateHydratableTextInstance(
text,
ancestor.tag,
hostContextDev.ancestorInfo.implicitRootScope,
hostContextDev.context,
);
}
}
Expand Down Expand Up @@ -4635,7 +4637,7 @@ export function resolveSingletonInstance(
if (__DEV__) {
const hostContextDev = ((hostContext: any): HostContextDev);
if (validateDOMNestingDev) {
validateDOMNesting(type, hostContextDev.ancestorInfo);
validateDOMNesting(type, hostContextDev.ancestorInfo, hostContextDev.context);
}
}
const ownerDocument = getOwnerDocumentFromRootContainer(
Expand Down
41 changes: 37 additions & 4 deletions packages/react-dom-bindings/src/client/validateDOMNesting.js
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,31 @@ import {

import {describeDiff} from 'react-reconciler/src/ReactFiberHydrationDiffs';

import {MATH_NAMESPACE, SVG_NAMESPACE} from './DOMNamespaces';

/**
* Mirrors HostContextNamespace in ReactFiberConfigDOM.js (None / Svg / Math).
* Used only for dev-only nesting warning prefixes.
*/
export type HostContextNamespace = 0 | 1 | 2;

export function getHostContextNamespaceForDomNamespace(
namespaceURI: string | null,
): HostContextNamespace {
if (namespaceURI === SVG_NAMESPACE) {
return 1;
}
if (namespaceURI === MATH_NAMESPACE) {
return 2;
}
return 0;
}

function getContextLabel(hostContextNamespace: HostContextNamespace): string {
// "In SVG" only in the SVG namespace; foreignObject switches to HTML (integration point).
return hostContextNamespace === 1 ? 'In SVG' : 'In HTML';
}

function describeAncestors(
ancestor: Fiber,
child: Fiber,
Expand Down Expand Up @@ -549,6 +574,7 @@ function findAncestor(parent: null | Fiber, tagName: string): null | Fiber {
function validateDOMNesting(
childTag: string,
ancestorInfo: AncestorInfoDev,
hostContextNamespace: HostContextNamespace,
): boolean {
if (__DEV__) {
ancestorInfo = ancestorInfo || emptyAncestorInfoDev;
Expand Down Expand Up @@ -589,6 +615,7 @@ function validateDOMNesting(
: '';

const tagDisplayName = '<' + childTag + '>';
const contextLabel = getContextLabel(hostContextNamespace);
if (invalidParent) {
let info = '';
if (ancestorTag === 'table' && childTag === 'tr') {
Expand All @@ -597,17 +624,19 @@ function validateDOMNesting(
'the browser.';
}
console.error(
'In HTML, %s cannot be a child of <%s>.%s\n' +
'%s, %s cannot be a child of <%s>.%s\n' +
'This will cause a hydration error.%s',
contextLabel,
tagDisplayName,
ancestorTag,
info,
ancestorDescription,
);
} else {
console.error(
'In HTML, %s cannot be a descendant of <%s>.\n' +
'%s, %s cannot be a descendant of <%s>.\n' +
'This will cause a hydration error.%s',
contextLabel,
tagDisplayName,
ancestorTag,
ancestorDescription,
Expand Down Expand Up @@ -644,6 +673,7 @@ function validateTextNesting(
childText: string,
parentTag: string,
implicitRootScope: boolean,
hostContextNamespace: HostContextNamespace,
): boolean {
if (__DEV__) {
if (implicitRootScope || isTagValidWithParent('#text', parentTag, false)) {
Expand All @@ -668,19 +698,22 @@ function validateTextNesting(
)
: '';

const contextLabel = getContextLabel(hostContextNamespace);
if (/\S/.test(childText)) {
console.error(
'In HTML, text nodes cannot be a child of <%s>.\n' +
'%s, text nodes cannot be a child of <%s>.\n' +
'This will cause a hydration error.%s',
contextLabel,
parentTag,
ancestorDescription,
);
} else {
console.error(
'In HTML, whitespace text nodes cannot be a child of <%s>. ' +
'%s, whitespace text nodes cannot be a child of <%s>. ' +
"Make sure you don't have any extra whitespace between tags on " +
'each line of your source code.\n' +
'This will cause a hydration error.%s',
contextLabel,
parentTag,
ancestorDescription,
);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -192,7 +192,7 @@ describe('validateDOMNesting', () => {
expectWarnings(
['svg', 'foreignObject', 'body', 'p'],
[
// TODO, this should say "In SVG",
// foreignObject is an HTML integration point: nested HTML uses the HTML namespace.
'In HTML, <body> cannot be a child of <foreignObject>.\n' +
'This will cause a hydration error.\n' +
'\n' +
Expand Down