From dcdbc5a00f4aa178c9c2875afebdaf8710b68da0 Mon Sep 17 00:00:00 2001 From: Sebastian Markbage Date: Fri, 23 Feb 2024 17:12:40 -0500 Subject: [PATCH 1/5] Validate DOM nesting for hydration before the hydration warns / errors If there's invalid dom nesting, there will be mismatches following but the nesting is the most important cause. We could also silence the mismatch warning (but not thrown error) too. --- .../src/client/ReactFiberConfigDOM.js | 25 +++++++++++++++++++ .../src/ReactFiberConfigWithNoHydration.js | 2 ++ .../src/ReactFiberHydrationContext.js | 13 ++++++++++ .../src/forks/ReactFiberConfig.custom.js | 3 +++ 4 files changed, 43 insertions(+) diff --git a/packages/react-dom-bindings/src/client/ReactFiberConfigDOM.js b/packages/react-dom-bindings/src/client/ReactFiberConfigDOM.js index 281ec1fea0582..150f88d62360b 100644 --- a/packages/react-dom-bindings/src/client/ReactFiberConfigDOM.js +++ b/packages/react-dom-bindings/src/client/ReactFiberConfigDOM.js @@ -1355,6 +1355,18 @@ export function getFirstHydratableChildWithinSuspenseInstance( return getNextHydratable(parentInstance.nextSibling); } +export function validateHydratableInstance( + type: string, + props: Props, + hostContext: HostContext, +): void { + if (__DEV__) { + // TODO: take namespace into account when validating. + const hostContextDev: HostContextDev = (hostContext: any); + validateDOMNesting(type, hostContextDev.ancestorInfo); + } +} + export function hydrateInstance( instance: Instance, type: string, @@ -1383,6 +1395,19 @@ export function hydrateInstance( ); } +export function validateHydratableTextInstance( + text: string, + hostContext: HostContext, +): void { + if (__DEV__) { + const hostContextDev = ((hostContext: any): HostContextDev); + const ancestor = hostContextDev.ancestorInfo.current; + if (ancestor != null) { + validateTextNesting(text, ancestor.tag); + } + } +} + export function hydrateTextInstance( textInstance: TextInstance, text: string, diff --git a/packages/react-reconciler/src/ReactFiberConfigWithNoHydration.js b/packages/react-reconciler/src/ReactFiberConfigWithNoHydration.js index 7e2b181f4a623..6af759793203c 100644 --- a/packages/react-reconciler/src/ReactFiberConfigWithNoHydration.js +++ b/packages/react-reconciler/src/ReactFiberConfigWithNoHydration.js @@ -59,3 +59,5 @@ export const didNotFindHydratableInstance = shim; export const didNotFindHydratableTextInstance = shim; export const didNotFindHydratableSuspenseInstance = shim; export const errorHydratingContainer = shim; +export const validateHydratableInstance = shim; +export const validateHydratableTextInstance = shim; diff --git a/packages/react-reconciler/src/ReactFiberHydrationContext.js b/packages/react-reconciler/src/ReactFiberHydrationContext.js index a0838ea331b3c..eec261244579a 100644 --- a/packages/react-reconciler/src/ReactFiberHydrationContext.js +++ b/packages/react-reconciler/src/ReactFiberHydrationContext.js @@ -75,6 +75,8 @@ import { canHydrateFormStateMarker, isFormStateMarkerMatching, isHydratableText, + validateHydratableInstance, + validateHydratableTextInstance } from './ReactFiberConfig'; import {OffscreenLane} from './ReactFiberLane'; import { @@ -446,6 +448,11 @@ function tryToClaimNextHydratableInstance(fiber: Fiber): void { if (!isHydrating) { return; } + + // Validate that this is ok to render here before any mismatches. + const currentHostContext = getHostContext(); + validateHydratableInstance(fiber.type, fiber.pendingProps, currentHostContext); + const initialInstance = nextHydratableInstance; const nextInstance = nextHydratableInstance; if (!nextInstance) { @@ -497,6 +504,12 @@ function tryToClaimNextHydratableTextInstance(fiber: Fiber): void { const text = fiber.pendingProps; const isHydratable = isHydratableText(text); + if (isHydratable) { + // Validate that this is ok to render here before any mismatches. + const currentHostContext = getHostContext(); + validateHydratableTextInstance(text, currentHostContext); + } + const initialInstance = nextHydratableInstance; const nextInstance = nextHydratableInstance; if (!nextInstance || !isHydratable) { diff --git a/packages/react-reconciler/src/forks/ReactFiberConfig.custom.js b/packages/react-reconciler/src/forks/ReactFiberConfig.custom.js index dcb402013f796..8e6c96bd4fe0a 100644 --- a/packages/react-reconciler/src/forks/ReactFiberConfig.custom.js +++ b/packages/react-reconciler/src/forks/ReactFiberConfig.custom.js @@ -193,6 +193,9 @@ export const didNotFindHydratableTextInstance = export const didNotFindHydratableSuspenseInstance = $$$config.didNotFindHydratableSuspenseInstance; export const errorHydratingContainer = $$$config.errorHydratingContainer; +export const validateHydratableInstance = $$$config.validateHydratableInstance; +export const validateHydratableTextInstance = + $$$config.validateHydratableTextInstance; // ------------------- // Resources From f03b4c4211f96a10b7439cf9a2ed097033aee0fa Mon Sep 17 00:00:00 2001 From: Sebastian Markbage Date: Fri, 23 Feb 2024 17:43:17 -0500 Subject: [PATCH 2/5] Don't warn for immediate follow ups This really relies on us throwing to abort hydrating siblings so we don't get extra mismatches after. --- .../src/client/ReactFiberConfigDOM.js | 10 ++-- .../src/client/validateDOMNesting.js | 16 +++-- .../src/ReactFiberHydrationContext.js | 60 +++++++++++++++---- 3 files changed, 64 insertions(+), 22 deletions(-) diff --git a/packages/react-dom-bindings/src/client/ReactFiberConfigDOM.js b/packages/react-dom-bindings/src/client/ReactFiberConfigDOM.js index 150f88d62360b..9e2c5e41c062f 100644 --- a/packages/react-dom-bindings/src/client/ReactFiberConfigDOM.js +++ b/packages/react-dom-bindings/src/client/ReactFiberConfigDOM.js @@ -1359,12 +1359,13 @@ export function validateHydratableInstance( type: string, props: Props, hostContext: HostContext, -): void { +): boolean { if (__DEV__) { // TODO: take namespace into account when validating. const hostContextDev: HostContextDev = (hostContext: any); - validateDOMNesting(type, hostContextDev.ancestorInfo); + return validateDOMNesting(type, hostContextDev.ancestorInfo); } + return true; } export function hydrateInstance( @@ -1398,14 +1399,15 @@ export function hydrateInstance( export function validateHydratableTextInstance( text: string, hostContext: HostContext, -): void { +): boolean { if (__DEV__) { const hostContextDev = ((hostContext: any): HostContextDev); const ancestor = hostContextDev.ancestorInfo.current; if (ancestor != null) { - validateTextNesting(text, ancestor.tag); + return validateTextNesting(text, ancestor.tag); } } + return true; } export function hydrateTextInstance( diff --git a/packages/react-dom-bindings/src/client/validateDOMNesting.js b/packages/react-dom-bindings/src/client/validateDOMNesting.js index f910455f008be..c950ddde08c16 100644 --- a/packages/react-dom-bindings/src/client/validateDOMNesting.js +++ b/packages/react-dom-bindings/src/client/validateDOMNesting.js @@ -441,7 +441,7 @@ const didWarn: {[string]: boolean} = {}; function validateDOMNesting( childTag: string, ancestorInfo: AncestorInfoDev, -): void { +): boolean { if (__DEV__) { ancestorInfo = ancestorInfo || emptyAncestorInfoDev; const parentInfo = ancestorInfo.current; @@ -455,7 +455,7 @@ function validateDOMNesting( : findInvalidAncestorForTag(childTag, ancestorInfo); const invalidParentOrAncestor = invalidParent || invalidAncestor; if (!invalidParentOrAncestor) { - return; + return true; } const ancestorTag = invalidParentOrAncestor.tag; @@ -464,7 +464,7 @@ function validateDOMNesting( // eslint-disable-next-line react-internal/safe-string-coercion String(!!invalidParent) + '|' + childTag + '|' + ancestorTag; if (didWarn[warnKey]) { - return; + return false; } didWarn[warnKey] = true; @@ -489,19 +489,21 @@ function validateDOMNesting( ancestorTag, ); } + return false; } + return true; } -function validateTextNesting(childText: string, parentTag: string): void { +function validateTextNesting(childText: string, parentTag: string): boolean { if (__DEV__) { if (isTagValidWithParent('#text', parentTag)) { - return; + return true; } // eslint-disable-next-line react-internal/safe-string-coercion const warnKey = '#text|' + parentTag; if (didWarn[warnKey]) { - return; + return false; } didWarn[warnKey] = true; @@ -515,7 +517,9 @@ function validateTextNesting(childText: string, parentTag: string): void { parentTag, ); } + return false; } + return true; } export {updatedAncestorInfoDev, validateDOMNesting, validateTextNesting}; diff --git a/packages/react-reconciler/src/ReactFiberHydrationContext.js b/packages/react-reconciler/src/ReactFiberHydrationContext.js index eec261244579a..8960419b80163 100644 --- a/packages/react-reconciler/src/ReactFiberHydrationContext.js +++ b/packages/react-reconciler/src/ReactFiberHydrationContext.js @@ -76,7 +76,7 @@ import { isFormStateMarkerMatching, isHydratableText, validateHydratableInstance, - validateHydratableTextInstance + validateHydratableTextInstance, } from './ReactFiberConfig'; import {OffscreenLane} from './ReactFiberLane'; import { @@ -204,7 +204,6 @@ function deleteHydratableInstance( returnFiber: Fiber, instance: HydratableInstance, ) { - warnUnhydratedInstance(returnFiber, instance); const childToDelete = createFiberFromHostInstanceForDeletion(); childToDelete.stateNode = instance; childToDelete.return = returnFiber; @@ -218,7 +217,7 @@ function deleteHydratableInstance( } } -function warnNonhydratedInstance(returnFiber: Fiber, fiber: Fiber) { +function warnNonHydratedInstance(returnFiber: Fiber, fiber: Fiber) { if (__DEV__) { if (didSuspendOrErrorDEV) { // Inside a boundary that already suspended. We're currently rendering the @@ -341,7 +340,6 @@ function warnNonhydratedInstance(returnFiber: Fiber, fiber: Fiber) { } function insertNonHydratedInstance(returnFiber: Fiber, fiber: Fiber) { fiber.flags = (fiber.flags & ~Hydrating) | Placement; - warnNonhydratedInstance(returnFiber, fiber); } function tryHydrateInstance(fiber: Fiber, nextInstance: any) { @@ -451,17 +449,26 @@ function tryToClaimNextHydratableInstance(fiber: Fiber): void { // Validate that this is ok to render here before any mismatches. const currentHostContext = getHostContext(); - validateHydratableInstance(fiber.type, fiber.pendingProps, currentHostContext); + const shouldKeepWarning = validateHydratableInstance( + fiber.type, + fiber.pendingProps, + currentHostContext, + ); const initialInstance = nextHydratableInstance; const nextInstance = nextHydratableInstance; if (!nextInstance) { if (shouldClientRenderOnMismatch(fiber)) { - warnNonhydratedInstance((hydrationParentFiber: any), fiber); + if (shouldKeepWarning) { + warnNonHydratedInstance((hydrationParentFiber: any), fiber); + } throwOnHydrationMismatch(fiber); } // Nothing to hydrate. Make it an insertion. insertNonHydratedInstance((hydrationParentFiber: any), fiber); + if (shouldKeepWarning) { + warnNonHydratedInstance((hydrationParentFiber: any), fiber); + } isHydrating = false; hydrationParentFiber = fiber; nextHydratableInstance = initialInstance; @@ -470,7 +477,9 @@ function tryToClaimNextHydratableInstance(fiber: Fiber): void { const firstAttemptedInstance = nextInstance; if (!tryHydrateInstance(fiber, nextInstance)) { if (shouldClientRenderOnMismatch(fiber)) { - warnNonhydratedInstance((hydrationParentFiber: any), fiber); + if (shouldKeepWarning) { + warnNonHydratedInstance((hydrationParentFiber: any), fiber); + } throwOnHydrationMismatch(fiber); } // If we can't hydrate this instance let's try the next one. @@ -484,6 +493,9 @@ function tryToClaimNextHydratableInstance(fiber: Fiber): void { ) { // Nothing to hydrate. Make it an insertion. insertNonHydratedInstance((hydrationParentFiber: any), fiber); + if (shouldKeepWarning) { + warnNonHydratedInstance((hydrationParentFiber: any), fiber); + } isHydrating = false; hydrationParentFiber = fiber; nextHydratableInstance = initialInstance; @@ -493,6 +505,9 @@ function tryToClaimNextHydratableInstance(fiber: Fiber): void { // superfluous and we'll delete it. Since we can't eagerly delete it // we'll have to schedule a deletion. To do that, this node needs a dummy // fiber associated with it. + if (shouldKeepWarning) { + warnUnhydratedInstance(prevHydrationParentFiber, firstAttemptedInstance); + } deleteHydratableInstance(prevHydrationParentFiber, firstAttemptedInstance); } } @@ -504,10 +519,14 @@ function tryToClaimNextHydratableTextInstance(fiber: Fiber): void { const text = fiber.pendingProps; const isHydratable = isHydratableText(text); + let shouldKeepWarning = true; if (isHydratable) { // Validate that this is ok to render here before any mismatches. const currentHostContext = getHostContext(); - validateHydratableTextInstance(text, currentHostContext); + shouldKeepWarning = validateHydratableTextInstance( + text, + currentHostContext, + ); } const initialInstance = nextHydratableInstance; @@ -516,11 +535,16 @@ function tryToClaimNextHydratableTextInstance(fiber: Fiber): void { // We exclude non hydrabable text because we know there are no matching hydratables. // We either throw or insert depending on the render mode. if (shouldClientRenderOnMismatch(fiber)) { - warnNonhydratedInstance((hydrationParentFiber: any), fiber); + if (shouldKeepWarning) { + warnNonHydratedInstance((hydrationParentFiber: any), fiber); + } throwOnHydrationMismatch(fiber); } // Nothing to hydrate. Make it an insertion. insertNonHydratedInstance((hydrationParentFiber: any), fiber); + if (shouldKeepWarning) { + warnNonHydratedInstance((hydrationParentFiber: any), fiber); + } isHydrating = false; hydrationParentFiber = fiber; nextHydratableInstance = initialInstance; @@ -529,7 +553,9 @@ function tryToClaimNextHydratableTextInstance(fiber: Fiber): void { const firstAttemptedInstance = nextInstance; if (!tryHydrateText(fiber, nextInstance)) { if (shouldClientRenderOnMismatch(fiber)) { - warnNonhydratedInstance((hydrationParentFiber: any), fiber); + if (shouldKeepWarning) { + warnNonHydratedInstance((hydrationParentFiber: any), fiber); + } throwOnHydrationMismatch(fiber); } // If we can't hydrate this instance let's try the next one. @@ -544,6 +570,9 @@ function tryToClaimNextHydratableTextInstance(fiber: Fiber): void { ) { // Nothing to hydrate. Make it an insertion. insertNonHydratedInstance((hydrationParentFiber: any), fiber); + if (shouldKeepWarning) { + warnNonHydratedInstance((hydrationParentFiber: any), fiber); + } isHydrating = false; hydrationParentFiber = fiber; nextHydratableInstance = initialInstance; @@ -553,6 +582,9 @@ function tryToClaimNextHydratableTextInstance(fiber: Fiber): void { // superfluous and we'll delete it. Since we can't eagerly delete it // we'll have to schedule a deletion. To do that, this node needs a dummy // fiber associated with it. + if (shouldKeepWarning) { + warnUnhydratedInstance(prevHydrationParentFiber, firstAttemptedInstance); + } deleteHydratableInstance(prevHydrationParentFiber, firstAttemptedInstance); } } @@ -565,11 +597,12 @@ function tryToClaimNextHydratableSuspenseInstance(fiber: Fiber): void { const nextInstance = nextHydratableInstance; if (!nextInstance) { if (shouldClientRenderOnMismatch(fiber)) { - warnNonhydratedInstance((hydrationParentFiber: any), fiber); + warnNonHydratedInstance((hydrationParentFiber: any), fiber); throwOnHydrationMismatch(fiber); } // Nothing to hydrate. Make it an insertion. insertNonHydratedInstance((hydrationParentFiber: any), fiber); + warnNonHydratedInstance((hydrationParentFiber: any), fiber); isHydrating = false; hydrationParentFiber = fiber; nextHydratableInstance = initialInstance; @@ -578,7 +611,7 @@ function tryToClaimNextHydratableSuspenseInstance(fiber: Fiber): void { const firstAttemptedInstance = nextInstance; if (!tryHydrateSuspense(fiber, nextInstance)) { if (shouldClientRenderOnMismatch(fiber)) { - warnNonhydratedInstance((hydrationParentFiber: any), fiber); + warnNonHydratedInstance((hydrationParentFiber: any), fiber); throwOnHydrationMismatch(fiber); } // If we can't hydrate this instance let's try the next one. @@ -593,6 +626,7 @@ function tryToClaimNextHydratableSuspenseInstance(fiber: Fiber): void { ) { // Nothing to hydrate. Make it an insertion. insertNonHydratedInstance((hydrationParentFiber: any), fiber); + warnNonHydratedInstance((hydrationParentFiber: any), fiber); isHydrating = false; hydrationParentFiber = fiber; nextHydratableInstance = initialInstance; @@ -602,6 +636,7 @@ function tryToClaimNextHydratableSuspenseInstance(fiber: Fiber): void { // superfluous and we'll delete it. Since we can't eagerly delete it // we'll have to schedule a deletion. To do that, this node needs a dummy // fiber associated with it. + warnUnhydratedInstance(prevHydrationParentFiber, firstAttemptedInstance); deleteHydratableInstance(prevHydrationParentFiber, firstAttemptedInstance); } } @@ -847,6 +882,7 @@ function popHydrationState(fiber: Fiber): boolean { throwOnHydrationMismatch(fiber); } else { while (nextInstance) { + warnUnhydratedInstance(fiber, nextInstance); deleteHydratableInstance(fiber, nextInstance); nextInstance = getNextHydratableSibling(nextInstance); } From 1095095b43c8508ed024bd92123ee51c5d2c5395 Mon Sep 17 00:00:00 2001 From: Sebastian Markbage Date: Fri, 23 Feb 2024 17:50:36 -0500 Subject: [PATCH 3/5] Refine the error message slightly to clarify That this is according to the rules of HTML, not React, and that this will cause a hydration error if not addressed. "appear as" is a bit academic. --- .../src/client/validateDOMNesting.js | 17 ++++++++++++----- 1 file changed, 12 insertions(+), 5 deletions(-) diff --git a/packages/react-dom-bindings/src/client/validateDOMNesting.js b/packages/react-dom-bindings/src/client/validateDOMNesting.js index c950ddde08c16..ab49cb4f4b025 100644 --- a/packages/react-dom-bindings/src/client/validateDOMNesting.js +++ b/packages/react-dom-bindings/src/client/validateDOMNesting.js @@ -477,14 +477,16 @@ function validateDOMNesting( 'the browser.'; } console.error( - '%s cannot appear as a child of <%s>.%s', + 'In HTML, %s cannot be a child of <%s>.%s\n' + + 'This will cause a hydration error.', tagDisplayName, ancestorTag, info, ); } else { console.error( - '%s cannot appear as a descendant of ' + '<%s>.', + 'In HTML, %s cannot be a descendant of <%s>.\n' + + 'This will cause a hydration error.', tagDisplayName, ancestorTag, ); @@ -508,12 +510,17 @@ function validateTextNesting(childText: string, parentTag: string): boolean { didWarn[warnKey] = true; if (/\S/.test(childText)) { - console.error('Text nodes cannot appear as a child of <%s>.', parentTag); + console.error( + 'In HTML, text nodes cannot be a child of <%s>.\n' + + 'This will cause a hydration error.', + parentTag, + ); } else { console.error( - 'Whitespace text nodes cannot appear as a child of <%s>. ' + + 'In HTML, 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.', + 'each line of your source code.\n' + + 'This will cause a hydration error.', parentTag, ); } From 58868bce164cee8049c155ec9766a9fa975b9ecd Mon Sep 17 00:00:00 2001 From: Sebastian Markbage Date: Fri, 23 Feb 2024 18:52:45 -0500 Subject: [PATCH 4/5] Move hydration outside the host context of the own instance Because otherwise we'll act as if we're already inside the child so the child is already inside itself. I don't think we use the context for anything at runtime. --- packages/react-reconciler/src/ReactFiberBeginWork.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/react-reconciler/src/ReactFiberBeginWork.js b/packages/react-reconciler/src/ReactFiberBeginWork.js index f44ec08829eb4..3e7d3693ded9b 100644 --- a/packages/react-reconciler/src/ReactFiberBeginWork.js +++ b/packages/react-reconciler/src/ReactFiberBeginWork.js @@ -1512,12 +1512,12 @@ function updateHostComponent( workInProgress: Fiber, renderLanes: Lanes, ) { - pushHostContext(workInProgress); - if (current === null) { tryToClaimNextHydratableInstance(workInProgress); } + pushHostContext(workInProgress); + const type = workInProgress.type; const nextProps = workInProgress.pendingProps; const prevProps = current !== null ? current.memoizedProps : null; From 353871a1fef9dd2430ed0c8bf0e1530b82a161fa Mon Sep 17 00:00:00 2001 From: Sebastian Markbage Date: Fri, 23 Feb 2024 19:04:12 -0500 Subject: [PATCH 5/5] Update tests --- .../src/__tests__/ReactDOMComponent-test.js | 39 +++++++++++-------- .../src/__tests__/ReactDOMFloat-test.js | 20 +++++----- .../src/__tests__/ReactDOMForm-test.js | 3 +- .../src/__tests__/ReactDOMOption-test.js | 5 ++- .../src/__tests__/validateDOMNesting-test.js | 37 ++++++++++++++---- 5 files changed, 67 insertions(+), 37 deletions(-) diff --git a/packages/react-dom/src/__tests__/ReactDOMComponent-test.js b/packages/react-dom/src/__tests__/ReactDOMComponent-test.js index 53ad186761b74..6541293f51333 100644 --- a/packages/react-dom/src/__tests__/ReactDOMComponent-test.js +++ b/packages/react-dom/src/__tests__/ReactDOMComponent-test.js @@ -2188,8 +2188,9 @@ describe('ReactDOMComponent', () => { ); }); }).toErrorDev([ - 'Warning: cannot appear as a child of ' + - '
.' + + 'Warning: In HTML, cannot be a child of ' + + '
.\n' + + 'This will cause a hydration error.' + '\n in tr (at **)' + '\n in div (at **)', ]); @@ -2208,8 +2209,9 @@ describe('ReactDOMComponent', () => { ); }); }).toErrorDev( - 'Warning:

