From f7aa5e0aa3e2aa51279af4b6cb5413912cacd7f5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebastian=20Markb=C3=A5ge?= Date: Tue, 26 Mar 2024 17:01:41 -0700 Subject: [PATCH] Move Hydration Mismatch Errors to Throw or Log Once (Kind of) (#28502) Stacked on #28476. We used to `console.error` for every mismatch we found, up until the error we threw for the hydration mismatch. This changes it so that we build up a set of diffs up until we either throw or complete hydrating the root/suspense boundary. If we throw, we append the diff to the error message which gets passed to onRecoverableError (which by default is also logged to console). If we complete, we append it to a `console.error`. Since we early abort when something throws, it effectively means that we can only collect multiple diffs if there were preceding non-throwing mismatches - i.e. only properties mismatched but tag name matched. There can still be multiple logs if multiple siblings Suspense boundaries all error hydrating but then they're separate errors entirely. We still log an extra line about something erroring but I think the goal should be that it leads to a single recoverable or console.error. This doesn't yet actually print the diff as part of this message. That's in a follow up PR. --- .../src/__tests__/ReactDOMFizzForm-test.js | 10 +- .../src/__tests__/ReactDOMFizzServer-test.js | 140 ++- ...actDOMFizzSuppressHydrationWarning-test.js | 68 +- .../src/__tests__/ReactDOMFloat-test.js | 2 - .../__tests__/ReactDOMHydrationDiff-test.js | 780 +++++++++----- .../src/__tests__/ReactDOMOption-test.js | 1 - .../src/__tests__/ReactDOMRoot-test.js | 3 +- ...DOMServerPartialHydration-test.internal.js | 985 +++++++----------- .../ReactDOMSingletonComponents-test.js | 29 +- .../src/__tests__/ReactRenderDocument-test.js | 45 +- .../ReactServerRenderingHydration-test.js | 16 +- .../ReactDOMServerIntegrationTestUtils.js | 13 +- .../src/ReactFiberCompleteWork.js | 3 + .../src/ReactFiberHydrationContext.js | 354 +++---- .../src/ReactFiberHydrationDiffs.js | 24 + scripts/error-codes/codes.json | 5 +- 16 files changed, 1207 insertions(+), 1271 deletions(-) create mode 100644 packages/react-reconciler/src/ReactFiberHydrationDiffs.js diff --git a/packages/react-dom/src/__tests__/ReactDOMFizzForm-test.js b/packages/react-dom/src/__tests__/ReactDOMFizzForm-test.js index 0c4edff51a490..f578748e923d2 100644 --- a/packages/react-dom/src/__tests__/ReactDOMFizzForm-test.js +++ b/packages/react-dom/src/__tests__/ReactDOMFizzForm-test.js @@ -182,7 +182,8 @@ describe('ReactDOMFizzForm', () => { ReactDOMClient.hydrateRoot(container, ); }); }).toErrorDev( - 'Prop `action` did not match. Server: "function" Client: "action"', + "A tree hydrated but some attributes of the server rendered HTML didn't match the client properties.", + {withoutStack: true}, ); }); @@ -344,7 +345,12 @@ describe('ReactDOMFizzForm', () => { await act(async () => { root = ReactDOMClient.hydrateRoot(container, ); }); - }).toErrorDev(['Prop `formTarget` did not match.']); + }).toErrorDev( + [ + "A tree hydrated but some attributes of the server rendered HTML didn't match the client properties.", + ], + {withoutStack: true}, + ); await act(async () => { root.render(); }); diff --git a/packages/react-dom/src/__tests__/ReactDOMFizzServer-test.js b/packages/react-dom/src/__tests__/ReactDOMFizzServer-test.js index 33b2b322de873..1caaeca68d293 100644 --- a/packages/react-dom/src/__tests__/ReactDOMFizzServer-test.js +++ b/packages/react-dom/src/__tests__/ReactDOMFizzServer-test.js @@ -47,6 +47,15 @@ let waitForPaint; let clientAct; let streamingContainer; +function normalizeError(msg) { + // Take the first sentence to make it easier to assert on. + const idx = msg.indexOf('.'); + if (idx > -1) { + return msg.slice(0, idx + 1); + } + return msg; +} + describe('ReactDOMFizzServer', () => { beforeEach(() => { jest.resetModules(); @@ -2391,7 +2400,9 @@ describe('ReactDOMFizzServer', () => { ReactDOMClient.hydrateRoot(container, , { onRecoverableError(error) { - Scheduler.log('Log recoverable error: ' + error.message); + Scheduler.log( + 'Log recoverable error: ' + normalizeError(error.message), + ); }, }); @@ -2399,18 +2410,12 @@ describe('ReactDOMFizzServer', () => { // The first paint switches to client rendering due to mismatch await waitForPaint([ 'client', - 'Log recoverable error: Hydration failed because the initial ' + - 'UI does not match what was rendered on the server.', - 'Log recoverable error: There was an error while hydrating. ' + - 'Because the error happened outside of a Suspense boundary, the ' + - 'entire root will switch to client rendering.', + "Log recoverable error: Hydration failed because the server rendered HTML didn't match the client.", + 'Log recoverable error: There was an error while hydrating.', ]); }).toErrorDev( [ 'Warning: An error occurred during hydration. The server HTML was replaced with client content.', - 'Warning: Expected server HTML to contain a matching
in the root.\n' + - ' in div (at **)\n' + - ' in App (at **)', ], {withoutStack: 1}, ); @@ -2474,7 +2479,9 @@ describe('ReactDOMFizzServer', () => { ReactDOMClient.hydrateRoot(container, , { onRecoverableError(error) { - Scheduler.log('Log recoverable error: ' + error.message); + Scheduler.log( + 'Log recoverable error: ' + normalizeError(error.message), + ); }, }); @@ -2483,18 +2490,12 @@ describe('ReactDOMFizzServer', () => { // The first paint switches to client rendering due to mismatch await waitForPaint([ 'client', - 'Log recoverable error: Hydration failed because the initial ' + - 'UI does not match what was rendered on the server.', - 'Log recoverable error: There was an error while hydrating. ' + - 'Because the error happened outside of a Suspense boundary, the ' + - 'entire root will switch to client rendering.', + "Log recoverable error: Hydration failed because the server rendered HTML didn't match the client.", + 'Log recoverable error: There was an error while hydrating.', ]); }).toErrorDev( [ 'Warning: An error occurred during hydration. The server HTML was replaced with client content', - 'Warning: Expected server HTML to contain a matching
in the root.\n' + - ' in div (at **)\n' + - ' in App (at **)', ], {withoutStack: 1}, ); @@ -2557,7 +2558,7 @@ describe('ReactDOMFizzServer', () => { isClient = true; ReactDOMClient.hydrateRoot(container, , { onRecoverableError(error) { - Scheduler.log(error.message); + Scheduler.log(normalizeError(error.message)); }, }); @@ -2567,9 +2568,7 @@ describe('ReactDOMFizzServer', () => { await waitForAll([ 'Yay!', 'Hydration error', - 'There was an error while hydrating. Because the error happened ' + - 'outside of a Suspense boundary, the entire root will switch ' + - 'to client rendering.', + 'There was an error while hydrating.', ]); }).toErrorDev( 'An error occurred during hydration. The server HTML was replaced', @@ -2739,7 +2738,7 @@ describe('ReactDOMFizzServer', () => { isClient = true; ReactDOMClient.hydrateRoot(container, , { onRecoverableError(error) { - Scheduler.log(error.message); + Scheduler.log(normalizeError(error.message)); }, }); @@ -2748,7 +2747,7 @@ describe('ReactDOMFizzServer', () => { await waitForAll([ 'Yay!', 'Hydration error', - 'There was an error while hydrating this Suspense boundary. Switched to client rendering.', + 'There was an error while hydrating this Suspense boundary.', ]); expect(getVisibleChildren(container)).toEqual(
@@ -3194,7 +3193,7 @@ describe('ReactDOMFizzServer', () => { isClient = true; ReactDOMClient.hydrateRoot(container, , { onRecoverableError(error) { - Scheduler.log(error.message); + Scheduler.log(normalizeError(error.message)); }, }); @@ -3202,8 +3201,7 @@ describe('ReactDOMFizzServer', () => { // to client rendering. await waitForAll([ 'Hydration error', - 'There was an error while hydrating this Suspense boundary. Switched ' + - 'to client rendering.', + 'There was an error while hydrating this Suspense boundary.', ]); expect(getVisibleChildren(container)).toEqual(
@@ -3263,7 +3261,9 @@ describe('ReactDOMFizzServer', () => { const root = ReactDOMClient.createRoot(container, { onRecoverableError(error) { - Scheduler.log('Logged a recoverable error: ' + error.message); + Scheduler.log( + 'Logged a recoverable error: ' + normalizeError(error.message), + ); }, }); React.startTransition(() => { @@ -3339,7 +3339,9 @@ describe('ReactDOMFizzServer', () => { isClient = true; ReactDOMClient.hydrateRoot(container, , { onRecoverableError(error) { - Scheduler.log('Logged recoverable error: ' + error.message); + Scheduler.log( + 'Logged recoverable error: ' + normalizeError(error.message), + ); }, }); @@ -3349,11 +3351,11 @@ describe('ReactDOMFizzServer', () => { 'Logged recoverable error: Hydration error', 'Logged recoverable error: There was an error while hydrating this ' + - 'Suspense boundary. Switched to client rendering.', + 'Suspense boundary.', 'Logged recoverable error: Hydration error', 'Logged recoverable error: There was an error while hydrating this ' + - 'Suspense boundary. Switched to client rendering.', + 'Suspense boundary.', ]); }); @@ -4395,7 +4397,9 @@ describe('ReactDOMFizzServer', () => { const [ClientApp, clientResolve] = makeApp(); ReactDOMClient.hydrateRoot(container, , { onRecoverableError(error) { - Scheduler.log('Logged recoverable error: ' + error.message); + Scheduler.log( + 'Logged recoverable error: ' + normalizeError(error.message), + ); }, }); await waitForAll([]); @@ -4471,7 +4475,9 @@ describe('ReactDOMFizzServer', () => { const [ClientApp, clientResolve] = makeApp(); ReactDOMClient.hydrateRoot(container, , { onRecoverableError(error) { - Scheduler.log('Logged recoverable error: ' + error.message); + Scheduler.log( + 'Logged recoverable error: ' + normalizeError(error.message), + ); }, }); await waitForAll([]); @@ -4486,14 +4492,10 @@ describe('ReactDOMFizzServer', () => { // Now that the boundary resolves to it's children the hydration completes and discovers that there is a mismatch requiring // client-side rendering. await clientResolve(); - await expect(async () => { - await waitForAll([ - 'Logged recoverable error: Text content does not match server-rendered HTML.', - 'Logged recoverable error: There was an error while hydrating this Suspense boundary. Switched to client rendering.', - ]); - }).toErrorDev( - 'Warning: Text content did not match. Server: "initial" Client: "replaced', - ); + await waitForAll([ + "Logged recoverable error: Hydration failed because the server rendered HTML didn't match the client.", + 'Logged recoverable error: There was an error while hydrating this Suspense boundary.', + ]); expect(getVisibleChildren(container)).toEqual(

A

@@ -4539,12 +4541,14 @@ describe('ReactDOMFizzServer', () => { ReactDOMClient.hydrateRoot(container, , { onRecoverableError(error) { - Scheduler.log('Logged recoverable error: ' + error.message); + Scheduler.log( + 'Logged recoverable error: ' + normalizeError(error.message), + ); }, }); await waitForAll([ - 'Logged recoverable error: Text content does not match server-rendered HTML.', - 'Logged recoverable error: There was an error while hydrating this Suspense boundary. Switched to client rendering.', + "Logged recoverable error: Hydration failed because the server rendered HTML didn't match the client.", + 'Logged recoverable error: There was an error while hydrating this Suspense boundary.', ]); expect(getVisibleChildren(container)).toEqual( @@ -4556,21 +4560,7 @@ describe('ReactDOMFizzServer', () => { ); await waitForAll([]); - if (__DEV__) { - expect(mockError.mock.calls.length).toBe(1); - expect(mockError.mock.calls[0]).toEqual([ - 'Warning: Text content did not match. Server: "%s" Client: "%s"%s', - 'initial', - 'replaced', - '\n' + - ' in h2 (at **)\n' + - ' in Suspense (at **)\n' + - ' in div (at **)\n' + - ' in App (at **)', - ]); - } else { - expect(mockError.mock.calls.length).toBe(0); - } + expect(mockError.mock.calls.length).toBe(0); } finally { console.error = originalConsoleError; } @@ -4626,12 +4616,14 @@ describe('ReactDOMFizzServer', () => { ReactDOMClient.hydrateRoot(container, , { onRecoverableError(error) { - Scheduler.log('Logged recoverable error: ' + error.message); + Scheduler.log( + 'Logged recoverable error: ' + normalizeError(error.message), + ); }, }); await waitForAll([ 'Logged recoverable error: uh oh', - 'Logged recoverable error: There was an error while hydrating this Suspense boundary. Switched to client rendering.', + 'Logged recoverable error: There was an error while hydrating this Suspense boundary.', ]); expect(getVisibleChildren(container)).toEqual( @@ -4713,7 +4705,9 @@ describe('ReactDOMFizzServer', () => { ReactDOMClient.hydrateRoot(container, , { onRecoverableError(error) { - Scheduler.log('Logged recoverable error: ' + error.message); + Scheduler.log( + 'Logged recoverable error: ' + normalizeError(error.message), + ); }, }); await waitForAll([ @@ -4722,7 +4716,7 @@ describe('ReactDOMFizzServer', () => { // onRecoverableError because the UI recovered without surfacing the // error to the user. 'Logged recoverable error: first error', - 'Logged recoverable error: There was an error while hydrating this Suspense boundary. Switched to client rendering.', + 'Logged recoverable error: There was an error while hydrating this Suspense boundary.', ]); expect(mockError.mock.calls).toEqual([]); mockError.mockClear(); @@ -4830,7 +4824,9 @@ describe('ReactDOMFizzServer', () => { ReactDOMClient.hydrateRoot(container, , { onRecoverableError(error) { - Scheduler.log('Logged recoverable error: ' + error.message); + Scheduler.log( + 'Logged recoverable error: ' + normalizeError(error.message), + ); }, }); await waitForAll(['suspending']); @@ -4847,7 +4843,7 @@ describe('ReactDOMFizzServer', () => { await waitForAll([ 'throwing: first error', 'Logged recoverable error: first error', - 'Logged recoverable error: There was an error while hydrating this Suspense boundary. Switched to client rendering.', + 'Logged recoverable error: There was an error while hydrating this Suspense boundary.', ]); expect(getVisibleChildren(container)).toEqual(
@@ -4954,14 +4950,16 @@ describe('ReactDOMFizzServer', () => { ReactDOMClient.hydrateRoot(container, , { onRecoverableError(error) { - Scheduler.log('Logged recoverable error: ' + error.message); + Scheduler.log( + 'Logged recoverable error: ' + normalizeError(error.message), + ); }, }); await waitForAll([ 'throwing: first error', 'suspending', 'Logged recoverable error: first error', - 'Logged recoverable error: There was an error while hydrating this Suspense boundary. Switched to client rendering.', + 'Logged recoverable error: There was an error while hydrating this Suspense boundary.', ]); expect(mockError.mock.calls).toEqual([]); mockError.mockClear(); @@ -6341,13 +6339,7 @@ describe('ReactDOMFizzServer', () => { }); await expect(async () => { await waitForAll([]); - }).toErrorDev( - [ - 'Expected server HTML to contain a matching in the root', - 'An error occurred during hydration', - ], - {withoutStack: 1}, - ); + }).toErrorDev(['An error occurred during hydration'], {withoutStack: 1}); expect(errors.length).toEqual(2); expect(getVisibleChildren(container)).toEqual(); }); diff --git a/packages/react-dom/src/__tests__/ReactDOMFizzSuppressHydrationWarning-test.js b/packages/react-dom/src/__tests__/ReactDOMFizzSuppressHydrationWarning-test.js index 5fa719e3d36cc..36c3d4f804dd3 100644 --- a/packages/react-dom/src/__tests__/ReactDOMFizzSuppressHydrationWarning-test.js +++ b/packages/react-dom/src/__tests__/ReactDOMFizzSuppressHydrationWarning-test.js @@ -24,6 +24,15 @@ let hasErrored = false; let fatalError = undefined; let waitForAll; +function normalizeError(msg) { + // Take the first sentence to make it easier to assert on. + const idx = msg.indexOf('.'); + if (idx > -1) { + return msg.slice(0, idx + 1); + } + return msg; +} + describe('ReactDOMFizzServerHydrationWarning', () => { beforeEach(() => { jest.resetModules(); @@ -156,7 +165,7 @@ describe('ReactDOMFizzServerHydrationWarning', () => { ReactDOMClient.hydrateRoot(container, , { onRecoverableError(error) { // Don't miss a hydration error. There should be none. - Scheduler.log(error.message); + Scheduler.log(normalizeError(error.message)); }, }); await waitForAll([]); @@ -196,7 +205,7 @@ describe('ReactDOMFizzServerHydrationWarning', () => { ); ReactDOMClient.hydrateRoot(container, , { onRecoverableError(error) { - Scheduler.log(error.message); + Scheduler.log(normalizeError(error.message)); }, }); await waitForAll([]); @@ -237,17 +246,16 @@ describe('ReactDOMFizzServerHydrationWarning', () => { ); ReactDOMClient.hydrateRoot(container, , { onRecoverableError(error) { - Scheduler.log(error.message); + Scheduler.log(normalizeError(error.message)); }, }); await expect(async () => { await waitForAll([ - 'Hydration failed because the initial UI does not match what was rendered on the server.', - 'There was an error while hydrating. Because the error happened outside of a Suspense boundary, the entire root will switch to client rendering.', + "Hydration failed because the server rendered HTML didn't match the client.", + 'There was an error while hydrating.', ]); }).toErrorDev( [ - 'Expected server HTML to contain a matching in ', 'An error occurred during hydration. The server HTML was replaced with client content.', ], {withoutStack: 1}, @@ -282,7 +290,7 @@ describe('ReactDOMFizzServerHydrationWarning', () => { ); const root = ReactDOMClient.hydrateRoot(container, , { onRecoverableError(error) { - Scheduler.log(error.message); + Scheduler.log(normalizeError(error.message)); }, }); await waitForAll([]); @@ -326,17 +334,16 @@ describe('ReactDOMFizzServerHydrationWarning', () => { ); ReactDOMClient.hydrateRoot(container, , { onRecoverableError(error) { - Scheduler.log(error.message); + Scheduler.log(normalizeError(error.message)); }, }); await expect(async () => { await waitForAll([ - 'Hydration failed because the initial UI does not match what was rendered on the server.', - 'There was an error while hydrating. Because the error happened outside of a Suspense boundary, the entire root will switch to client rendering.', + "Hydration failed because the server rendered HTML didn't match the client.", + 'There was an error while hydrating.', ]); }).toErrorDev( [ - 'Did not expect server HTML to contain the text node "Server" in ', 'An error occurred during hydration. The server HTML was replaced with client content.', ], {withoutStack: 1}, @@ -374,17 +381,16 @@ describe('ReactDOMFizzServerHydrationWarning', () => { ); ReactDOMClient.hydrateRoot(container, , { onRecoverableError(error) { - Scheduler.log(error.message); + Scheduler.log(normalizeError(error.message)); }, }); await expect(async () => { await waitForAll([ - 'Hydration failed because the initial UI does not match what was rendered on the server.', - 'There was an error while hydrating. Because the error happened outside of a Suspense boundary, the entire root will switch to client rendering.', + "Hydration failed because the server rendered HTML didn't match the client.", + 'There was an error while hydrating.', ]); }).toErrorDev( [ - 'Expected server HTML to contain a matching text node for "Client" in .', 'An error occurred during hydration. The server HTML was replaced with client content.', ], {withoutStack: 1}, @@ -425,17 +431,16 @@ describe('ReactDOMFizzServerHydrationWarning', () => { ); ReactDOMClient.hydrateRoot(container, , { onRecoverableError(error) { - Scheduler.log(error.message); + Scheduler.log(normalizeError(error.message)); }, }); await expect(async () => { await waitForAll([ - 'Hydration failed because the initial UI does not match what was rendered on the server.', - 'There was an error while hydrating. Because the error happened outside of a Suspense boundary, the entire root will switch to client rendering.', + "Hydration failed because the server rendered HTML didn't match the client.", + 'There was an error while hydrating.', ]); }).toErrorDev( [ - 'Did not expect server HTML to contain the text node "Server" in .', 'An error occurred during hydration. The server HTML was replaced with client content.', ], {withoutStack: 1}, @@ -474,17 +479,16 @@ describe('ReactDOMFizzServerHydrationWarning', () => { ); ReactDOMClient.hydrateRoot(container, , { onRecoverableError(error) { - Scheduler.log(error.message); + Scheduler.log(normalizeError(error.message)); }, }); await expect(async () => { await waitForAll([ - 'Hydration failed because the initial UI does not match what was rendered on the server.', - 'There was an error while hydrating. Because the error happened outside of a Suspense boundary, the entire root will switch to client rendering.', + "Hydration failed because the server rendered HTML didn't match the client.", + 'There was an error while hydrating.', ]); }).toErrorDev( [ - 'Expected server HTML to contain a matching text node for "Client" in .', 'An error occurred during hydration. The server HTML was replaced with client content.', ], {withoutStack: 1}, @@ -527,7 +531,7 @@ describe('ReactDOMFizzServerHydrationWarning', () => { ); ReactDOMClient.hydrateRoot(container, , { onRecoverableError(error) { - Scheduler.log(error.message); + Scheduler.log(normalizeError(error.message)); }, }); await waitForAll([]); @@ -564,7 +568,7 @@ describe('ReactDOMFizzServerHydrationWarning', () => { ); ReactDOMClient.hydrateRoot(container, , { onRecoverableError(error) { - Scheduler.log(error.message); + Scheduler.log(normalizeError(error.message)); }, }); await waitForAll([]); @@ -597,17 +601,16 @@ describe('ReactDOMFizzServerHydrationWarning', () => { ); ReactDOMClient.hydrateRoot(container, , { onRecoverableError(error) { - Scheduler.log(error.message); + Scheduler.log(normalizeError(error.message)); }, }); await expect(async () => { await waitForAll([ - 'Hydration failed because the initial UI does not match what was rendered on the server.', - 'There was an error while hydrating. Because the error happened outside of a Suspense boundary, the entire root will switch to client rendering.', + "Hydration failed because the server rendered HTML didn't match the client.", + 'There was an error while hydrating.', ]); }).toErrorDev( [ - 'Expected server HTML to contain a matching

in

.', 'An error occurred during hydration. The server HTML was replaced with client content.', ], {withoutStack: 1}, @@ -643,17 +646,16 @@ describe('ReactDOMFizzServerHydrationWarning', () => { ); ReactDOMClient.hydrateRoot(container, , { onRecoverableError(error) { - Scheduler.log(error.message); + Scheduler.log(normalizeError(error.message)); }, }); await expect(async () => { await waitForAll([ - 'Hydration failed because the initial UI does not match what was rendered on the server.', - 'There was an error while hydrating. Because the error happened outside of a Suspense boundary, the entire root will switch to client rendering.', + "Hydration failed because the server rendered HTML didn't match the client.", + 'There was an error while hydrating.', ]); }).toErrorDev( [ - 'Did not expect server HTML to contain a

in

.', 'An error occurred during hydration. The server HTML was replaced with client content.', ], {withoutStack: 1}, diff --git a/packages/react-dom/src/__tests__/ReactDOMFloat-test.js b/packages/react-dom/src/__tests__/ReactDOMFloat-test.js index fae926c622564..7b8e27654c6a6 100644 --- a/packages/react-dom/src/__tests__/ReactDOMFloat-test.js +++ b/packages/react-dom/src/__tests__/ReactDOMFloat-test.js @@ -6480,7 +6480,6 @@ body { await waitForAll([]); }).toErrorDev( [ - 'Warning: Text content did not match. Server: "server" Client: "client"', 'Warning: An error occurred during hydration. The server HTML was replaced with client content.', ], {withoutStack: 1}, @@ -8270,7 +8269,6 @@ background-color: green; await waitForAll([]); }).toErrorDev( [ - 'Warning: Text content did not match. Server: "server" Client: "client"', 'Warning: An error occurred during hydration. The server HTML was replaced with client content.', ], {withoutStack: 1}, diff --git a/packages/react-dom/src/__tests__/ReactDOMHydrationDiff-test.js b/packages/react-dom/src/__tests__/ReactDOMHydrationDiff-test.js index ef5507744b4ed..c134ab973d58c 100644 --- a/packages/react-dom/src/__tests__/ReactDOMHydrationDiff-test.js +++ b/packages/react-dom/src/__tests__/ReactDOMHydrationDiff-test.js @@ -82,12 +82,19 @@ describe('ReactDOMServerHydration', () => { } expect(testMismatch(Mismatch)).toMatchInlineSnapshot(` [ - "Warning: Text content did not match. Server: "server" Client: "client" - in main (at **) - in div (at **) - in Mismatch (at **)", "Warning: An error occurred during hydration. The server HTML was replaced with client content.", - "Caught [Text content does not match server-rendered HTML.]", + "Caught [Hydration failed because the server rendered HTML didn't match the client. As a result this tree will be regenerated on the client. This can happen if a SSR-ed Client Component used: + + - A server/client branch \`if (typeof window !== 'undefined')\`. + - Variable input such as \`Date.now()\` or \`Math.random()\` which changes each time it's called. + - Date formatting in a user's locale which doesn't match the server. + - External changing data without sending a snapshot of it along with the HTML. + - Invalid HTML tag nesting. + + It can also happen if the client has a browser extension installed which messes with the HTML before React loaded. + + https://react.dev/link/hydration-mismatch + ]", "Caught [There was an error while hydrating. Because the error happened outside of a Suspense boundary, the entire root will switch to client rendering.]", ] `); @@ -109,11 +116,19 @@ describe('ReactDOMServerHydration', () => { /* eslint-disable no-irregular-whitespace */ expect(testMismatch(Mismatch)).toMatchInlineSnapshot(` [ - "Warning: Text content did not match. Server: "This markup contains an nbsp entity:   server text" Client: "This markup contains an nbsp entity:   client text" - in div (at **) - in Mismatch (at **)", "Warning: An error occurred during hydration. The server HTML was replaced with client content.", - "Caught [Text content does not match server-rendered HTML.]", + "Caught [Hydration failed because the server rendered HTML didn't match the client. As a result this tree will be regenerated on the client. This can happen if a SSR-ed Client Component used: + + - A server/client branch \`if (typeof window !== 'undefined')\`. + - Variable input such as \`Date.now()\` or \`Math.random()\` which changes each time it's called. + - Date formatting in a user's locale which doesn't match the server. + - External changing data without sending a snapshot of it along with the HTML. + - Invalid HTML tag nesting. + + It can also happen if the client has a browser extension installed which messes with the HTML before React loaded. + + https://react.dev/link/hydration-mismatch + ]", "Caught [There was an error while hydrating. Because the error happened outside of a Suspense boundary, the entire root will switch to client rendering.]", ] `); @@ -138,10 +153,18 @@ describe('ReactDOMServerHydration', () => { } expect(testMismatch(Mismatch)).toMatchInlineSnapshot(` [ - "Warning: Prop \`dangerouslySetInnerHTML\` did not match. Server: {"__html":"server"} Client: {"__html":"client"} - in main (at **) - in div (at **) - in Mismatch (at **)", + "Warning: A tree hydrated but some attributes of the server rendered HTML didn't match the client properties. This won't be patched up. This can happen if a SSR-ed Client Component used: + + - A server/client branch \`if (typeof window !== 'undefined')\`. + - Variable input such as \`Date.now()\` or \`Math.random()\` which changes each time it's called. + - Date formatting in a user's locale which doesn't match the server. + - External changing data without sending a snapshot of it along with the HTML. + - Invalid HTML tag nesting. + + It can also happen if the client has a browser extension installed which messes with the HTML before React loaded. + + https://react.dev/link/hydration-mismatch + ", ] `); }); @@ -162,10 +185,18 @@ describe('ReactDOMServerHydration', () => { } expect(testMismatch(Mismatch)).toMatchInlineSnapshot(` [ - "Warning: Prop \`className\` did not match. Server: "child server" Client: "child client" - in main (at **) - in div (at **) - in Mismatch (at **)", + "Warning: A tree hydrated but some attributes of the server rendered HTML didn't match the client properties. This won't be patched up. This can happen if a SSR-ed Client Component used: + + - A server/client branch \`if (typeof window !== 'undefined')\`. + - Variable input such as \`Date.now()\` or \`Math.random()\` which changes each time it's called. + - Date formatting in a user's locale which doesn't match the server. + - External changing data without sending a snapshot of it along with the HTML. + - Invalid HTML tag nesting. + + It can also happen if the client has a browser extension installed which messes with the HTML before React loaded. + + https://react.dev/link/hydration-mismatch + ", ] `); }); @@ -185,10 +216,18 @@ describe('ReactDOMServerHydration', () => { } expect(testMismatch(Mismatch)).toMatchInlineSnapshot(` [ - "Warning: Prop \`tabIndex\` did not match. Server: null Client: 1 - in main (at **) - in div (at **) - in Mismatch (at **)", + "Warning: A tree hydrated but some attributes of the server rendered HTML didn't match the client properties. This won't be patched up. This can happen if a SSR-ed Client Component used: + + - A server/client branch \`if (typeof window !== 'undefined')\`. + - Variable input such as \`Date.now()\` or \`Math.random()\` which changes each time it's called. + - Date formatting in a user's locale which doesn't match the server. + - External changing data without sending a snapshot of it along with the HTML. + - Invalid HTML tag nesting. + + It can also happen if the client has a browser extension installed which messes with the HTML before React loaded. + + https://react.dev/link/hydration-mismatch + ", ] `); }); @@ -208,10 +247,18 @@ describe('ReactDOMServerHydration', () => { } expect(testMismatch(Mismatch)).toMatchInlineSnapshot(` [ - "Warning: Extra attribute from the server: tabindex - in main (at **) - in div (at **) - in Mismatch (at **)", + "Warning: A tree hydrated but some attributes of the server rendered HTML didn't match the client properties. This won't be patched up. This can happen if a SSR-ed Client Component used: + + - A server/client branch \`if (typeof window !== 'undefined')\`. + - Variable input such as \`Date.now()\` or \`Math.random()\` which changes each time it's called. + - Date formatting in a user's locale which doesn't match the server. + - External changing data without sending a snapshot of it along with the HTML. + - Invalid HTML tag nesting. + + It can also happen if the client has a browser extension installed which messes with the HTML before React loaded. + + https://react.dev/link/hydration-mismatch + ", ] `); }); @@ -231,10 +278,18 @@ describe('ReactDOMServerHydration', () => { } expect(testMismatch(Mismatch)).toMatchInlineSnapshot(` [ - "Warning: Prop \`tabIndex\` did not match. Server: null Client: 1 - in main (at **) - in div (at **) - in Mismatch (at **)", + "Warning: A tree hydrated but some attributes of the server rendered HTML didn't match the client properties. This won't be patched up. This can happen if a SSR-ed Client Component used: + + - A server/client branch \`if (typeof window !== 'undefined')\`. + - Variable input such as \`Date.now()\` or \`Math.random()\` which changes each time it's called. + - Date formatting in a user's locale which doesn't match the server. + - External changing data without sending a snapshot of it along with the HTML. + - Invalid HTML tag nesting. + + It can also happen if the client has a browser extension installed which messes with the HTML before React loaded. + + https://react.dev/link/hydration-mismatch + ", ] `); }); @@ -255,10 +310,18 @@ describe('ReactDOMServerHydration', () => { } expect(testMismatch(Mismatch)).toMatchInlineSnapshot(` [ - "Warning: Prop \`style\` did not match. Server: {"opacity":"0"} Client: {"opacity":1} - in main (at **) - in div (at **) - in Mismatch (at **)", + "Warning: A tree hydrated but some attributes of the server rendered HTML didn't match the client properties. This won't be patched up. This can happen if a SSR-ed Client Component used: + + - A server/client branch \`if (typeof window !== 'undefined')\`. + - Variable input such as \`Date.now()\` or \`Math.random()\` which changes each time it's called. + - Date formatting in a user's locale which doesn't match the server. + - External changing data without sending a snapshot of it along with the HTML. + - Invalid HTML tag nesting. + + It can also happen if the client has a browser extension installed which messes with the HTML before React loaded. + + https://react.dev/link/hydration-mismatch + ", ] `); }); @@ -276,16 +339,23 @@ describe('ReactDOMServerHydration', () => { ); } expect(testMismatch(Mismatch)).toMatchInlineSnapshot(` - [ - "Warning: Expected server HTML to contain a matching
in
. - in main (at **) - in div (at **) - in Mismatch (at **)", - "Warning: An error occurred during hydration. The server HTML was replaced with client content.", - "Caught [Hydration failed because the initial UI does not match what was rendered on the server.]", - "Caught [There was an error while hydrating. Because the error happened outside of a Suspense boundary, the entire root will switch to client rendering.]", - ] - `); + [ + "Warning: An error occurred during hydration. The server HTML was replaced with client content.", + "Caught [Hydration failed because the server rendered HTML didn't match the client. As a result this tree will be regenerated on the client. This can happen if a SSR-ed Client Component used: + + - A server/client branch \`if (typeof window !== 'undefined')\`. + - Variable input such as \`Date.now()\` or \`Math.random()\` which changes each time it's called. + - Date formatting in a user's locale which doesn't match the server. + - External changing data without sending a snapshot of it along with the HTML. + - Invalid HTML tag nesting. + + It can also happen if the client has a browser extension installed which messes with the HTML before React loaded. + + https://react.dev/link/hydration-mismatch + ]", + "Caught [There was an error while hydrating. Because the error happened outside of a Suspense boundary, the entire root will switch to client rendering.]", + ] + `); }); // @gate __DEV__ @@ -300,16 +370,23 @@ describe('ReactDOMServerHydration', () => { ); } expect(testMismatch(Mismatch)).toMatchInlineSnapshot(` - [ - "Warning: Expected server HTML to contain a matching
in
. - in header (at **) - in div (at **) - in Mismatch (at **)", - "Warning: An error occurred during hydration. The server HTML was replaced with client content.", - "Caught [Hydration failed because the initial UI does not match what was rendered on the server.]", - "Caught [There was an error while hydrating. Because the error happened outside of a Suspense boundary, the entire root will switch to client rendering.]", - ] - `); + [ + "Warning: An error occurred during hydration. The server HTML was replaced with client content.", + "Caught [Hydration failed because the server rendered HTML didn't match the client. As a result this tree will be regenerated on the client. This can happen if a SSR-ed Client Component used: + + - A server/client branch \`if (typeof window !== 'undefined')\`. + - Variable input such as \`Date.now()\` or \`Math.random()\` which changes each time it's called. + - Date formatting in a user's locale which doesn't match the server. + - External changing data without sending a snapshot of it along with the HTML. + - Invalid HTML tag nesting. + + It can also happen if the client has a browser extension installed which messes with the HTML before React loaded. + + https://react.dev/link/hydration-mismatch + ]", + "Caught [There was an error while hydrating. Because the error happened outside of a Suspense boundary, the entire root will switch to client rendering.]", + ] + `); }); // @gate __DEV__ @@ -324,16 +401,23 @@ describe('ReactDOMServerHydration', () => { ); } expect(testMismatch(Mismatch)).toMatchInlineSnapshot(` - [ - "Warning: Expected server HTML to contain a matching
in
. - in main (at **) - in div (at **) - in Mismatch (at **)", - "Warning: An error occurred during hydration. The server HTML was replaced with client content.", - "Caught [Hydration failed because the initial UI does not match what was rendered on the server.]", - "Caught [There was an error while hydrating. Because the error happened outside of a Suspense boundary, the entire root will switch to client rendering.]", - ] - `); + [ + "Warning: An error occurred during hydration. The server HTML was replaced with client content.", + "Caught [Hydration failed because the server rendered HTML didn't match the client. As a result this tree will be regenerated on the client. This can happen if a SSR-ed Client Component used: + + - A server/client branch \`if (typeof window !== 'undefined')\`. + - Variable input such as \`Date.now()\` or \`Math.random()\` which changes each time it's called. + - Date formatting in a user's locale which doesn't match the server. + - External changing data without sending a snapshot of it along with the HTML. + - Invalid HTML tag nesting. + + It can also happen if the client has a browser extension installed which messes with the HTML before React loaded. + + https://react.dev/link/hydration-mismatch + ]", + "Caught [There was an error while hydrating. Because the error happened outside of a Suspense boundary, the entire root will switch to client rendering.]", + ] + `); }); // @gate __DEV__ @@ -348,16 +432,23 @@ describe('ReactDOMServerHydration', () => { ); } expect(testMismatch(Mismatch)).toMatchInlineSnapshot(` - [ - "Warning: Expected server HTML to contain a matching