Skip to content

Commit aee09bb

Browse files
committed
[Fiber] relax DOM validation rules at root
in react-dom in Dev we validate that the tag nesting is valid. This is motivated primarily because while browsers are tolerant to poor HTML there are many cases that if server rendered will be hydrated in a way that will break hydration. With the changes to singleton scoping where the document body is now the implicit render/hydration context for arbitrary tags at the root we need to adjust the validation logic to allow for valid programs such as rendering divs as a child of a Document (since this div will actually insert into the body).
1 parent c40b0ae commit aee09bb

File tree

4 files changed

+25
-22
lines changed

4 files changed

+25
-22
lines changed

packages/react-dom-bindings/src/client/validateDOMNesting.js

+25-6
Original file line numberDiff line numberDiff line change
@@ -71,6 +71,7 @@ export type AncestorInfoDev = {
7171

7272
// <head> or <body>
7373
containerTagInScope: ?Info,
74+
implicitBodyScope: boolean,
7475
};
7576

7677
// This validation code was written based on the HTML5 parsing spec:
@@ -219,16 +220,19 @@ const emptyAncestorInfoDev: AncestorInfoDev = {
219220
dlItemTagAutoclosing: null,
220221

221222
containerTagInScope: null,
223+
implicitBodyScope: false,
222224
};
223225