cannot appear as a descendant ' + - 'of

.' + + 'Warning: In HTML,

cannot be a descendant ' + + 'of

.\n' + + 'This will cause a hydration error.' + // There is no outer `p` here because root container is not part of the stack. '\n in p (at **)' + '\n in span (at **)', @@ -2241,22 +2243,25 @@ describe('ReactDOMComponent', () => { root.render(); }); }).toErrorDev([ - 'Warning: cannot appear as a child of ' + + 'Warning: In HTML, cannot be a child of ' + '. Add a , or to your code to match the DOM tree generated ' + - 'by the browser.' + + 'by the browser.\n' + + 'This will cause a hydration error.' + '\n in tr (at **)' + '\n in Row (at **)' + '\n in table (at **)' + '\n in Foo (at **)', - 'Warning: Text nodes cannot appear as a ' + - 'child of .' + + 'Warning: In HTML, text nodes cannot be a ' + + 'child of .\n' + + 'This will cause a hydration error.' + '\n in tr (at **)' + '\n in Row (at **)' + '\n in table (at **)' + '\n in Foo (at **)', - 'Warning: Whitespace text nodes cannot ' + - "appear as a child of
. Make sure you don't have any extra " + - 'whitespace between tags on each line of your source code.' + + 'Warning: In HTML, whitespace text nodes cannot ' + + "be a child of
. 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.' + '\n in table (at **)' + '\n in Foo (at **)', ]); @@ -2283,9 +2288,10 @@ describe('ReactDOMComponent', () => { root.render( ); }); }).toErrorDev([ - 'Warning: Whitespace text nodes cannot ' + - "appear as a child of
. Make sure you don't have any extra " + - 'whitespace between tags on each line of your source code.' + + 'Warning: In HTML, whitespace text nodes cannot ' + + "be a child of
. 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.' + '\n in table (at **)' + '\n in Foo (at **)', ]); @@ -2311,8 +2317,9 @@ describe('ReactDOMComponent', () => { ); }); }).toErrorDev([ - 'Warning: Text nodes cannot appear as a ' + - 'child of .' + + 'Warning: In HTML, text nodes cannot be a ' + + 'child of .\n' + + 'This will cause a hydration error.' + '\n in tr (at **)' + '\n in Row (at **)' + '\n in tbody (at **)' + diff --git a/packages/react-dom/src/__tests__/ReactDOMFloat-test.js b/packages/react-dom/src/__tests__/ReactDOMFloat-test.js index 64f79140917c8..31c1607cc149c 100644 --- a/packages/react-dom/src/__tests__/ReactDOMFloat-test.js +++ b/packages/react-dom/src/__tests__/ReactDOMFloat-test.js @@ -523,7 +523,7 @@ describe('ReactDOMFloat', () => { }).toErrorDev( [ 'Cannot render