From 9a6686041eb313948b8e9da7066364e5574c339d Mon Sep 17 00:00:00 2001 From: Marco Salazar Date: Mon, 7 Feb 2022 14:32:09 -0500 Subject: [PATCH 1/3] add back warning --- .../src/__tests__/ReactDOMFizzServer-test.js | 18 +- ...DOMServerPartialHydration-test.internal.js | 340 ++++++++++-------- .../src/ReactFiberHydrationContext.new.js | 29 +- .../src/ReactFiberHydrationContext.old.js | 29 +- 4 files changed, 245 insertions(+), 171 deletions(-) diff --git a/packages/react-dom/src/__tests__/ReactDOMFizzServer-test.js b/packages/react-dom/src/__tests__/ReactDOMFizzServer-test.js index 483c588c37b9f..2fddae2d64df2 100644 --- a/packages/react-dom/src/__tests__/ReactDOMFizzServer-test.js +++ b/packages/react-dom/src/__tests__/ReactDOMFizzServer-test.js @@ -1740,8 +1740,13 @@ describe('ReactDOMFizzServer', () => { 'The server HTML was replaced with client content', ]); }).toErrorDev( - 'Warning: An error occurred during hydration. The server HTML was replaced with client content', - {withoutStack: true}, + [ + 'Warning: An error occurred during hydration. The server HTML was replaced with client content in
.', + 'Warning: Expected server HTML to contain a matching
in
.\n' + + ' in div (at **)\n' + + ' in App (at **)', + ], + {withoutStack: 1}, ); expect(getVisibleChildren(container)).toEqual(
client
); } else { @@ -1833,8 +1838,13 @@ describe('ReactDOMFizzServer', () => { 'The server HTML was replaced with client content', ]); }).toErrorDev( - 'Warning: An error occurred during hydration. The server HTML was replaced with client content', - {withoutStack: true}, + [ + 'Warning: An error occurred during hydration. The server HTML was replaced with client content', + 'Warning: Expected server HTML to contain a matching
in
.\n' + + ' in div (at **)\n' + + ' in App (at **)', + ], + {withoutStack: 1}, ); expect(getVisibleChildren(container)).toEqual(
client
); } else { diff --git a/packages/react-dom/src/__tests__/ReactDOMServerPartialHydration-test.internal.js b/packages/react-dom/src/__tests__/ReactDOMServerPartialHydration-test.internal.js index e071a36737104..ce75f49492425 100644 --- a/packages/react-dom/src/__tests__/ReactDOMServerPartialHydration-test.internal.js +++ b/packages/react-dom/src/__tests__/ReactDOMServerPartialHydration-test.internal.js @@ -19,6 +19,15 @@ let SuspenseList; let act; let IdleEventPriority; +function normalizeCodeLocInfo(strOrErr) { + if (strOrErr && strOrErr.replace) { + return strOrErr.replace(/\n +(?:at|in) ([\S]+)[^\n]*/g, function(m, name) { + return '\n in ' + name + ' (at **)'; + }); + } + return strOrErr; +} + function dispatchMouseEvent(to, from) { if (!to) { to = null; @@ -240,106 +249,126 @@ describe('ReactDOMServerPartialHydration', () => { // @gate enableClientRenderFallbackOnHydrationMismatch it('falls back to client rendering boundary on mismatch', async () => { - let client = false; - let suspend = false; - let resolve; - const promise = new Promise(resolvePromise => { - resolve = () => { - suspend = false; - resolvePromise(); - }; - }); - function Child() { - if (suspend) { - Scheduler.unstable_yieldValue('Suspend'); - throw promise; - } else { - Scheduler.unstable_yieldValue('Hello'); - return 'Hello'; + // We can't use the toErrorDev helper here because this is async. + const originalConsoleError = console.error; + const mockError = jest.fn(); + console.error = (...args) => { + mockError(...args.map(normalizeCodeLocInfo)); + }; + try { + let client = false; + let suspend = false; + let resolve; + const promise = new Promise(resolvePromise => { + resolve = () => { + suspend = false; + resolvePromise(); + }; + }); + function Child() { + if (suspend) { + Scheduler.unstable_yieldValue('Suspend'); + throw promise; + } else { + Scheduler.unstable_yieldValue('Hello'); + return 'Hello'; + } } - } - function Component({shouldMismatch}) { - Scheduler.unstable_yieldValue('Component'); - if (shouldMismatch && client) { - return
Mismatch
; + function Component({shouldMismatch}) { + Scheduler.unstable_yieldValue('Component'); + if (shouldMismatch && client) { + return
Mismatch
; + } + return
Component
; } - return
Component
; - } - function App() { - return ( - - - - - - - - ); - } - const finalHTML = ReactDOMServer.renderToString(); - const container = document.createElement('div'); - container.innerHTML = finalHTML; - expect(Scheduler).toHaveYielded([ - 'Hello', - 'Component', - 'Component', - 'Component', - 'Component', - ]); + function App() { + return ( + + + + + + + + ); + } + const finalHTML = ReactDOMServer.renderToString(); + const container = document.createElement('div'); + container.innerHTML = finalHTML; + expect(Scheduler).toHaveYielded([ + 'Hello', + 'Component', + 'Component', + 'Component', + 'Component', + ]); - expect(container.innerHTML).toBe( - 'Hello
Component
Component
Component
Component
', - ); + expect(container.innerHTML).toBe( + 'Hello
Component
Component
Component
Component
', + ); - suspend = true; - client = true; + suspend = true; + client = true; - ReactDOM.hydrateRoot(container, , { - onRecoverableError(error) { - Scheduler.unstable_yieldValue(error.message); - }, - }); - expect(Scheduler).toFlushAndYield([ - 'Suspend', - 'Component', - 'Component', - 'Component', - 'Component', - ]); - jest.runAllTimers(); + ReactDOM.hydrateRoot(container, , { + onRecoverableError(error) { + Scheduler.unstable_yieldValue(error.message); + }, + }); + expect(Scheduler).toFlushAndYield([ + 'Suspend', + 'Component', + 'Component', + 'Component', + 'Component', + ]); + jest.runAllTimers(); - // Unchanged - expect(container.innerHTML).toBe( - 'Hello
Component
Component
Component
Component
', - ); + // Unchanged + expect(container.innerHTML).toBe( + 'Hello
Component
Component
Component
Component
', + ); - suspend = false; - resolve(); - await promise; + suspend = false; + resolve(); + await promise; + expect(Scheduler).toFlushAndYield([ + // first pass, mismatches at end + 'Hello', + 'Component', + 'Component', + 'Component', + 'Component', + + // second pass as client render + 'Hello', + 'Component', + 'Component', + 'Component', + 'Component', + + // Hydration mismatch is logged + 'An error occurred during hydration. The server HTML was replaced with client content', + ]); - expect(Scheduler).toFlushAndYield([ - // first pass, mismatches at end - 'Hello', - 'Component', - 'Component', - 'Component', - 'Component', - - // second pass as client render - 'Hello', - 'Component', - 'Component', - 'Component', - 'Component', - - // Hydration mismatch is logged - 'An error occurred during hydration. The server HTML was replaced with client content', - ]); + // Client rendered - suspense comment nodes removed + expect(container.innerHTML).toBe( + 'Hello
Component
Component
Component
Mismatch
', + ); - // Client rendered - suspense comment nodes removed - expect(container.innerHTML).toBe( - 'Hello
Component
Component
Component
Mismatch
', - ); + expect(mockError.mock.calls[0]).toEqual([ + 'Warning: Expected server HTML to contain a matching <%s> in <%s>.%s', + 'div', + 'div', + '\n' + + ' in div (at **)\n' + + ' in Component (at **)\n' + + ' in Suspense (at **)\n' + + ' in App (at **)', + ]); + } finally { + console.error = originalConsoleError; + } }); it('calls the hydration callbacks after hydration or deletion', async () => { @@ -493,66 +522,82 @@ describe('ReactDOMServerPartialHydration', () => { }); it('recovers with client render when server rendered additional nodes at suspense root after unsuspending', async () => { - spyOnDev(console, 'error'); - const ref = React.createRef(); - function App({hasB}) { - return ( -
- - - A - {hasB ? B : null} - -
Sibling
-
- ); - } + // We can't use the toErrorDev helper here because this is async. + const originalConsoleError = console.error; + const mockError = jest.fn(); + console.error = (...args) => { + mockError(...args.map(normalizeCodeLocInfo)); + }; + try { + const ref = React.createRef(); + function App({hasB}) { + return ( +
+ + + A + {hasB ? B : null} + +
Sibling
+
+ ); + } - let shouldSuspend = false; - let resolve; - const promise = new Promise(res => { - resolve = () => { - shouldSuspend = false; - res(); - }; - }); - function Suspender() { - if (shouldSuspend) { - throw promise; + let shouldSuspend = false; + let resolve; + const promise = new Promise(res => { + resolve = () => { + shouldSuspend = false; + res(); + }; + }); + function Suspender() { + if (shouldSuspend) { + throw promise; + } + return <>; } - return <>; - } - const finalHTML = ReactDOMServer.renderToString(); + const finalHTML = ReactDOMServer.renderToString(); - const container = document.createElement('div'); - container.innerHTML = finalHTML; + const container = document.createElement('div'); + container.innerHTML = finalHTML; - const span = container.getElementsByTagName('span')[0]; + const span = container.getElementsByTagName('span')[0]; - expect(container.innerHTML).toContain('A'); - expect(container.innerHTML).toContain('B'); - expect(ref.current).toBe(null); + expect(container.innerHTML).toContain('A'); + expect(container.innerHTML).toContain('B'); + expect(ref.current).toBe(null); - shouldSuspend = true; - act(() => { - ReactDOM.hydrateRoot(container, ); - }); + shouldSuspend = true; + act(() => { + ReactDOM.hydrateRoot(container, ); + }); - // await expect(async () => { - resolve(); - await promise; - Scheduler.unstable_flushAll(); - await null; - jest.runAllTimers(); - // }).toErrorDev('Did not expect server HTML to contain a in
'); + resolve(); + await promise; + Scheduler.unstable_flushAll(); + await null; + jest.runAllTimers(); - expect(container.innerHTML).toContain('A'); - expect(container.innerHTML).not.toContain('B'); - if (gate(flags => flags.enableClientRenderFallbackOnHydrationMismatch)) { - expect(ref.current).not.toBe(span); - } else { - expect(ref.current).toBe(span); + expect(container.innerHTML).toContain('A'); + expect(container.innerHTML).not.toContain('B'); + if (gate(flags => flags.enableClientRenderFallbackOnHydrationMismatch)) { + expect(ref.current).not.toBe(span); + } else { + expect(ref.current).toBe(span); + } + expect(mockError).toHaveBeenCalledWith( + 'Warning: Did not expect server HTML to contain a <%s> in <%s>.%s', + 'span', + 'div', + '\n' + + ' in Suspense (at **)\n' + + ' in div (at **)\n' + + ' in App (at **)', + ); + } finally { + console.error = originalConsoleError; } }); @@ -3179,9 +3224,14 @@ describe('ReactDOMServerPartialHydration', () => { }); }); }).toErrorDev( - 'Warning: An error occurred during hydration. ' + - 'The server HTML was replaced with client content in
.', - {withoutStack: true}, + [ + 'Warning: An error occurred during hydration. ' + + 'The server HTML was replaced with client content in
.', + 'Warning: Expected server HTML to contain a matching in
.\n' + + ' in span (at **)\n' + + ' in App (at **)', + ], + {withoutStack: 1}, ); expect(Scheduler).toHaveYielded([ 'Log recoverable error: An error occurred during hydration. The server ' + diff --git a/packages/react-reconciler/src/ReactFiberHydrationContext.new.js b/packages/react-reconciler/src/ReactFiberHydrationContext.new.js index 4a460d584a53d..ab2d05cea0aac 100644 --- a/packages/react-reconciler/src/ReactFiberHydrationContext.new.js +++ b/packages/react-reconciler/src/ReactFiberHydrationContext.new.js @@ -183,8 +183,7 @@ function deleteHydratableInstance( } } -function insertNonHydratedInstance(returnFiber: Fiber, fiber: Fiber) { - fiber.flags = (fiber.flags & ~Hydrating) | Placement; +function warnNonhydratedInstance(returnFiber: Fiber, fiber: Fiber) { if (__DEV__) { switch (returnFiber.tag) { case HostRoot: { @@ -283,6 +282,10 @@ function insertNonHydratedInstance(returnFiber: Fiber, fiber: Fiber) { } } } +function insertNonHydratedInstance(returnFiber: Fiber, fiber: Fiber) { + fiber.flags = (fiber.flags & ~Hydrating) | Placement; + warnNonhydratedInstance(returnFiber, fiber); +} function tryHydrate(fiber, nextInstance) { switch (fiber.tag) { @@ -353,12 +356,10 @@ function shouldClientRenderOnMismatch(fiber: Fiber) { ); } -function throwOnHydrationMismatchIfConcurrentMode(fiber: Fiber) { - if (shouldClientRenderOnMismatch(fiber)) { - throw new Error( - 'An error occurred during hydration. The server HTML was replaced with client content', - ); - } +function throwOnHydrationMismatch(fiber: Fiber) { + throw new Error( + 'An error occurred during hydration. The server HTML was replaced with client content', + ); } function tryToClaimNextHydratableInstance(fiber: Fiber): void { @@ -367,7 +368,10 @@ function tryToClaimNextHydratableInstance(fiber: Fiber): void { } let nextInstance = nextHydratableInstance; if (!nextInstance) { - throwOnHydrationMismatchIfConcurrentMode(fiber); + if (shouldClientRenderOnMismatch(fiber)) { + warnNonhydratedInstance((hydrationParentFiber: any), fiber); + throwOnHydrationMismatch(fiber); + } // Nothing to hydrate. Make it an insertion. insertNonHydratedInstance((hydrationParentFiber: any), fiber); isHydrating = false; @@ -376,7 +380,10 @@ function tryToClaimNextHydratableInstance(fiber: Fiber): void { } const firstAttemptedInstance = nextInstance; if (!tryHydrate(fiber, nextInstance)) { - throwOnHydrationMismatchIfConcurrentMode(fiber); + if (shouldClientRenderOnMismatch(fiber)) { + warnNonhydratedInstance((hydrationParentFiber: any), fiber); + throwOnHydrationMismatch(fiber); + } // If we can't hydrate this instance let's try the next one. // We use this as a heuristic. It's based on intuition and not data so it // might be flawed or unnecessary. @@ -565,7 +572,7 @@ function popHydrationState(fiber: Fiber): boolean { if (nextInstance) { if (shouldClientRenderOnMismatch(fiber)) { warnIfUnhydratedTailNodes(fiber); - throwOnHydrationMismatchIfConcurrentMode(fiber); + throwOnHydrationMismatch(fiber); } else { while (nextInstance) { deleteHydratableInstance(fiber, nextInstance); diff --git a/packages/react-reconciler/src/ReactFiberHydrationContext.old.js b/packages/react-reconciler/src/ReactFiberHydrationContext.old.js index 3ee040237829a..9ad186495d8e2 100644 --- a/packages/react-reconciler/src/ReactFiberHydrationContext.old.js +++ b/packages/react-reconciler/src/ReactFiberHydrationContext.old.js @@ -183,8 +183,7 @@ function deleteHydratableInstance( } } -function insertNonHydratedInstance(returnFiber: Fiber, fiber: Fiber) { - fiber.flags = (fiber.flags & ~Hydrating) | Placement; +function warnNonhydratedInstance(returnFiber: Fiber, fiber: Fiber) { if (__DEV__) { switch (returnFiber.tag) { case HostRoot: { @@ -283,6 +282,10 @@ function insertNonHydratedInstance(returnFiber: Fiber, fiber: Fiber) { } } } +function insertNonHydratedInstance(returnFiber: Fiber, fiber: Fiber) { + fiber.flags = (fiber.flags & ~Hydrating) | Placement; + warnNonhydratedInstance(returnFiber, fiber); +} function tryHydrate(fiber, nextInstance) { switch (fiber.tag) { @@ -353,12 +356,10 @@ function shouldClientRenderOnMismatch(fiber: Fiber) { ); } -function throwOnHydrationMismatchIfConcurrentMode(fiber: Fiber) { - if (shouldClientRenderOnMismatch(fiber)) { - throw new Error( - 'An error occurred during hydration. The server HTML was replaced with client content', - ); - } +function throwOnHydrationMismatch(fiber: Fiber) { + throw new Error( + 'An error occurred during hydration. The server HTML was replaced with client content', + ); } function tryToClaimNextHydratableInstance(fiber: Fiber): void { @@ -367,7 +368,10 @@ function tryToClaimNextHydratableInstance(fiber: Fiber): void { } let nextInstance = nextHydratableInstance; if (!nextInstance) { - throwOnHydrationMismatchIfConcurrentMode(fiber); + if (shouldClientRenderOnMismatch(fiber)) { + warnNonhydratedInstance((hydrationParentFiber: any), fiber); + throwOnHydrationMismatch(fiber); + } // Nothing to hydrate. Make it an insertion. insertNonHydratedInstance((hydrationParentFiber: any), fiber); isHydrating = false; @@ -376,7 +380,10 @@ function tryToClaimNextHydratableInstance(fiber: Fiber): void { } const firstAttemptedInstance = nextInstance; if (!tryHydrate(fiber, nextInstance)) { - throwOnHydrationMismatchIfConcurrentMode(fiber); + if (shouldClientRenderOnMismatch(fiber)) { + warnNonhydratedInstance((hydrationParentFiber: any), fiber); + throwOnHydrationMismatch(fiber); + } // If we can't hydrate this instance let's try the next one. // We use this as a heuristic. It's based on intuition and not data so it // might be flawed or unnecessary. @@ -565,7 +572,7 @@ function popHydrationState(fiber: Fiber): boolean { if (nextInstance) { if (shouldClientRenderOnMismatch(fiber)) { warnIfUnhydratedTailNodes(fiber); - throwOnHydrationMismatchIfConcurrentMode(fiber); + throwOnHydrationMismatch(fiber); } else { while (nextInstance) { deleteHydratableInstance(fiber, nextInstance); From 2c74911ebb30bd101d420af68fd68775979762ee Mon Sep 17 00:00:00 2001 From: Marco Salazar Date: Mon, 7 Feb 2022 14:50:59 -0500 Subject: [PATCH 2/3] wrapper errorMock in __DEV__ flag --- ...DOMServerPartialHydration-test.internal.js | 42 ++++++++++--------- 1 file changed, 23 insertions(+), 19 deletions(-) diff --git a/packages/react-dom/src/__tests__/ReactDOMServerPartialHydration-test.internal.js b/packages/react-dom/src/__tests__/ReactDOMServerPartialHydration-test.internal.js index ce75f49492425..e5eea593b869b 100644 --- a/packages/react-dom/src/__tests__/ReactDOMServerPartialHydration-test.internal.js +++ b/packages/react-dom/src/__tests__/ReactDOMServerPartialHydration-test.internal.js @@ -356,16 +356,18 @@ describe('ReactDOMServerPartialHydration', () => { 'Hello
Component
Component
Component
Mismatch
', ); - expect(mockError.mock.calls[0]).toEqual([ - 'Warning: Expected server HTML to contain a matching <%s> in <%s>.%s', - 'div', - 'div', - '\n' + - ' in div (at **)\n' + - ' in Component (at **)\n' + - ' in Suspense (at **)\n' + - ' in App (at **)', - ]); + if (__DEV__) { + expect(mockError.mock.calls[0]).toEqual([ + 'Warning: Expected server HTML to contain a matching <%s> in <%s>.%s', + 'div', + 'div', + '\n' + + ' in div (at **)\n' + + ' in Component (at **)\n' + + ' in Suspense (at **)\n' + + ' in App (at **)', + ]); + } } finally { console.error = originalConsoleError; } @@ -587,15 +589,17 @@ describe('ReactDOMServerPartialHydration', () => { } else { expect(ref.current).toBe(span); } - expect(mockError).toHaveBeenCalledWith( - 'Warning: Did not expect server HTML to contain a <%s> in <%s>.%s', - 'span', - 'div', - '\n' + - ' in Suspense (at **)\n' + - ' in div (at **)\n' + - ' in App (at **)', - ); + if (__DEV__) { + expect(mockError).toHaveBeenCalledWith( + 'Warning: Did not expect server HTML to contain a <%s> in <%s>.%s', + 'span', + 'div', + '\n' + + ' in Suspense (at **)\n' + + ' in div (at **)\n' + + ' in App (at **)', + ); + } } finally { console.error = originalConsoleError; } From 85d47ccb0d129fd053e6318bfb1b6c7855587537 Mon Sep 17 00:00:00 2001 From: Marco Salazar Date: Mon, 7 Feb 2022 14:54:19 -0500 Subject: [PATCH 3/3] lint --- ...DOMServerPartialHydration-test.internal.js | 125 +++++++++--------- 1 file changed, 62 insertions(+), 63 deletions(-) diff --git a/packages/react-dom/src/__tests__/ReactDOMServerPartialHydration-test.internal.js b/packages/react-dom/src/__tests__/ReactDOMServerPartialHydration-test.internal.js index e5eea593b869b..586901baa14c3 100644 --- a/packages/react-dom/src/__tests__/ReactDOMServerPartialHydration-test.internal.js +++ b/packages/react-dom/src/__tests__/ReactDOMServerPartialHydration-test.internal.js @@ -255,43 +255,43 @@ describe('ReactDOMServerPartialHydration', () => { console.error = (...args) => { mockError(...args.map(normalizeCodeLocInfo)); }; - try { - let client = false; - let suspend = false; - let resolve; - const promise = new Promise(resolvePromise => { - resolve = () => { - suspend = false; - resolvePromise(); - }; - }); - function Child() { - if (suspend) { - Scheduler.unstable_yieldValue('Suspend'); - throw promise; - } else { - Scheduler.unstable_yieldValue('Hello'); - return 'Hello'; - } - } - function Component({shouldMismatch}) { - Scheduler.unstable_yieldValue('Component'); - if (shouldMismatch && client) { - return
Mismatch
; - } - return
Component
; + let client = false; + let suspend = false; + let resolve; + const promise = new Promise(resolvePromise => { + resolve = () => { + suspend = false; + resolvePromise(); + }; + }); + function Child() { + if (suspend) { + Scheduler.unstable_yieldValue('Suspend'); + throw promise; + } else { + Scheduler.unstable_yieldValue('Hello'); + return 'Hello'; } - function App() { - return ( - - - - - - - - ); + } + function Component({shouldMismatch}) { + Scheduler.unstable_yieldValue('Component'); + if (shouldMismatch && client) { + return
Mismatch
; } + return
Component
; + } + function App() { + return ( + + + + + + + + ); + } + try { const finalHTML = ReactDOMServer.renderToString(); const container = document.createElement('div'); container.innerHTML = finalHTML; @@ -530,36 +530,35 @@ describe('ReactDOMServerPartialHydration', () => { console.error = (...args) => { mockError(...args.map(normalizeCodeLocInfo)); }; - try { - const ref = React.createRef(); - function App({hasB}) { - return ( -
- - - A - {hasB ? B : null} - -
Sibling
-
- ); - } - let shouldSuspend = false; - let resolve; - const promise = new Promise(res => { - resolve = () => { - shouldSuspend = false; - res(); - }; - }); - function Suspender() { - if (shouldSuspend) { - throw promise; - } - return <>; + const ref = React.createRef(); + let shouldSuspend = false; + let resolve; + const promise = new Promise(res => { + resolve = () => { + shouldSuspend = false; + res(); + }; + }); + function Suspender() { + if (shouldSuspend) { + throw promise; } - + return <>; + } + function App({hasB}) { + return ( +
+ + + A + {hasB ? B : null} + +
Sibling
+
+ ); + } + try { const finalHTML = ReactDOMServer.renderToString(); const container = document.createElement('div');