224226
function updatedAncestorInfoDev(
225-
oldInfo: ?AncestorInfoDev,
227+
oldInfo: null | AncestorInfoDev,
226228
tag: string,
227229
): AncestorInfoDev {
228230
if (__DEV__) {
229231
const ancestorInfo = {...(oldInfo || emptyAncestorInfoDev)};
230232
const info = {tag};
231233

234+
ancestorInfo.implicitBodyScope = false;
235+
232236
if (inScopeTags.indexOf(tag) !== -1) {
233237
ancestorInfo.aTagInScope = null;
234238
ancestorInfo.buttonTagInScope = null;
@@ -238,14 +242,14 @@ function updatedAncestorInfoDev(
238242
ancestorInfo.pTagInButtonScope = null;
239243
}
240244

241-
// See rules for 'li', 'dd', 'dt' start tags in
242-
// https://html.spec.whatwg.org/multipage/syntax.html#parsing-main-inbody
243245
if (
244246
specialTags.indexOf(tag) !== -1 &&
245247
tag !== 'address' &&
246248
tag !== 'div' &&
247249
tag !== 'p'
248250
) {
251+
// See rules for 'li', 'dd', 'dt' start tags in
252+
// https://html.spec.whatwg.org/multipage/syntax.html#parsing-main-inbody
249253
ancestorInfo.listItemTagAutoclosing = null;
250254
ancestorInfo.dlItemTagAutoclosing = null;
251255
}
@@ -274,6 +278,10 @@ function updatedAncestorInfoDev(
274278
ancestorInfo.dlItemTagAutoclosing = info;
275279
}
276280
if (tag === '#document' || tag === 'html') {
281+
if (oldInfo === null) {
282+
// We're at the root with implicit body scope
283+
ancestorInfo.implicitBodyScope = true;
284+
}
277285
ancestorInfo.containerTagInScope = null;
278286
} else if (!ancestorInfo.containerTagInScope) {
279287
ancestorInfo.containerTagInScope = info;
@@ -363,11 +371,16 @@ function isTagValidWithParent(tag: string, parentTag: ?string): boolean {
363371
);
364372
// https://html.spec.whatwg.org/multipage/semantics.html#the-html-element
365373
case 'html':
366-
return tag === 'head' || tag === 'body' || tag === 'frameset';
374+
return (
375+
tag === 'head' ||
376+
tag === 'body' ||
377+
tag === 'frameset' ||
378+
parentTag === null
379+
);
367380
case 'frameset':
368381
return tag === 'frame';
369382
case '#document':
370-
return tag === 'html';
383+
return tag === 'html' || parentTag === null;
371384
}
372385

373386
// Probably in the "in body" parsing mode, so we outlaw only tag combos
@@ -511,7 +524,13 @@ function validateDOMNesting(
511524
if (__DEV__) {
512525
ancestorInfo = ancestorInfo || emptyAncestorInfoDev;
513526
const parentInfo = ancestorInfo.current;
514-
const parentTag = parentInfo && parentInfo.tag;
527+
const parentTag =
528+
parentInfo &&
529+
// If we are in implicit body scope we validate as if we have no parent.
530+
// This is because we expect the host to insert the tag into the necessary context
531+
ancestorInfo.implicitBodyScope === false
532+
? parentInfo.tag
533+
: null;
515534

516535
const invalidParent = isTagValidWithParent(childTag, parentTag)
517536
? null

packages/react-dom/src/__tests__/ReactDOM-test.js

-8
Original file line numberDiff line numberDiff line change
@@ -601,10 +601,6 @@ describe('ReactDOM', () => {
601601
'<html lang="en"><head data-h=""><meta itemprop="" content="head"></head><body data-b=""><div>before</div><div>inside</div><div>after</div></body></html>',
602602
);
603603

604-
// @TODO remove this warning check when we loosen the tag nesting restrictions to allow arbitrary tags at the
605-
// root of the application
606-
assertConsoleErrorDev(['In HTML, <div> cannot be a child of <#document>']);
607-
608604
await act(() => {
609605
root.render(<App phase={1} />);
610606
});
@@ -666,10 +662,6 @@ describe('ReactDOM', () => {
666662
'<html><head data-h=""><meta itemprop="" content="head"></head><body data-b=""><div>before</div><div>inside</div><div>after</div></body></html>',
667663
);
668664

669-
// @TODO remove this warning check when we loosen the tag nesting restrictions to allow arbitrary tags at the
670-
// root of the application
671-
assertConsoleErrorDev(['In HTML, <div> cannot be a child of <html>']);
672-
673665
await act(() => {
674666
root.render(<App phase={1} />);
675667
});

packages/react-dom/src/__tests__/ReactDOMFizzServer-test.js

-2
Original file line numberDiff line numberDiff line change
@@ -9004,7 +9004,6 @@ describe('ReactDOMFizzServer', () => {
90049004
</body>
90059005
</html>,
90069006
);
9007-
assertConsoleErrorDev(['In HTML, <div> cannot be a child of <#document>']);
90089007

90099008
root.unmount();
90109009
expect(getVisibleChildren(document)).toEqual(
@@ -10173,7 +10172,6 @@ describe('ReactDOMFizzServer', () => {
1017310172
</body>
1017410173
</html>,
1017510174
);
10176-
assertConsoleErrorDev(['In HTML, <div> cannot be a child of <#document>']);
1017710175

1017810176
root.unmount();
1017910177
expect(getVisibleChildren(document)).toEqual(

packages/react-dom/src/__tests__/ReactDOMFloat-test.js

-6
Original file line numberDiff line numberDiff line change
@@ -511,9 +511,6 @@ describe('ReactDOMFloat', () => {
511511
'Cannot render <noscript> outside the main document. Try moving it into the root <head> tag.',
512512
{withoutStack: true},
513513
],
514-
'In HTML, <noscript> cannot be a child of <#document>.\n' +
515-
'This will cause a hydration error.\n' +
516-
' in noscript (at **)',
517514
]);
518515

519516
root.render(
@@ -577,9 +574,6 @@ describe('ReactDOMFloat', () => {
577574
'Consider adding precedence="default" or moving it into the root <head> tag.',
578575
{withoutStack: true},
579576
],
580-
'In HTML, <link> cannot be a child of <#document>.\n' +
581-
'This will cause a hydration error.\n' +
582-
' in link (at **)',
583577
]);
584578

585579
root.render(

0 commit comments

Comments
 (0)