@samitouri / QOS-React-1 / commits / 118ad2afa7

Validate DOM nesting for hydration before the hydration warns / errors (#28434)

If there's invalid dom nesting, there will be mismatches following but the nesting is the most important cause of the problem. Previously we would include the DOM nesting when rerendering thanks to the new model of throw and recovery. However, the log would come during the recovery phase which is after we've already logged that there was a hydration mismatch. People would consistently miss this log. Which is fair because you should always look at the first log first as the most probable cause. This ensures that we log in the hydration phase if there's a dom nesting issue. This assumes that the consequence of nesting will appear such that the won't have a mismatch before this. That's typically the case because the node will move up and to be a later sibling. So as long as that happens and we keep hydrating depth first, it should hold true. There might be an issue if there's a suspense boundary between the nodes we'll find discover the new child in the outer path since suspense boundaries as breadth first. Before: <img width="996" alt="Screenshot 2024-02-23 at 7 34 01 PM" src="https://github.com/facebook/react/assets/63648/af70cf7f-898b-477f-be39-13b01cfe585f"> After: <img width="853" alt="Screenshot 2024-02-23 at 7 22 24 PM" src="https://github.com/facebook/react/assets/63648/896c6348-1620-4f99-881d-b6069263925e"> Cameo: RSC stacks.

Sebastian Markbåge committed Feb 24, 2024 at 00:45 UTC 118ad2afa75a44fe3715ad7fb0023c408125bef7
11 files changed +181 -59
packages/react-dom-bindings/src/client/ReactFiberConfigDOM.js
+27
@@ -1355,6 +1355,19 @@ export function getFirstHydratableChildWithinSuspenseInstance(
1355 return getNextHydratable(parentInstance.nextSibling);
1356 }
1357
1358 +export function validateHydratableInstance(
1359 + type: string,
1360 + props: Props,
1361 + hostContext: HostContext,
1362 +): boolean {
1363 + if (__DEV__) {
1364 + // TODO: take namespace into account when validating.
1365 + const hostContextDev: HostContextDev = (hostContext: any);
1366 + return validateDOMNesting(type, hostContextDev.ancestorInfo);
1367 + }
1368 + return true;
1369 +}
1370 +
1371 export function hydrateInstance(
1372 instance: Instance,
1373 type: string,
@@ -1383,6 +1396,20 @@ export function hydrateInstance(
1396 );
1397 }
1398
1399 +export function validateHydratableTextInstance(
1400 + text: string,
1401 + hostContext: HostContext,
1402 +): boolean {
1403 + if (__DEV__) {
1404 + const hostContextDev = ((hostContext: any): HostContextDev);
1405 + const ancestor = hostContextDev.ancestorInfo.current;
1406 + if (ancestor != null) {
1407 + return validateTextNesting(text, ancestor.tag);
1408 + }
1409 + }
1410 + return true;
1411 +}
1412 +
1413 export function hydrateTextInstance(
1414 textInstance: TextInstance,
1415 text: string,
packages/react-dom-bindings/src/client/validateDOMNesting.js
+22 -11
@@ -441,7 +441,7 @@ const didWarn: {[string]: boolean} = {};
441 function validateDOMNesting(
442 childTag: string,
443 ancestorInfo: AncestorInfoDev,
444 -): void {
444 +): boolean {
445 if (__DEV__) {
446 ancestorInfo = ancestorInfo || emptyAncestorInfoDev;
447 const parentInfo = ancestorInfo.current;
@@ -455,7 +455,7 @@ function validateDOMNesting(
455 : findInvalidAncestorForTag(childTag, ancestorInfo);
456 const invalidParentOrAncestor = invalidParent || invalidAncestor;
457 if (!invalidParentOrAncestor) {
458 - return;
458 + return true;
459 }
460
461 const ancestorTag = invalidParentOrAncestor.tag;
@@ -464,7 +464,7 @@ function validateDOMNesting(
464 // eslint-disable-next-line react-internal/safe-string-coercion
465 String(!!invalidParent) + '|' + childTag + '|' + ancestorTag;
466 if (didWarn[warnKey]) {
467 - return;
467 + return false;
468 }
469 didWarn[warnKey] = true;
470
@@ -477,45 +477,56 @@ function validateDOMNesting(
477 'the browser.';
478 }
479 console.error(
480 - '%s cannot appear as a child of <%s>.%s',
480 + 'In HTML, %s cannot be a child of <%s>.%s\n' +
481 + 'This will cause a hydration error.',
482 tagDisplayName,
483 ancestorTag,
484 info,
485 );
486 } else {
487 console.error(
487 - '%s cannot appear as a descendant of ' + '<%s>.',
488 + 'In HTML, %s cannot be a descendant of <%s>.\n' +
489 + 'This will cause a hydration error.',
490 tagDisplayName,
491 ancestorTag,
492 );
493 }
494 + return false;
495 }
496 + return true;
497 }
498
495 -function validateTextNesting(childText: string, parentTag: string): void {
499 +function validateTextNesting(childText: string, parentTag: string): boolean {
500 if (__DEV__) {
501 if (isTagValidWithParent('#text', parentTag)) {
498 - return;
502 + return true;
503 }
504
505 // eslint-disable-next-line react-internal/safe-string-coercion
506 const warnKey = '#text|' + parentTag;
507 if (didWarn[warnKey]) {
504 - return;
508 + return false;
509 }
510 didWarn[warnKey] = true;
511
512 if (/\S/.test(childText)) {
509 - console.error('Text nodes cannot appear as a child of <%s>.', parentTag);
513 + console.error(
514 + 'In HTML, text nodes cannot be a child of <%s>.\n' +
515 + 'This will cause a hydration error.',
516 + parentTag,
517 + );
518 } else {
519 console.error(
512 - 'Whitespace text nodes cannot appear as a child of <%s>. ' +
520 + 'In HTML, whitespace text nodes cannot be a child of <%s>. ' +
521 "Make sure you don't have any extra whitespace between tags on " +
514 - 'each line of your source code.',
522 + 'each line of your source code.\n' +
523 + 'This will cause a hydration error.',
524 parentTag,
525 );
526 }
527 + return false;
528 }
529 + return true;
530 }
531
532 export {updatedAncestorInfoDev, validateDOMNesting, validateTextNesting};
packages/react-dom/src/__tests__/ReactDOMComponent-test.js
+23 -16
@@ -2188,8 +2188,9 @@ describe('ReactDOMComponent', () => {
2188 );
2189 });
2190 }).toErrorDev([
2191 - 'Warning: <tr> cannot appear as a child of ' +
2192 - '<div>.' +
2191 + 'Warning: In HTML, <tr> cannot be a child of ' +
2192 + '<div>.\n' +
2193 + 'This will cause a hydration error.' +
2194 '\n in tr (at **)' +
2195 '\n in div (at **)',
2196 ]);
@@ -2208,8 +2209,9 @@ describe('ReactDOMComponent', () => {
2209 );
2210 });
2211 }).toErrorDev(
2211 - 'Warning: <p> cannot appear as a descendant ' +
2212 - 'of <p>.' +
2212 + 'Warning: In HTML, <p> cannot be a descendant ' +
2213 + 'of <p>.\n' +
2214 + 'This will cause a hydration error.' +
2215 // There is no outer `p` here because root container is not part of the stack.
2216 '\n in p (at **)' +
2217 '\n in span (at **)',
@@ -2241,22 +2243,25 @@ describe('ReactDOMComponent', () => {
2243 root.render(<Foo />);
2244 });
2245 }).toErrorDev([
2244 - 'Warning: <tr> cannot appear as a child of ' +
2246 + 'Warning: In HTML, <tr> cannot be a child of ' +
2247 '<table>. Add a <tbody>, <thead> or <tfoot> to your code to match the DOM tree generated ' +
2246 - 'by the browser.' +
2248 + 'by the browser.\n' +
2249 + 'This will cause a hydration error.' +
2250 '\n in tr (at **)' +
2251 '\n in Row (at **)' +
2252 '\n in table (at **)' +
2253 '\n in Foo (at **)',
2251 - 'Warning: Text nodes cannot appear as a ' +
2252 - 'child of <tr>.' +
2254 + 'Warning: In HTML, text nodes cannot be a ' +
2255 + 'child of <tr>.\n' +
2256 + 'This will cause a hydration error.' +
2257 '\n in tr (at **)' +
2258 '\n in Row (at **)' +
2259 '\n in table (at **)' +
2260 '\n in Foo (at **)',
2257 - 'Warning: Whitespace text nodes cannot ' +
2258 - "appear as a child of <table>. Make sure you don't have any extra " +
2259 - 'whitespace between tags on each line of your source code.' +
2261 + 'Warning: In HTML, whitespace text nodes cannot ' +
2262 + "be a child of <table>. Make sure you don't have any extra " +
2263 + 'whitespace between tags on each line of your source code.\n' +
2264 + 'This will cause a hydration error.' +
2265 '\n in table (at **)' +
2266 '\n in Foo (at **)',
2267 ]);
@@ -2283,9 +2288,10 @@ describe('ReactDOMComponent', () => {
2288 root.render(<Foo> </Foo>);
2289 });
2290 }).toErrorDev([
2286 - 'Warning: Whitespace text nodes cannot ' +
2287 - "appear as a child of <table>. Make sure you don't have any extra " +
2288 - 'whitespace between tags on each line of your source code.' +
2291 + 'Warning: In HTML, whitespace text nodes cannot ' +
2292 + "be a child of <table>. Make sure you don't have any extra " +
2293 + 'whitespace between tags on each line of your source code.\n' +
2294 + 'This will cause a hydration error.' +
2295 '\n in table (at **)' +
2296 '\n in Foo (at **)',
2297 ]);
@@ -2311,8 +2317,9 @@ describe('ReactDOMComponent', () => {
2317 );
2318 });
2319 }).toErrorDev([
2314 - 'Warning: Text nodes cannot appear as a ' +
2315 - 'child of <tr>.' +
2320 + 'Warning: In HTML, text nodes cannot be a ' +
2321 + 'child of <tr>.\n' +
2322 + 'This will cause a hydration error.' +
2323 '\n in tr (at **)' +
2324 '\n in Row (at **)' +
2325 '\n in tbody (at **)' +
packages/react-dom/src/__tests__/ReactDOMFloat-test.js
+10 -10
@@ -523,7 +523,7 @@ describe('ReactDOMFloat', () => {
523 }).toErrorDev(
524 [
525 'Cannot render <noscript> outside the main document. Try moving it into the root <head> tag.',
526 - 'Warning: <noscript> cannot appear as a child of <#document>.',
526 + 'Warning: In HTML, <noscript> cannot be a child of <#document>.',
527 ],
528 {withoutStack: 1},
529 );
@@ -538,7 +538,7 @@ describe('ReactDOMFloat', () => {
538 await waitForAll([]);
539 }).toErrorDev([
540 'Cannot render <template> outside the main document. Try moving it into the root <head> tag.',
541 - 'Warning: <template> cannot appear as a child of <html>.',
541 + 'Warning: In HTML, <template> cannot be a child of <html>.',
542 ]);
543
544 await expect(async () => {
@@ -551,7 +551,7 @@ describe('ReactDOMFloat', () => {
551 await waitForAll([]);
552 }).toErrorDev([
553 'Cannot render a <style> outside the main document without knowing its precedence and a unique href key. React can hoist and deduplicate <style> tags if you provide a `precedence` prop along with an `href` prop that does not conflic with the `href` values used in any other hoisted <style> or <link rel="stylesheet" ...> tags. Note that hoisting <style> tags is considered an advanced feature that most will not use directly. Consider moving the <style> tag to the <head> or consider adding a `precedence="default"` and `href="some unique resource identifier"`, or move the <style> to the <style> tag.',
554 - 'Warning: <style> cannot appear as a child of <html>.',
554 + 'Warning: In HTML, <style> cannot be a child of <html>.',
555 ]);
556
557 await expect(async () => {
@@ -574,7 +574,7 @@ describe('ReactDOMFloat', () => {
574 }).toErrorDev(
575 [
576 'Cannot render a <link rel="stylesheet" /> outside the main document without knowing its precedence. Consider adding precedence="default" or moving it into the root <head> tag.',
577 - 'Warning: <link> cannot appear as a child of <#document>.',
577 + 'Warning: In HTML, <link> cannot be a child of <#document>.',
578 ],
579 {withoutStack: 1},
580 );
@@ -591,7 +591,7 @@ describe('ReactDOMFloat', () => {
591 await waitForAll([]);
592 }).toErrorDev([
593 'Cannot render a sync or defer <script> outside the main document without knowing its order. Try adding async="" or moving it into the root <head> tag.',
594 - 'Warning: <script> cannot appear as a child of <html>.',
594 + 'Warning: In HTML, <script> cannot be a child of <html>.',
595 ]);
596
597 await expect(async () => {
@@ -2552,11 +2552,11 @@ body {
2552 'Cannot render a <style> outside the main document if it has an `itemProp` prop. `itemProp` suggests the tag belongs to an `itemScope` which can appear anywhere in the DOM. If you were intending for React to hoist this <style> remove the `itemProp` prop. Otherwise, try moving this tag into the <head> or <body> of the Document.',
2553 'Cannot render a <link> outside the main document if it has an `itemProp` prop. `itemProp` suggests the tag belongs to an `itemScope` which can appear anywhere in the DOM. If you were intending for React to hoist this <link> remove the `itemProp` prop. Otherwise, try moving this tag into the <head> or <body> of the Document.',
2554 'Cannot render a <script> outside the main document if it has an `itemProp` prop. `itemProp` suggests the tag belongs to an `itemScope` which can appear anywhere in the DOM. If you were intending for React to hoist this <script> remove the `itemProp` prop. Otherwise, try moving this tag into the <head> or <body> of the Document.',
2555 - '<meta> cannot appear as a child of <html>',
2556 - '<title> cannot appear as a child of <html>',
2557 - '<style> cannot appear as a child of <html>',
2558 - '<link> cannot appear as a child of <html>',
2559 - '<script> cannot appear as a child of <html>',
2555 + 'In HTML, <meta> cannot be a child of <html>',
2556 + 'In HTML, <title> cannot be a child of <html>',
2557 + 'In HTML, <style> cannot be a child of <html>',
2558 + 'In HTML, <link> cannot be a child of <html>',
2559 + 'In HTML, <script> cannot be a child of <html>',
2560 ]);
2561 });
2562
packages/react-dom/src/__tests__/ReactDOMForm-test.js
+2 -1
@@ -381,7 +381,8 @@ describe('ReactDOMForm', () => {
381 );
382 });
383 }).toErrorDev([
384 - 'Warning: <form> cannot appear as a descendant of <form>.' +
384 + 'Warning: In HTML, <form> cannot be a descendant of <form>.\n' +
385 + 'This will cause a hydration error.' +
386 '\n in form (at **)' +
387 '\n in form (at **)',
388 ]);
packages/react-dom/src/__tests__/ReactDOMOption-test.js
+3 -2
@@ -46,7 +46,8 @@ describe('ReactDOMOption', () => {
46 expect(() => {
47 node = ReactTestUtils.renderIntoDocument(el);
48 }).toErrorDev(
49 - '<div> cannot appear as a child of <option>.\n' +
49 + 'In HTML, <div> cannot be a child of <option>.\n' +
50 + 'This will cause a hydration error.\n' +
51 ' in div (at **)\n' +
52 ' in option (at **)',
53 );
@@ -263,7 +264,7 @@ describe('ReactDOMOption', () => {
264 [
265 'Warning: Text content did not match. Server: "FooBaz" Client: "Foo"',
266 'Warning: An error occurred during hydration. The server HTML was replaced with client content in <div>',
266 - 'Warning: <div> cannot appear as a child of <option>',
267 + 'Warning: In HTML, <div> cannot be a child of <option>',
268 ],
269 {withoutStack: 1},
270 );
packages/react-dom/src/__tests__/validateDOMNesting-test.js
+29 -8
@@ -60,31 +60,42 @@ describe('validateDOMNesting', () => {
60 it('prevents problematic nestings', () => {
61 expectWarnings(
62 ['a', 'a'],
63 - ['<a> cannot appear as a descendant of <a>.\n' + ' in a (at **)'],
63 + [
64 + 'In HTML, <a> cannot be a descendant of <a>.\n' +
65 + 'This will cause a hydration error.\n' +
66 + ' in a (at **)',
67 + ],
68 );
69 expectWarnings(
70 ['form', 'form'],
71 [
68 - '<form> cannot appear as a descendant of <form>.\n' +
72 + 'In HTML, <form> cannot be a descendant of <form>.\n' +
73 + 'This will cause a hydration error.\n' +
74 ' in form (at **)',
75 ],
76 );
77 expectWarnings(
78 ['p', 'p'],
74 - ['<p> cannot appear as a descendant of <p>.\n' + ' in p (at **)'],
79 + [
80 + 'In HTML, <p> cannot be a descendant of <p>.\n' +
81 + 'This will cause a hydration error.\n' +
82 + ' in p (at **)',
83 + ],
84 );
85 expectWarnings(
86 ['table', 'tr'],
87 [
79 - '<tr> cannot appear as a child of <table>. ' +
88 + 'In HTML, <tr> cannot be a child of <table>. ' +
89 'Add a <tbody>, <thead> or <tfoot> to your code to match the DOM tree generated by the browser.\n' +
90 + 'This will cause a hydration error.\n' +
91 ' in tr (at **)',
92 ],
93 );
94 expectWarnings(
95 ['div', 'ul', 'li', 'div', 'li'],
96 [
87 - '<li> cannot appear as a descendant of <li>.\n' +
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' +
@@ -93,16 +104,26 @@ describe('validateDOMNesting', () => {
104 );
105 expectWarnings(
106 ['div', 'html'],
96 - ['<html> cannot appear as a child of <div>.\n' + ' in html (at **)'],
107 + [
108 + 'In HTML, <html> cannot be a child of <div>.\n' +
109 + 'This will cause a hydration error.\n' +
110 + ' in html (at **)',
111 + ],
112 );
113 expectWarnings(
114 ['body', 'body'],
100 - ['<body> cannot appear as a child of <body>.\n' + ' in body (at **)'],
115 + [
116 + 'In HTML, <body> cannot be a child of <body>.\n' +
117 + 'This will cause a hydration error.\n' +
118 + ' in body (at **)',
119 + ],
120 );
121 expectWarnings(
122 ['svg', 'foreignObject', 'body', 'p'],
123 [
105 - '<body> cannot appear as a child of <foreignObject>.\n' +
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 'Warning: 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' +
packages/react-reconciler/src/ReactFiberBeginWork.js
+2 -2
@@ -1512,12 +1512,12 @@ function updateHostComponent(
1512 workInProgress: Fiber,
1513 renderLanes: Lanes,
1514 ) {
1515 - pushHostContext(workInProgress);
1516 -
1515 if (current === null) {
1516 tryToClaimNextHydratableInstance(workInProgress);
1517 }
1518
1519 + pushHostContext(workInProgress);
1520 +
1521 const type = workInProgress.type;
1522 const nextProps = workInProgress.pendingProps;
1523 const prevProps = current !== null ? current.memoizedProps : null;
packages/react-reconciler/src/ReactFiberConfigWithNoHydration.js
+2
@@ -59,3 +59,5 @@ export const didNotFindHydratableInstance = shim;
59 export const didNotFindHydratableTextInstance = shim;
60 export const didNotFindHydratableSuspenseInstance = shim;
61 export const errorHydratingContainer = shim;
62 +export const validateHydratableInstance = shim;
63 +export const validateHydratableTextInstance = shim;
packages/react-reconciler/src/ReactFiberHydrationContext.js
+58 -9
@@ -75,6 +75,8 @@ import {
75 canHydrateFormStateMarker,
76 isFormStateMarkerMatching,
77 isHydratableText,
78 + validateHydratableInstance,
79 + validateHydratableTextInstance,
80 } from './ReactFiberConfig';
81 import {OffscreenLane} from './ReactFiberLane';
82 import {
@@ -202,7 +204,6 @@ function deleteHydratableInstance(
204 returnFiber: Fiber,
205 instance: HydratableInstance,
206 ) {
205 - warnUnhydratedInstance(returnFiber, instance);
207 const childToDelete = createFiberFromHostInstanceForDeletion();
208 childToDelete.stateNode = instance;
209 childToDelete.return = returnFiber;
@@ -216,7 +217,7 @@ function deleteHydratableInstance(
217 }
218 }
219
219 -function warnNonhydratedInstance(returnFiber: Fiber, fiber: Fiber) {
220 +function warnNonHydratedInstance(returnFiber: Fiber, fiber: Fiber) {
221 if (__DEV__) {
222 if (didSuspendOrErrorDEV) {
223 // Inside a boundary that already suspended. We're currently rendering the
@@ -339,7 +340,6 @@ function warnNonhydratedInstance(returnFiber: Fiber, fiber: Fiber) {
340 }
341 function insertNonHydratedInstance(returnFiber: Fiber, fiber: Fiber) {
342 fiber.flags = (fiber.flags & ~Hydrating) | Placement;
342 - warnNonhydratedInstance(returnFiber, fiber);
343 }
344
345 function tryHydrateInstance(fiber: Fiber, nextInstance: any) {
@@ -446,15 +446,29 @@ function tryToClaimNextHydratableInstance(fiber: Fiber): void {
446 if (!isHydrating) {
447 return;
448 }
449 +
450 + // Validate that this is ok to render here before any mismatches.
451 + const currentHostContext = getHostContext();
452 + const shouldKeepWarning = validateHydratableInstance(
453 + fiber.type,
454 + fiber.pendingProps,
455 + currentHostContext,
456 + );
457 +
458 const initialInstance = nextHydratableInstance;
459 const nextInstance = nextHydratableInstance;
460 if (!nextInstance) {
461 if (shouldClientRenderOnMismatch(fiber)) {
453 - warnNonhydratedInstance((hydrationParentFiber: any), fiber);
462 + if (shouldKeepWarning) {
463 + warnNonHydratedInstance((hydrationParentFiber: any), fiber);
464 + }
465 throwOnHydrationMismatch(fiber);
466 }
467 // Nothing to hydrate. Make it an insertion.
468 insertNonHydratedInstance((hydrationParentFiber: any), fiber);
469 + if (shouldKeepWarning) {
470 + warnNonHydratedInstance((hydrationParentFiber: any), fiber);
471 + }
472 isHydrating = false;
473 hydrationParentFiber = fiber;
474 nextHydratableInstance = initialInstance;
@@ -463,7 +477,9 @@ function tryToClaimNextHydratableInstance(fiber: Fiber): void {
477 const firstAttemptedInstance = nextInstance;
478 if (!tryHydrateInstance(fiber, nextInstance)) {
479 if (shouldClientRenderOnMismatch(fiber)) {
466 - warnNonhydratedInstance((hydrationParentFiber: any), fiber);
480 + if (shouldKeepWarning) {
481 + warnNonHydratedInstance((hydrationParentFiber: any), fiber);
482 + }
483 throwOnHydrationMismatch(fiber);
484 }
485 // If we can't hydrate this instance let's try the next one.
@@ -477,6 +493,9 @@ function tryToClaimNextHydratableInstance(fiber: Fiber): void {
493 ) {
494 // Nothing to hydrate. Make it an insertion.
495 insertNonHydratedInstance((hydrationParentFiber: any), fiber);
496 + if (shouldKeepWarning) {
497 + warnNonHydratedInstance((hydrationParentFiber: any), fiber);
498 + }
499 isHydrating = false;
500 hydrationParentFiber = fiber;
501 nextHydratableInstance = initialInstance;
@@ -486,6 +505,9 @@ function tryToClaimNextHydratableInstance(fiber: Fiber): void {
505 // superfluous and we'll delete it. Since we can't eagerly delete it
506 // we'll have to schedule a deletion. To do that, this node needs a dummy
507 // fiber associated with it.
508 + if (shouldKeepWarning) {
509 + warnUnhydratedInstance(prevHydrationParentFiber, firstAttemptedInstance);
510 + }
511 deleteHydratableInstance(prevHydrationParentFiber, firstAttemptedInstance);
512 }
513 }
@@ -497,17 +519,32 @@ function tryToClaimNextHydratableTextInstance(fiber: Fiber): void {
519 const text = fiber.pendingProps;
520 const isHydratable = isHydratableText(text);
521
522 + let shouldKeepWarning = true;
523 + if (isHydratable) {
524 + // Validate that this is ok to render here before any mismatches.
525 + const currentHostContext = getHostContext();
526 + shouldKeepWarning = validateHydratableTextInstance(
527 + text,
528 + currentHostContext,
529 + );
530 + }
531 +
532 const initialInstance = nextHydratableInstance;
533 const nextInstance = nextHydratableInstance;
534 if (!nextInstance || !isHydratable) {
535 // We exclude non hydrabable text because we know there are no matching hydratables.
536 // We either throw or insert depending on the render mode.
537 if (shouldClientRenderOnMismatch(fiber)) {
506 - warnNonhydratedInstance((hydrationParentFiber: any), fiber);
538 + if (shouldKeepWarning) {
539 + warnNonHydratedInstance((hydrationParentFiber: any), fiber);
540 + }
541 throwOnHydrationMismatch(fiber);
542 }
543 // Nothing to hydrate. Make it an insertion.
544 insertNonHydratedInstance((hydrationParentFiber: any), fiber);
545 + if (shouldKeepWarning) {
546 + warnNonHydratedInstance((hydrationParentFiber: any), fiber);
547 + }
548 isHydrating = false;
549 hydrationParentFiber = fiber;
550 nextHydratableInstance = initialInstance;
@@ -516,7 +553,9 @@ function tryToClaimNextHydratableTextInstance(fiber: Fiber): void {
553 const firstAttemptedInstance = nextInstance;
554 if (!tryHydrateText(fiber, nextInstance)) {
555 if (shouldClientRenderOnMismatch(fiber)) {
519 - warnNonhydratedInstance((hydrationParentFiber: any), fiber);
556 + if (shouldKeepWarning) {
557 + warnNonHydratedInstance((hydrationParentFiber: any), fiber);
558 + }
559 throwOnHydrationMismatch(fiber);
560 }
561 // If we can't hydrate this instance let's try the next one.
@@ -531,6 +570,9 @@ function tryToClaimNextHydratableTextInstance(fiber: Fiber): void {
570 ) {
571 // Nothing to hydrate. Make it an insertion.
572 insertNonHydratedInstance((hydrationParentFiber: any), fiber);
573 + if (shouldKeepWarning) {
574 + warnNonHydratedInstance((hydrationParentFiber: any), fiber);
575 + }
576 isHydrating = false;
577 hydrationParentFiber = fiber;
578 nextHydratableInstance = initialInstance;
@@ -540,6 +582,9 @@ function tryToClaimNextHydratableTextInstance(fiber: Fiber): void {
582 // superfluous and we'll delete it. Since we can't eagerly delete it
583 // we'll have to schedule a deletion. To do that, this node needs a dummy
584 // fiber associated with it.
585 + if (shouldKeepWarning) {
586 + warnUnhydratedInstance(prevHydrationParentFiber, firstAttemptedInstance);
587 + }
588 deleteHydratableInstance(prevHydrationParentFiber, firstAttemptedInstance);
589 }
590 }
@@ -552,11 +597,12 @@ function tryToClaimNextHydratableSuspenseInstance(fiber: Fiber): void {
597 const nextInstance = nextHydratableInstance;
598 if (!nextInstance) {
599 if (shouldClientRenderOnMismatch(fiber)) {
555 - warnNonhydratedInstance((hydrationParentFiber: any), fiber);
600 + warnNonHydratedInstance((hydrationParentFiber: any), fiber);
601 throwOnHydrationMismatch(fiber);
602 }
603 // Nothing to hydrate. Make it an insertion.
604 insertNonHydratedInstance((hydrationParentFiber: any), fiber);
605 + warnNonHydratedInstance((hydrationParentFiber: any), fiber);
606 isHydrating = false;
607 hydrationParentFiber = fiber;
608 nextHydratableInstance = initialInstance;
@@ -565,7 +611,7 @@ function tryToClaimNextHydratableSuspenseInstance(fiber: Fiber): void {
611 const firstAttemptedInstance = nextInstance;
612 if (!tryHydrateSuspense(fiber, nextInstance)) {
613 if (shouldClientRenderOnMismatch(fiber)) {
568 - warnNonhydratedInstance((hydrationParentFiber: any), fiber);
614 + warnNonHydratedInstance((hydrationParentFiber: any), fiber);
615 throwOnHydrationMismatch(fiber);
616 }
617 // If we can't hydrate this instance let's try the next one.
@@ -580,6 +626,7 @@ function tryToClaimNextHydratableSuspenseInstance(fiber: Fiber): void {
626 ) {
627 // Nothing to hydrate. Make it an insertion.
628 insertNonHydratedInstance((hydrationParentFiber: any), fiber);
629 + warnNonHydratedInstance((hydrationParentFiber: any), fiber);
630 isHydrating = false;
631 hydrationParentFiber = fiber;
632 nextHydratableInstance = initialInstance;
@@ -589,6 +636,7 @@ function tryToClaimNextHydratableSuspenseInstance(fiber: Fiber): void {
636 // superfluous and we'll delete it. Since we can't eagerly delete it
637 // we'll have to schedule a deletion. To do that, this node needs a dummy
638 // fiber associated with it.
639 + warnUnhydratedInstance(prevHydrationParentFiber, firstAttemptedInstance);
640 deleteHydratableInstance(prevHydrationParentFiber, firstAttemptedInstance);
641 }
642 }
@@ -834,6 +882,7 @@ function popHydrationState(fiber: Fiber): boolean {
882 throwOnHydrationMismatch(fiber);
883 } else {
884 while (nextInstance) {
885 + warnUnhydratedInstance(fiber, nextInstance);
886 deleteHydratableInstance(fiber, nextInstance);
887 nextInstance = getNextHydratableSibling(nextInstance);
888 }
packages/react-reconciler/src/forks/ReactFiberConfig.custom.js
+3
@@ -193,6 +193,9 @@ export const didNotFindHydratableTextInstance =
193 export const didNotFindHydratableSuspenseInstance =
194 $$$config.didNotFindHydratableSuspenseInstance;
195 export const errorHydratingContainer = $$$config.errorHydratingContainer;
196 +export const validateHydratableInstance = $$$config.validateHydratableInstance;
197 +export const validateHydratableTextInstance =
198 + $$$config.validateHydratableTextInstance;
199
200 // -------------------
201 // Resources