@samitouri / QOS-React-2 / commits / 2d3f81bb6a

Format DOM Nesting Warning as Diff View + An Additional Log for Stack Trace (#30302)

Currently we're printing parent stacks at the end of DOM nesting even with owner stacks enabled. That's because the context of parent tree is relevant for determining why two things are nested. It might not be sufficient to see the owner stack alone. I'm trying to get rid of parent stacks and rely on more of the plain owner stacks or ideally console.createTask. These are generally better anyway since the exact line for creating the JSX is available. It also lets you find a parent stack frame that is most relevant e.g. if it's hidden inside internals. For DOM nesting there's really only two stacks that are relevant. The creation of the parent and the creation of the child. Sometimes they're close enough to be the same thing. Such as for parents that can't have text children or when the ancestor is the direct parent created at the same place (same owner). Sometimes they're far apart. In this case I add a second console.error within the context of the ancestor. That way the second stack trace can be used to read the stack trace for where it was created. To preserve some parent context I now print the parent stack in a diff view format using the logic from hydration diffs. This includes some siblings and props for context. <img width="756" alt="Screenshot 2024-07-10 at 12 21 38 AM" src="https://github.com/facebook/react/assets/63648/0843133d-cc7a-4ecc-91c0-f46ae8e99f20"> Text Nodes: <img width="749" alt="Screenshot 2024-07-10 at 12 37 40 AM" src="https://github.com/facebook/react/assets/63648/ee377d82-54ee-450a-99d1-fcc3ef290d59"> --------- Co-authored-by: tjallingt <tjallingt@gmail.com>

Sebastian Markbåge committed Jul 10, 2024 at 12:17 UTC 2d3f81bb6a650386832d885d7b63a7d0d517ba15
8 files changed +453 -139
packages/react-dom-bindings/src/client/validateDOMNesting.js
+113 -29
@@ -7,7 +7,54 @@
7 * @flow
8 */
9
10 -import {getCurrentParentStackInDev} from 'react-reconciler/src/ReactCurrentFiber';
10 +import type {Fiber} from 'react-reconciler/src/ReactInternalTypes';
11 +import type {HydrationDiffNode} from 'react-reconciler/src/ReactFiberHydrationDiffs';
12 +
13 +import {enableOwnerStacks} from 'shared/ReactFeatureFlags';
14 +
15 +import {
16 + current,
17 + runWithFiberInDEV,
18 +} from 'react-reconciler/src/ReactCurrentFiber';
19 +import {
20 + HostComponent,
21 + HostHoistable,
22 + HostSingleton,
23 + HostText,
24 +} from 'react-reconciler/src/ReactWorkTags';
25 +
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).replaceAll(/^[+-]/gm, '>');
55 + }
56 + return '';
57 +}
58
59 type Info = {tag: string};
60 export type AncestorInfoDev = {
@@ -440,6 +487,21 @@ function findInvalidAncestorForTag(
487
488 const didWarn: {[string]: boolean} = {};
489
490 +function findAncestor(parent: null | Fiber, tagName: string): null | Fiber {
491 + while (parent) {
492 + switch (parent.tag) {
493 + case HostComponent:
494 + case HostHoistable:
495 + case HostSingleton:
496 + if (parent.type === tagName) {
497 + return parent;
498 + }
499 + }
500 + parent = parent.return;
501 + }
502 + return null;
503 +}
504 +
505 function validateDOMNesting(
506 childTag: string,
507 ancestorInfo: AncestorInfoDev,
@@ -470,6 +532,14 @@ function validateDOMNesting(
532 }
533 didWarn[warnKey] = true;
534
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 +
543 const tagDisplayName = '<' + childTag + '>';
544 if (invalidParent) {
545 let info = '';
@@ -478,33 +548,45 @@ function validateDOMNesting(
548 ' Add a <tbody>, <thead> or <tfoot> to your code to match the DOM tree generated by ' +
549 'the browser.';
550 }
481 - // Don't transform into consoleWithStackDev here because we add a manual stack.
482 - // We use the parent stack here instead of the owner stack because the parent
483 - // stack has more useful context for nesting.
484 - // TODO: Format this as a linkified "diff view" with props instead of
485 - // a stack trace since the stack trace format is now for owner stacks.
486 - console['error'](
551 + console.error(
552 'In HTML, %s cannot be a child of <%s>.%s\n' +
553 'This will cause a hydration error.%s',
554 tagDisplayName,
555 ancestorTag,
556 info,
492 - getCurrentParentStackInDev(),
557 + ancestorDescription,
558 );
559 } else {
495 - // Don't transform into consoleWithStackDev here because we add a manual stack.
496 - // We use the parent stack here instead of the owner stack because the parent
497 - // stack has more useful context for nesting.
498 - // TODO: Format this as a linkified "diff view" with props instead of
499 - // a stack trace since the stack trace format is now for owner stacks.
500 - console['error'](
560 + console.error(
561 'In HTML, %s cannot be a descendant of <%s>.\n' +
562 'This will cause a hydration error.%s',
563 tagDisplayName,
564 ancestorTag,
505 - getCurrentParentStackInDev(),
565 + ancestorDescription,
566 );
567 }
568 + if (enableOwnerStacks && child) {
569 + // For debugging purposes find the nearest ancestor that caused the issue.
570 + // The stack trace of this ancestor can be useful to find the cause.
571 + // If the parent is a direct parent in the same owner, we don't bother.
572 + const parent = child.return;
573 + if (
574 + ancestor !== null &&
575 + parent !== null &&
576 + (ancestor !== parent || parent._debugOwner !== child._debugOwner)
577 + ) {
578 + runWithFiberInDEV(ancestor, () => {
579 + console.error(
580 + // We repeat some context because this log might be taken out of context
581 + // such as in React DevTools or grouped server logs.
582 + '<%s> cannot contain a nested %s.\n' +
583 + 'See this log for the ancestor stack trace.',
584 + ancestorTag,
585 + tagDisplayName,
586 + );
587 + });
588 + }
589 + }
590 return false;
591 }
592 return true;
@@ -522,31 +604,33 @@ function validateTextNesting(childText: string, parentTag: string): boolean {
604 }
605 didWarn[warnKey] = true;
606
607 + const child = current;
608 + const ancestor = child ? findAncestor(child, parentTag) : null;
609 +
610 + const ancestorDescription =
611 + child !== null && ancestor !== null
612 + ? describeAncestors(
613 + ancestor,
614 + child,
615 + child.tag !== HostText ? {children: null} : null,
616 + )
617 + : '';
618 +
619 if (/\S/.test(childText)) {
526 - // Don't transform into consoleWithStackDev here because we add a manual stack.
527 - // We use the parent stack here instead of the owner stack because the parent
528 - // stack has more useful context for nesting.
529 - // TODO: Format this as a linkified "diff view" with props instead of
530 - // a stack trace since the stack trace format is now for owner stacks.
531 - console['error'](
620 + console.error(
621 'In HTML, text nodes cannot be a child of <%s>.\n' +
622 'This will cause a hydration error.%s',
623 parentTag,
535 - getCurrentParentStackInDev(),
624 + ancestorDescription,
625 );
626 } else {
538 - // Don't transform into consoleWithStackDev here because we add a manual stack.
539 - // We use the parent stack here instead of the owner stack because the parent
540 - // stack has more useful context for nesting.
541 - // TODO: Format this as a linkified "diff view" with props instead of
542 - // a stack trace since the stack trace format is now for owner stacks.
543 - console['error'](
627 + console.error(
628 'In HTML, whitespace text nodes cannot be a child of <%s>. ' +
629 "Make sure you don't have any extra whitespace between tags on " +
630 'each line of your source code.\n' +
631 'This will cause a hydration error.%s',
632 parentTag,
549 - getCurrentParentStackInDev(),
633 + ancestorDescription,
634 );
635 }
636 return false;
packages/react-dom/src/__tests__/ReactDOMComponent-test.js
+207 -68
@@ -2193,13 +2193,18 @@ describe('ReactDOMComponent', () => {
2193 </div>,
2194 );
2195 });
2196 - }).toErrorDev([
2197 - 'In HTML, <tr> cannot be a child of ' +
2198 - '<div>.\n' +
2199 - 'This will cause a hydration error.' +
2196 + }).toErrorDev(
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' +
2203 '\n in tr (at **)' +
2201 - '\n in div (at **)',
2202 - ]);
2204 + (gate(flags => flags.enableOwnerStacks)
2205 + ? ''
2206 + : '\n in div (at **)'),
2207 + );
2208 });
2209
2210 it('warns on invalid nesting at root', async () => {
@@ -2215,12 +2220,13 @@ describe('ReactDOMComponent', () => {
2220 );
2221 });
2222 }).toErrorDev(
2218 - 'In HTML, <p> cannot be a descendant ' +
2219 - 'of <p>.\n' +
2223 + 'In HTML, <p> cannot be a descendant of <p>.\n' +
2224 'This will cause a hydration error.' +
2225 // There is no outer `p` here because root container is not part of the stack.
2226 '\n in p (at **)' +
2223 - '\n in span (at **)',
2227 + (gate(flags => flags.enableOwnerStacks)
2228 + ? ''
2229 + : '\n in span (at **)'),
2230 );
2231 });
2232
@@ -2248,29 +2254,90 @@ describe('ReactDOMComponent', () => {
2254 await act(() => {
2255 root.render(<Foo />);
2256 });
2251 - }).toErrorDev([
2252 - 'In HTML, <tr> cannot be a child of ' +
2253 - '<table>. Add a <tbody>, <thead> or <tfoot> to your code to match the DOM tree generated ' +
2254 - 'by the browser.\n' +
2255 - 'This will cause a hydration error.' +
2256 - '\n in tr (at **)' +
2257 - '\n in Row (at **)' +
2258 - '\n in table (at **)' +
2259 - '\n in Foo (at **)',
2260 - 'In HTML, text nodes cannot be a ' +
2261 - 'child of <tr>.\n' +
2262 - 'This will cause a hydration error.' +
2263 - '\n in tr (at **)' +
2264 - '\n in Row (at **)' +
2265 - '\n in table (at **)' +
2266 - '\n in Foo (at **)',
2267 - 'In HTML, whitespace text nodes cannot ' +
2268 - "be a child of <table>. Make sure you don't have any extra " +
2269 - 'whitespace between tags on each line of your source code.\n' +
2270 - 'This will cause a hydration error.' +
2271 - '\n in table (at **)' +
2272 - '\n in Foo (at **)',
2273 - ]);
2257 + }).toErrorDev(
2258 + gate(flags => flags.enableOwnerStacks)
2259 + ? [
2260 + 'In HTML, <tr> cannot be a child of ' +
2261 + '<table>. Add a <tbody>, <thead> or <tfoot> to your code to match the DOM tree generated ' +
2262 + 'by the browser.\n' +
2263 + 'This will cause a hydration error.\n' +
2264 + '\n' +
2265 + ' <Foo>\n' +
2266 + '> <table>\n' +
2267 + ' <Row>\n' +
2268 + '> <tr>\n' +
2269 + ' ...\n' +
2270 + '\n in tr (at **)' +
2271 + '\n in Row (at **)',
2272 + '<table> cannot contain a nested <tr>.\nSee this log for the ancestor stack trace.' +
2273 + '\n in table (at **)' +
2274 + '\n in Foo (at **)',
2275 + 'In HTML, text nodes cannot be a ' +
2276 + 'child of <tr>.\n' +
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' +
2285 + '\n in tr (at **)' +
2286 + '\n in Row (at **)',
2287 + 'In HTML, whitespace text nodes cannot ' +
2288 + "be a child of <table>. Make sure you don't have any extra " +
2289 + 'whitespace between tags on each line of your source code.\n' +
2290 + 'This will cause a hydration error.\n' +
2291 + '\n' +
2292 + ' <Foo>\n' +
2293 + '> <table>\n' +
2294 + ' <Row>\n' +
2295 + '> {" "}\n' +
2296 + '\n in table (at **)' +
2297 + '\n in Foo (at **)',
2298 + ]
2299 + : [
2300 + 'In HTML, <tr> cannot be a child of ' +
2301 + '<table>. Add a <tbody>, <thead> or <tfoot> to your code to match the DOM tree generated ' +
2302 + 'by the browser.\n' +
2303 + 'This will cause a hydration error.\n' +
2304 + '\n' +
2305 + ' <Foo>\n' +
2306 + '> <table>\n' +
2307 + ' <Row>\n' +
2308 + '> <tr>\n' +
2309 + ' ...\n' +
2310 + '\n in tr (at **)' +
2311 + '\n in Row (at **)' +
2312 + '\n in table (at **)' +
2313 + '\n in Foo (at **)',
2314 + 'In HTML, text nodes cannot be a ' +
2315 + 'child of <tr>.\n' +
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' +
2324 + '\n in tr (at **)' +
2325 + '\n in Row (at **)' +
2326 + '\n in table (at **)' +
2327 + '\n in Foo (at **)',
2328 + 'In HTML, whitespace text nodes cannot ' +
2329 + "be a child of <table>. Make sure you don't have any extra " +
2330 + 'whitespace between tags on each line of your source code.\n' +
2331 + 'This will cause a hydration error.\n' +
2332 + '\n' +
2333 + ' <Foo>\n' +
2334 + '> <table>\n' +
2335 + ' <Row>\n' +
2336 + '> {" "}\n' +
2337 + '\n in table (at **)' +
2338 + '\n in Foo (at **)',
2339 + ],
2340 + );
2341 });
2342
2343 it('warns nicely for updating table rows to use text', async () => {
@@ -2297,7 +2364,11 @@ describe('ReactDOMComponent', () => {
2364 'In HTML, whitespace text nodes cannot ' +
2365 "be a child of <table>. Make sure you don't have any extra " +
2366 'whitespace between tags on each line of your source code.\n' +
2300 - 'This will cause a hydration error.' +
2367 + 'This will cause a hydration error.\n' +
2368 + '\n' +
2369 + ' <Foo>\n' +
2370 + ' <table>\n' +
2371 + '> {" "}\n' +
2372 '\n in table (at **)' +
2373 '\n in Foo (at **)',
2374 ]);
@@ -2325,12 +2396,21 @@ describe('ReactDOMComponent', () => {
2396 }).toErrorDev([
2397 'In HTML, text nodes cannot be a ' +
2398 'child of <tr>.\n' +
2328 - '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' +
2407 '\n in tr (at **)' +
2408 '\n in Row (at **)' +
2331 - '\n in tbody (at **)' +
2332 - '\n in table (at **)' +
2333 - '\n in Foo (at **)',
2409 + (gate(flags => flags.enableOwnerStacks)
2410 + ? ''
2411 + : '\n in tbody (at **)' +
2412 + '\n in table (at **)' +
2413 + '\n in Foo (at **)'),
2414 ]);
2415 });
2416
@@ -2359,11 +2439,21 @@ describe('ReactDOMComponent', () => {
2439 root.render(<App1 />);
2440 });
2441 }).toErrorDev(
2362 - '\n in tr (at **)' +
2363 - '\n in Row (at **)' +
2364 - '\n in FancyRow (at **)' +
2365 - '\n in table (at **)' +
2366 - '\n in Viz1 (at **)',
2442 + gate(flags => flags.enableOwnerStacks)
2443 + ? [
2444 + '\n in tr (at **)' +
2445 + '\n in Row (at **)' +
2446 + '\n in FancyRow (at **)' +
2447 + '\n in Viz1 (at **)',
2448 + '\n in table (at **)' + '\n in Viz1 (at **)',
2449 + ]
2450 + : [
2451 + '\n in tr (at **)' +
2452 + '\n in Row (at **)' +
2453 + '\n in FancyRow (at **)' +
2454 + '\n in table (at **)' +
2455 + '\n in Viz1 (at **)',
2456 + ],
2457 );
2458 });
2459
@@ -2405,13 +2495,26 @@ describe('ReactDOMComponent', () => {
2495 root.render(<App2 />);
2496 });
2497 }).toErrorDev(
2408 - '\n in tr (at **)' +
2409 - '\n in Row (at **)' +
2410 - '\n in FancyRow (at **)' +
2411 - '\n in table (at **)' +
2412 - '\n in Table (at **)' +
2413 - '\n in FancyTable (at **)' +
2414 - '\n in Viz2 (at **)',
2498 + gate(flags => flags.enableOwnerStacks)
2499 + ? [
2500 + '\n in tr (at **)' +
2501 + '\n in Row (at **)' +
2502 + '\n in FancyRow (at **)' +
2503 + '\n in Viz2 (at **)',
2504 + '\n in table (at **)' +
2505 + '\n in Table (at **)' +
2506 + '\n in FancyTable (at **)' +
2507 + '\n in Viz2 (at **)',
2508 + ]
2509 + : [
2510 + '\n in tr (at **)' +
2511 + '\n in Row (at **)' +
2512 + '\n in FancyRow (at **)' +
2513 + '\n in table (at **)' +
2514 + '\n in Table (at **)' +
2515 + '\n in FancyTable (at **)' +
2516 + '\n in Viz2 (at **)',
2517 + ],
2518 );
2519 });
2520
@@ -2446,12 +2549,23 @@ describe('ReactDOMComponent', () => {
2549 );
2550 });
2551 }).toErrorDev(
2449 - '\n in tr (at **)' +
2450 - '\n in Row (at **)' +
2451 - '\n in FancyRow (at **)' +
2452 - '\n in table (at **)' +
2453 - '\n in Table (at **)' +
2454 - '\n in FancyTable (at **)',
2552 + gate(flags => flags.enableOwnerStacks)
2553 + ? [
2554 + '\n in tr (at **)' +
2555 + '\n in Row (at **)' +
2556 + '\n in FancyRow (at **)',
2557 + '\n in table (at **)' +
2558 + '\n in Table (at **)' +
2559 + '\n in FancyTable (at **)',
2560 + ]
2561 + : [
2562 + '\n in tr (at **)' +
2563 + '\n in Row (at **)' +
2564 + '\n in FancyRow (at **)' +
2565 + '\n in table (at **)' +
2566 + '\n in Table (at **)' +
2567 + '\n in FancyTable (at **)',
2568 + ],
2569 );
2570 });
2571
@@ -2475,10 +2589,19 @@ describe('ReactDOMComponent', () => {
2589 );
2590 });
2591 }).toErrorDev(
2478 - '\n in tr (at **)' +
2479 - '\n in Row (at **)' +
2480 - '\n in FancyRow (at **)' +
2481 - '\n in table (at **)',
2592 + gate(flags => flags.enableOwnerStacks)
2593 + ? [
2594 + '\n in tr (at **)' +
2595 + '\n in Row (at **)' +
2596 + '\n in FancyRow (at **)',
2597 + '\n in table (at **)',
2598 + ]
2599 + : [
2600 + '\n in tr (at **)' +
2601 + '\n in Row (at **)' +
2602 + '\n in FancyRow (at **)' +
2603 + '\n in table (at **)',
2604 + ],
2605 );
2606 });
2607
@@ -2506,10 +2629,19 @@ describe('ReactDOMComponent', () => {
2629 );
2630 });
2631 }).toErrorDev(
2509 - '\n in tr (at **)' +
2510 - '\n in table (at **)' +
2511 - '\n in Table (at **)' +
2512 - '\n in FancyTable (at **)',
2632 + gate(flags => flags.enableOwnerStacks)
2633 + ? [
2634 + '\n in tr (at **)',
2635 + '\n in table (at **)' +
2636 + '\n in Table (at **)' +
2637 + '\n in FancyTable (at **)',
2638 + ]
2639 + : [
2640 + '\n in tr (at **)' +
2641 + '\n in table (at **)' +
2642 + '\n in Table (at **)' +
2643 + '\n in FancyTable (at **)',
2644 + ],
2645 );
2646
2647 class Link extends React.Component {
@@ -2531,11 +2663,18 @@ describe('ReactDOMComponent', () => {
2663 );
2664 });
2665 }).toErrorDev(
2534 - '\n in a (at **)' +
2535 - '\n in Link (at **)' +
2536 - '\n in div (at **)' +
2537 - '\n in a (at **)' +
2538 - '\n in Link (at **)',
2666 + gate(flags => flags.enableOwnerStacks)
2667 + ? [
2668 + '\n in a (at **)' + '\n in Link (at **)',
2669 + '\n in a (at **)' + '\n in Link (at **)',
2670 + ]
2671 + : [
2672 + '\n in a (at **)' +
2673 + '\n in Link (at **)' +
2674 + '\n in div (at **)' +
2675 + '\n in a (at **)' +
2676 + '\n in Link (at **)',
2677 + ],
2678 );
2679 });
2680
packages/react-dom/src/__tests__/ReactDOMForm-test.js
+8 -4
@@ -385,12 +385,16 @@ describe('ReactDOMForm', () => {
385 </form>,
386 );
387 });
388 - }).toErrorDev([
388 + }).toErrorDev(
389 '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' +
395 '\n in form (at **)' +
392 - '\n in form (at **)',
393 - ]);
396 + (gate(flags => flags.enableOwnerStacks) ? '' : '\n in form (at **)'),
397 + );
398
399 await submit(ref.current);
400
packages/react-dom/src/__tests__/ReactDOMOption-test.js
+23 -3
@@ -53,8 +53,15 @@ describe('ReactDOMOption', () => {
53 }).toErrorDev(
54 'In HTML, <div> cannot be a child of <option>.\n' +
55 'This will cause a hydration error.\n' +
56 - ' in div (at **)\n' +
57 - ' in option (at **)',
56 + '\n' +
57 + '> <option value="12">\n' +
58 + '> <div>\n' +
59 + ' ...\n' +
60 + '\n' +
61 + ' in div (at **)' +
62 + (gate(flags => flags.enableOwnerStacks)
63 + ? ''
64 + : '\n in option (at **)'),
65 );
66 expect(container.firstChild.innerHTML).toBe('1 <div></div> 2');
67 await renderIntoDocument(el);
@@ -266,7 +273,20 @@ describe('ReactDOMOption', () => {
273 onRecoverableError: () => {},
274 });
275 });
269 - }).toErrorDev(['In HTML, <div> cannot be a child of <option>']);
276 + }).toErrorDev(
277 + 'In HTML, <div> cannot be a child of <option>.\n' +
278 + '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' +
285 + ' in div (at **)' +
286 + (gate(flags => flags.enableOwnerStacks)
287 + ? ''
288 + : '\n in option (at **)'),
289 + );
290 option = container.firstChild.firstChild;
291
292 expect(option.textContent).toBe('BarFooBaz');
packages/react-dom/src/__tests__/validateDOMNesting-test.js
+54 -17
@@ -93,14 +93,34 @@ describe('validateDOMNesting', () => {
93 );
94 expectWarnings(
95 ['div', 'ul', 'li', 'div', 'li'],
96 - [
97 - 'In HTML, <li> cannot be a descendant of <li>.\n' +
98 - 'This will cause a hydration error.\n' +
99 - ' in li (at **)\n' +
100 - ' in div (at **)\n' +
101 - ' in li (at **)\n' +
102 - ' in ul (at **)',
103 - ],
96 + gate(flags => flags.enableOwnerStacks)
97 + ? [
98 + 'In HTML, <li> cannot be a descendant of <li>.\n' +
99 + 'This will cause a hydration error.\n' +
100 + '\n' +
101 + ' <ul>\n' +
102 + '> <li>\n' +
103 + ' <div>\n' +
104 + '> <li>\n' +
105 + '\n' +
106 + ' in li (at **)',
107 + '<li> cannot contain a nested <li>.\nSee this log for the ancestor stack trace.\n' +
108 + ' in li (at **)',
109 + ]
110 + : [
111 + 'In HTML, <li> cannot be a descendant of <li>.\n' +
112 + 'This will cause a hydration error.\n' +
113 + '\n' +
114 + ' <ul>\n' +
115 + '> <li>\n' +
116 + ' <div>\n' +
117 + '> <li>\n' +
118 + '\n' +
119 + ' in li (at **)\n' +
120 + ' in div (at **)\n' +
121 + ' in li (at **)\n' +
122 + ' in ul (at **)',
123 + ],
124 );
125 expectWarnings(
126 ['div', 'html'],
@@ -120,15 +140,32 @@ describe('validateDOMNesting', () => {
140 );
141 expectWarnings(
142 ['svg', 'foreignObject', 'body', 'p'],
123 - [
124 - // TODO, this should say "In SVG",
125 - 'In HTML, <body> cannot be a child of <foreignObject>.\n' +
126 - 'This will cause a hydration error.\n' +
127 - ' in body (at **)\n' +
128 - ' in foreignObject (at **)',
129 - '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' +
130 - ' in body (at **)',
131 - ],
143 + gate(flags => flags.enableOwnerStacks)
144 + ? [
145 + // TODO, this should say "In SVG",
146 + 'In HTML, <body> cannot be a child of <foreignObject>.\n' +
147 + 'This will cause a hydration error.\n' +
148 + '\n' +
149 + '> <foreignObject>\n' +
150 + '> <body>\n' +
151 + '\n' +
152 + ' in body (at **)',
153 + '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' +
154 + ' in body (at **)',
155 + ]
156 + : [
157 + // TODO, this should say "In SVG",
158 + 'In HTML, <body> cannot be a child of <foreignObject>.\n' +
159 + 'This will cause a hydration error.\n' +
160 + '\n' +
161 + '> <foreignObject>\n' +
162 + '> <body>\n' +
163 + '\n' +
164 + ' in body (at **)\n' +
165 + ' in foreignObject (at **)',
166 + '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' +
167 + ' in body (at **)',
168 + ],
169 );
170 });
171 });
packages/react-reconciler/src/ReactChildFiber.js
+33
@@ -514,6 +514,11 @@ function createChildReconciler(
514 const created = createFiberFromText(textContent, returnFiber.mode, lanes);
515 created.return = returnFiber;
516 if (__DEV__) {
517 + // We treat the parent as the owner for stack purposes.
518 + created._debugOwner = returnFiber;
519 + if (enableOwnerStacks) {
520 + created._debugTask = returnFiber._debugTask;
521 + }
522 created._debugInfo = currentDebugInfo;
523 }
524 return created;
@@ -630,6 +635,11 @@ function createChildReconciler(
635 );
636 created.return = returnFiber;
637 if (__DEV__) {
638 + // We treat the parent as the owner for stack purposes.
639 + created._debugOwner = returnFiber;
640 + if (enableOwnerStacks) {
641 + created._debugTask = returnFiber._debugTask;
642 + }
643 created._debugInfo = currentDebugInfo;
644 }
645 return created;
@@ -665,6 +675,11 @@ function createChildReconciler(
675 );
676 created.return = returnFiber;
677 if (__DEV__) {
678 + // We treat the parent as the owner for stack purposes.
679 + created._debugOwner = returnFiber;
680 + if (enableOwnerStacks) {
681 + created._debugTask = returnFiber._debugTask;
682 + }
683 created._debugInfo = currentDebugInfo;
684 }
685 return created;
@@ -729,6 +744,11 @@ function createChildReconciler(
744 );
745 created.return = returnFiber;
746 if (__DEV__) {
747 + // We treat the parent as the owner for stack purposes.
748 + created._debugOwner = returnFiber;
749 + if (enableOwnerStacks) {
750 + created._debugTask = returnFiber._debugTask;
751 + }
752 const prevDebugInfo = pushDebugInfo(newChild._debugInfo);
753 created._debugInfo = currentDebugInfo;
754 currentDebugInfo = prevDebugInfo;
@@ -1610,6 +1630,14 @@ function createChildReconciler(
1630 deleteRemainingChildren(returnFiber, currentFirstChild);
1631 const created = createFiberFromText(textContent, returnFiber.mode, lanes);
1632 created.return = returnFiber;
1633 + if (__DEV__) {
1634 + // We treat the parent as the owner for stack purposes.
1635 + created._debugOwner = returnFiber;
1636 + if (enableOwnerStacks) {
1637 + created._debugTask = returnFiber._debugTask;
1638 + }
1639 + created._debugInfo = currentDebugInfo;
1640 + }
1641 return created;
1642 }
1643
@@ -1683,6 +1711,11 @@ function createChildReconciler(
1711 );
1712 created.return = returnFiber;
1713 if (__DEV__) {
1714 + // We treat the parent as the owner for stack purposes.
1715 + created._debugOwner = returnFiber;
1716 + if (enableOwnerStacks) {
1717 + created._debugTask = returnFiber._debugTask;
1718 + }
1719 created._debugInfo = currentDebugInfo;
1720 }
1721 validateFragmentProps(element, created, returnFiber);
packages/react-reconciler/src/ReactCurrentFiber.js
-11
@@ -33,17 +33,6 @@ export function getCurrentFiberOwnerNameInDevOrNull(): string | null {
33 return null;
34 }
35
36 -export function getCurrentParentStackInDev(): string {
37 - // This is used to get the parent stack even with owner stacks turned on.
38 - if (__DEV__) {
39 - if (current === null) {
40 - return '';
41 - }
42 - return getStackByFiberInDevAndProd(current);
43 - }
44 - return '';
45 -}
46 -
36 function getCurrentFiberStackInDev(stack: null | Error): string {
37 if (__DEV__) {
38 if (current === null) {
packages/react-reconciler/src/ReactFiberHydrationDiffs.js
+15 -7
@@ -101,7 +101,7 @@ function describeFiberType(fiber: Fiber): null | string {
101 }
102 }
103
104 -const needsEscaping = /["'&<>\n\t]/;
104 +const needsEscaping = /["'&<>\n\t]|^\s|\s$/;
105
106 function describeTextNode(content: string, maxLength: number): string {
107 if (needsEscaping.test(content)) {
@@ -498,9 +498,15 @@ function describeElementDiff(
498 typeof clientChildren === 'number' ||
499 typeof clientChildren === 'bigint'
500 ) {
501 - // The client has children but it's not considered a difference from the server.
502 - // $FlowFixMe[unsafe-addition]
503 - content += describeTextDiff('' + clientChildren, undefined, indent + 1);
501 + if (serverChildren == null) {
502 + // This is a new string child.
503 + // $FlowFixMe[unsafe-addition]
504 + content += describeTextDiff('' + clientChildren, null, indent + 1);
505 + } else {
506 + // The client has children but it's not considered a difference from the server.
507 + // $FlowFixMe[unsafe-addition]
508 + content += describeTextDiff('' + clientChildren, undefined, indent + 1);
509 + }
510 }
511 return content;
512 }
@@ -554,6 +560,7 @@ function describeNode(node: HydrationDiffNode, indent: number): string {
560 if (node.fiber.tag === HostText) {
561 // Text Node
562 selfContent = describeTextDiff(clientProps, node.serverProps, indent);
563 + indent++;
564 } else {
565 const type = describeFiberType(node.fiber);
566 if (type !== null) {
@@ -564,9 +571,7 @@ function describeNode(node: HydrationDiffNode, indent: number): string {
571 indent++;
572 } else if (node.serverProps === null) {
573 selfContent = describeExpandedElement(type, clientProps, added(indent));
567 - // If this was an insertion we won't step down further. Any tail
568 - // are considered siblings so we don't indent.
569 - // TODO: Model this a little better.
574 + indent++;
575 } else if (typeof node.serverProps === 'string') {
576 if (__DEV__) {
577 console.error(
@@ -611,6 +616,9 @@ function describeNode(node: HydrationDiffNode, indent: number): string {
616
617 // Deleted tail nodes
618 const serverTail = node.serverTail;
619 + if (node.serverProps === null) {
620 + indent--;
621 + }
622 for (let i = 0; i < serverTail.length; i++) {
623 const tailNode = serverTail[i];
624 if (typeof tailNode === 'string') {