Skip to content

Commit c07aaee

Browse files
committed
Build a "hydration diff" highlighting the two nodes involved
1 parent cd6656b commit c07aaee

File tree

6 files changed

+183
-31
lines changed

6 files changed

+183
-31
lines changed

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

Lines changed: 71 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -7,9 +7,11 @@
77
* @flow
88
*/
99

10+
import type {Fiber} from 'react-reconciler/src/ReactInternalTypes';
11+
import type {HydrationDiffNode} from 'react-reconciler/src/ReactFiberHydrationDiffs';
12+
1013
import {enableOwnerStacks} from 'shared/ReactFeatureFlags';
1114

12-
import type {Fiber} from 'react-reconciler/src/ReactInternalTypes';
1315
import {
1416
current,
1517
runWithFiberInDEV,
@@ -18,8 +20,42 @@ import {
1820
HostComponent,
1921
HostHoistable,
2022
HostSingleton,
23+
HostText,
2124
} from 'react-reconciler/src/ReactWorkTags';
2225

26+
import {describeDiff} from 'react-reconciler/src/ReactFiberHydrationDiffs';
27+
28+
function describeAncestors(
29+
ancestor: Fiber,
30+
child: Fiber,
31+
props: null | {children: null},
32+
): string {
33+
let fiber: null | Fiber = child;
34+
let node: null | HydrationDiffNode = null;
35+
let distanceFromLeaf = 0;
36+
while (fiber) {
37+
if (fiber === ancestor) {
38+
distanceFromLeaf = 0;
39+
}
40+
node = {
41+
fiber: fiber,
42+
children: node !== null ? [node] : [],
43+
serverProps:
44+
fiber === child ? props : fiber === ancestor ? null : undefined,
45+
serverTail: [],
46+
distanceFromLeaf: distanceFromLeaf,
47+
};
48+
distanceFromLeaf++;
49+
fiber = fiber.return;
50+
}
51+
if (node !== null) {
52+
// Describe the node using the hydration diff logic.
53+
// Replace + with - to mark ancestor and child. It's kind of arbitrary.
54+
return describeDiff(node).replace(/\n\+/g, '\n-');
55+
}
56+
return '';
57+
}
58+
2359
type Info = {tag: string};
2460
export type AncestorInfoDev = {
2561
current: ?Info,
@@ -451,7 +487,7 @@ function findInvalidAncestorForTag(
451487

452488
const didWarn: {[string]: boolean} = {};
453489

454-
function findAncestor(parent: Fiber, tagName: string): null | Fiber {
490+
function findAncestor(parent: null | Fiber, tagName: string): null | Fiber {
455491
while (parent) {
456492
switch (parent.tag) {
457493
case HostComponent:
@@ -496,6 +532,14 @@ function validateDOMNesting(
496532
}
497533
didWarn[warnKey] = true;
498534

535+
const child = current;
536+
const ancestor = child ? findAncestor(child.return, ancestorTag) : null;
537+
538+
const ancestorDescription =
539+
child !== null && ancestor !== null
540+
? describeAncestors(ancestor, child, null)
541+
: '';
542+
499543
const tagDisplayName = '<' + childTag + '>';
500544
if (invalidParent) {
501545
let info = '';
@@ -511,10 +555,11 @@ function validateDOMNesting(
511555
// a stack trace since the stack trace format is now for owner stacks.
512556
console.error(
513557
'In HTML, %s cannot be a child of <%s>.%s\n' +
514-
'This will cause a hydration error.',
558+
'This will cause a hydration error.%s',
515559
tagDisplayName,
516560
ancestorTag,
517561
info,
562+
ancestorDescription,
518563
);
519564
} else {
520565
// Don't transform into consoleWithStackDev here because we add a manual stack.
@@ -524,21 +569,21 @@ function validateDOMNesting(
524569
// a stack trace since the stack trace format is now for owner stacks.
525570
console.error(
526571
'In HTML, %s cannot be a descendant of <%s>.\n' +
527-
'This will cause a hydration error.',
572+
'This will cause a hydration error.%s',
528573
tagDisplayName,
529574
ancestorTag,
575+
ancestorDescription,
530576
);
531577
}
532-
if (enableOwnerStacks && current !== null) {
578+
if (enableOwnerStacks && child) {
533579
// For debugging purposes find the nearest ancestor that caused the issue.
534580
// The stack trace of this ancestor can be useful to find the cause.
535581
// If the parent is a direct parent in the same owner, we don't bother.
536-
const currentFiber = current;
537-
const parent = current.return;
538-
const ancestor = findAncestor(parent, ancestorTag);
582+
const parent = child.return;
539583
if (
540-
ancestor &&
541-
(ancestor !== parent || parent._debugOwner !== currentFiber._debugOwner)
584+
ancestor !== null &&
585+
parent !== null &&
586+
(ancestor !== parent || parent._debugOwner !== child._debugOwner)
542587
) {
543588
runWithFiberInDEV(ancestor, () => {
544589
console.error(
@@ -569,13 +614,26 @@ function validateTextNesting(childText: string, parentTag: string): boolean {
569614
}
570615
didWarn[warnKey] = true;
571616

617+
const child = current;
618+
const ancestor = child ? findAncestor(child, parentTag) : null;
619+
620+
const ancestorDescription =
621+
child !== null && ancestor !== null
622+
? describeAncestors(
623+
ancestor,
624+
child,
625+
child.tag !== HostText ? {children: null} : null,
626+
)
627+
: '';
628+
572629
if (/\S/.test(childText)) {
573630
// TODO: Format this as a linkified "diff view" with props instead of
574631
// a stack trace since the stack trace format is now for owner stacks.
575632
console.error(
576633
'In HTML, text nodes cannot be a child of <%s>.\n' +
577-
'This will cause a hydration error.',
634+
'This will cause a hydration error.%s',
578635
parentTag,
636+
ancestorDescription,
579637
);
580638
} else {
581639
// TODO: Format this as a linkified "diff view" with props instead of
@@ -584,8 +642,9 @@ function validateTextNesting(childText: string, parentTag: string): boolean {
584642
'In HTML, whitespace text nodes cannot be a child of <%s>. ' +
585643
"Make sure you don't have any extra whitespace between tags on " +
586644
'each line of your source code.\n' +
587-
'This will cause a hydration error.',
645+
'This will cause a hydration error.%s',
588646
parentTag,
647+
ancestorDescription,
589648
);
590649
}
591650
return false;

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

Lines changed: 61 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -2194,9 +2194,12 @@ describe('ReactDOMComponent', () => {
21942194
);
21952195
});
21962196
}).toErrorDev(
2197-
'In HTML, <tr> cannot be a child of ' +
2198-
'<div>.\n' +
2199-
'This will cause a hydration error.' +
2197+
'In HTML, <tr> cannot be a child of <div>.\n' +
2198+
'This will cause a hydration error.\n' +
2199+
'\n' +
2200+
'- <div>\n' +
2201+
'- <tr>\n' +
2202+
' ...\n' +
22002203
'\n in tr (at **)' +
22012204
(gate(flags => flags.enableOwnerStacks)
22022205
? ''
@@ -2257,44 +2260,80 @@ describe('ReactDOMComponent', () => {
22572260
'In HTML, <tr> cannot be a child of ' +
22582261
'<table>. Add a <tbody>, <thead> or <tfoot> to your code to match the DOM tree generated ' +
22592262
'by the browser.\n' +
2260-
'This will cause a hydration error.' +
2263+
'This will cause a hydration error.\n' +
2264+
'\n' +
2265+
' <Foo>\n' +
2266+
'- <table>\n' +
2267+
' <Row>\n' +
2268+
'- <tr>\n' +
2269+
' ...\n' +
22612270
'\n in tr (at **)' +
22622271
'\n in Row (at **)',
22632272
'<table> cannot contain a nested <tr>.\nSee this log for the ancestor stack trace.' +
22642273
'\n in table (at **)' +
22652274
'\n in Foo (at **)',
22662275
'In HTML, text nodes cannot be a ' +
22672276
'child of <tr>.\n' +
2268-
'This will cause a hydration error.' +
2277+
'This will cause a hydration error.\n' +
2278+
'\n' +
2279+
' <Foo>\n' +
2280+
' <table>\n' +
2281+
' <Row>\n' +
2282+
' <tr>\n' +
2283+
'- x\n' +
2284+
' ...\n' +
22692285
'\n in tr (at **)' +
22702286
'\n in Row (at **)',
22712287
'In HTML, whitespace text nodes cannot ' +
22722288
"be a child of <table>. Make sure you don't have any extra " +
22732289
'whitespace between tags on each line of your source code.\n' +
2274-
'This will cause a hydration error.' +
2290+
'This will cause a hydration error.\n' +
2291+
'\n' +
2292+
' <Foo>\n' +
2293+
'- <table>\n' +
2294+
' <Row>\n' +
2295+
'- {" "}\n' +
22752296
'\n in table (at **)' +
22762297
'\n in Foo (at **)',
22772298
]
22782299
: [
22792300
'In HTML, <tr> cannot be a child of ' +
22802301
'<table>. Add a <tbody>, <thead> or <tfoot> to your code to match the DOM tree generated ' +
22812302
'by the browser.\n' +
2282-
'This will cause a hydration error.' +
2303+
'This will cause a hydration error.\n' +
2304+
'\n' +
2305+
' <Foo>\n' +
2306+
'- <table>\n' +
2307+
' <Row>\n' +
2308+
'- <tr>\n' +
2309+
' ...\n' +
22832310
'\n in tr (at **)' +
22842311
'\n in Row (at **)' +
22852312
'\n in table (at **)' +
22862313
'\n in Foo (at **)',
22872314
'In HTML, text nodes cannot be a ' +
22882315
'child of <tr>.\n' +
2289-
'This will cause a hydration error.' +
2316+
'This will cause a hydration error.\n' +
2317+
'\n' +
2318+
' <Foo>\n' +
2319+
' <table>\n' +
2320+
' <Row>\n' +
2321+
' <tr>\n' +
2322+
'- x\n' +
2323+
' ...\n' +
22902324
'\n in tr (at **)' +
22912325
'\n in Row (at **)' +
22922326
'\n in table (at **)' +
22932327
'\n in Foo (at **)',
22942328
'In HTML, whitespace text nodes cannot ' +
22952329
"be a child of <table>. Make sure you don't have any extra " +
22962330
'whitespace between tags on each line of your source code.\n' +
2297-
'This will cause a hydration error.' +
2331+
'This will cause a hydration error.\n' +
2332+
'\n' +
2333+
' <Foo>\n' +
2334+
'- <table>\n' +
2335+
' <Row>\n' +
2336+
'- {" "}\n' +
22982337
'\n in table (at **)' +
22992338
'\n in Foo (at **)',
23002339
],
@@ -2325,7 +2364,11 @@ describe('ReactDOMComponent', () => {
23252364
'In HTML, whitespace text nodes cannot ' +
23262365
"be a child of <table>. Make sure you don't have any extra " +
23272366
'whitespace between tags on each line of your source code.\n' +
2328-
'This will cause a hydration error.' +
2367+
'This will cause a hydration error.\n' +
2368+
'\n' +
2369+
' <Foo>\n' +
2370+
' <table>\n' +
2371+
'- {" "}\n' +
23292372
'\n in table (at **)' +
23302373
'\n in Foo (at **)',
23312374
]);
@@ -2353,7 +2396,14 @@ describe('ReactDOMComponent', () => {
23532396
}).toErrorDev([
23542397
'In HTML, text nodes cannot be a ' +
23552398
'child of <tr>.\n' +
2356-
'This will cause a hydration error.' +
2399+
'This will cause a hydration error.\n' +
2400+
'\n' +
2401+
' <Foo>\n' +
2402+
' <table>\n' +
2403+
' <tbody>\n' +
2404+
' <Row>\n' +
2405+
' <tr>\n' +
2406+
'- text\n' +
23572407
'\n in tr (at **)' +
23582408
'\n in Row (at **)' +
23592409
(gate(flags => flags.enableOwnerStacks)

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

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -387,7 +387,11 @@ describe('ReactDOMForm', () => {
387387
});
388388
}).toErrorDev(
389389
'In HTML, <form> cannot be a descendant of <form>.\n' +
390-
'This will cause a hydration error.' +
390+
'This will cause a hydration error.\n' +
391+
'\n' +
392+
'- <form action={function outerAction}>\n' +
393+
' <input>\n' +
394+
'- <form action={function innerAction} ref={{current:null}}>\n' +
391395
'\n in form (at **)' +
392396
(gate(flags => flags.enableOwnerStacks) ? '' : '\n in form (at **)'),
393397
);

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

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,11 @@ describe('ReactDOMOption', () => {
5353
}).toErrorDev(
5454
'In HTML, <div> cannot be a child of <option>.\n' +
5555
'This will cause a hydration error.\n' +
56+
'\n' +
57+
'- <option value="12">\n' +
58+
'- <div>\n' +
59+
' ...\n' +
60+
'\n' +
5661
' in div (at **)' +
5762
(gate(flags => flags.enableOwnerStacks)
5863
? ''
@@ -271,6 +276,12 @@ describe('ReactDOMOption', () => {
271276
}).toErrorDev(
272277
'In HTML, <div> cannot be a child of <option>.\n' +
273278
'This will cause a hydration error.\n' +
279+
'\n' +
280+
' <select readOnly={true} value="bar">\n' +
281+
'- <option value="bar">\n' +
282+
'- <div ref={{current:null}}>\n' +
283+
' ...\n' +
284+
'\n' +
274285
' in div (at **)' +
275286
(gate(flags => flags.enableOwnerStacks)
276287
? ''

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

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -97,13 +97,25 @@ describe('validateDOMNesting', () => {
9797
? [
9898
'In HTML, <li> cannot be a descendant of <li>.\n' +
9999
'This will cause a hydration error.\n' +
100+
'\n' +
101+
' <ul>\n' +
102+
'- <li>\n' +
103+
' <div>\n' +
104+
'- <li>\n' +
105+
'\n' +
100106
' in li (at **)',
101107
'<li> cannot contain a nested <li>.\nSee this log for the ancestor stack trace.\n' +
102108
' in li (at **)',
103109
]
104110
: [
105111
'In HTML, <li> cannot be a descendant of <li>.\n' +
106112
'This will cause a hydration error.\n' +
113+
'\n' +
114+
' <ul>\n' +
115+
'- <li>\n' +
116+
' <div>\n' +
117+
'- <li>\n' +
118+
'\n' +
107119
' in li (at **)\n' +
108120
' in div (at **)\n' +
109121
' in li (at **)\n' +
@@ -133,6 +145,10 @@ describe('validateDOMNesting', () => {
133145
// TODO, this should say "In SVG",
134146
'In HTML, <body> cannot be a child of <foreignObject>.\n' +
135147
'This will cause a hydration error.\n' +
148+
'\n' +
149+
'- <foreignObject>\n' +
150+
'- <body>\n' +
151+
'\n' +
136152
' in body (at **)',
137153
'You are mounting a new body component when a previous one has not first unmounted. It is an error to render more than one body component at a time and attributes and children of these components will likely fail in unpredictable ways. Please only render a single instance of <body> and if you need to mount a new one, ensure any previous ones have unmounted first.\n' +
138154
' in body (at **)',
@@ -141,6 +157,10 @@ describe('validateDOMNesting', () => {
141157
// TODO, this should say "In SVG",
142158
'In HTML, <body> cannot be a child of <foreignObject>.\n' +
143159
'This will cause a hydration error.\n' +
160+
'\n' +
161+
'- <foreignObject>\n' +
162+
'- <body>\n' +
163+
'\n' +
144164
' in body (at **)\n' +
145165
' in foreignObject (at **)',
146166
'You are mounting a new body component when a previous one has not first unmounted. It is an error to render more than one body component at a time and attributes and children of these components will likely fail in unpredictable ways. Please only render a single instance of <body> and if you need to mount a new one, ensure any previous ones have unmounted first.\n' +

0 commit comments

Comments
 (0)