diff --git a/packages/react-dom/src/__tests__/ReactDOMFizzServer-test.js b/packages/react-dom/src/__tests__/ReactDOMFizzServer-test.js index 1caaeca68d293..5eea362a3e8cf 100644 --- a/packages/react-dom/src/__tests__/ReactDOMFizzServer-test.js +++ b/packages/react-dom/src/__tests__/ReactDOMFizzServer-test.js @@ -4423,6 +4423,7 @@ describe('ReactDOMFizzServer', () => { ); }); + // @gate favorSafetyOverHydrationPerf it('#24384: Suspending should halt hydration warnings but still emit hydration warnings after unsuspending if mismatches are genuine', async () => { const makeApp = () => { let resolve, resolved; @@ -4506,6 +4507,7 @@ describe('ReactDOMFizzServer', () => { await waitForAll([]); }); + // @gate favorSafetyOverHydrationPerf it('only warns once on hydration mismatch while within a suspense boundary', async () => { const originalConsoleError = console.error; const mockError = jest.fn(); diff --git a/packages/react-dom/src/__tests__/ReactDOMFloat-test.js b/packages/react-dom/src/__tests__/ReactDOMFloat-test.js index 7b8e27654c6a6..b0ea66c592a59 100644 --- a/packages/react-dom/src/__tests__/ReactDOMFloat-test.js +++ b/packages/react-dom/src/__tests__/ReactDOMFloat-test.js @@ -6446,6 +6446,7 @@ body { ); }); + // @gate favorSafetyOverHydrationPerf it('retains styles even when a new html, head, and/body mount', async () => { await act(() => { const {pipe} = renderToPipeableStream( @@ -8230,6 +8231,7 @@ background-color: green; ]); }); + // @gate favorSafetyOverHydrationPerf it('can render a title before a singleton even if that singleton clears its contents', async () => { await act(() => { const {pipe} = renderToPipeableStream( diff --git a/packages/react-dom/src/__tests__/ReactDOMHydrationDiff-test.js b/packages/react-dom/src/__tests__/ReactDOMHydrationDiff-test.js index a52bb65181c90..f05332d6f1b8b 100644 --- a/packages/react-dom/src/__tests__/ReactDOMHydrationDiff-test.js +++ b/packages/react-dom/src/__tests__/ReactDOMHydrationDiff-test.js @@ -80,30 +80,55 @@ describe('ReactDOMServerHydration', () => { ); } - expect(testMismatch(Mismatch)).toMatchInlineSnapshot(` - [ - "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: + if (gate(flags => flags.favorSafetyOverHydrationPerf)) { + expect(testMismatch(Mismatch)).toMatchInlineSnapshot(` + [ + "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 + + +
+
+ + client + - 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.]", + ] + `); + } else { + expect(testMismatch(Mismatch)).toMatchInlineSnapshot(` + [ + "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. + - 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. + 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 + https://react.dev/link/hydration-mismatch - -
-
- + client - - 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.]", - ] - `); + +
+
+ + client + - server + ", + ] + `); + } }); // @gate __DEV__ @@ -120,29 +145,53 @@ describe('ReactDOMServerHydration', () => { } /* eslint-disable no-irregular-whitespace */ - expect(testMismatch(Mismatch)).toMatchInlineSnapshot(` - [ - "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: + if (gate(flags => flags.favorSafetyOverHydrationPerf)) { + expect(testMismatch(Mismatch)).toMatchInlineSnapshot(` + [ + "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. + - 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. + 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 + https://react.dev/link/hydration-mismatch - -
- + This markup contains an nbsp entity:   client text - - This markup contains an nbsp entity:   server text - ]", - "Caught [There was an error while hydrating. Because the error happened outside of a Suspense boundary, the entire root will switch to client rendering.]", - ] - `); + +
+ + This markup contains an nbsp entity:   client text + - This markup contains an nbsp entity:   server text + ]", + "Caught [There was an error while hydrating. Because the error happened outside of a Suspense boundary, the entire root will switch to client rendering.]", + ] + `); + } else { + expect(testMismatch(Mismatch)).toMatchInlineSnapshot(` + [ + "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 + + +
+ + This markup contains an nbsp entity:   client text + - This markup contains an nbsp entity:   server text + ", + ] + `); + } /* eslint-enable no-irregular-whitespace */ }); @@ -549,29 +598,53 @@ describe('ReactDOMServerHydration', () => { function Mismatch({isClient}) { return
{isClient && 'only'}
; } - expect(testMismatch(Mismatch)).toMatchInlineSnapshot(` - [ - "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: + if (gate(flags => flags.favorSafetyOverHydrationPerf)) { + expect(testMismatch(Mismatch)).toMatchInlineSnapshot(` + [ + "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 + + +
+ + only + - + ]", + "Caught [There was an error while hydrating. Because the error happened outside of a Suspense boundary, the entire root will switch to client rendering.]", + ] + `); + } else { + expect(testMismatch(Mismatch)).toMatchInlineSnapshot(` + [ + "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. + - 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. + 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 + https://react.dev/link/hydration-mismatch - -
- + only - - - ]", - "Caught [There was an error while hydrating. Because the error happened outside of a Suspense boundary, the entire root will switch to client rendering.]", - ] - `); + +
+ + only + - + ", + ] + `); + } }); // @gate __DEV__ diff --git a/packages/react-dom/src/__tests__/ReactDOMServerPartialHydration-test.internal.js b/packages/react-dom/src/__tests__/ReactDOMServerPartialHydration-test.internal.js index 0953b0b3536dc..d27f5729685b1 100644 --- a/packages/react-dom/src/__tests__/ReactDOMServerPartialHydration-test.internal.js +++ b/packages/react-dom/src/__tests__/ReactDOMServerPartialHydration-test.internal.js @@ -3816,6 +3816,7 @@ describe('ReactDOMServerPartialHydration', () => { ); }); + // @gate favorSafetyOverHydrationPerf it("falls back to client rendering when there's a text mismatch (direct text child)", async () => { function DirectTextChild({text}) { return
{text}
; @@ -3845,6 +3846,7 @@ describe('ReactDOMServerPartialHydration', () => { ]); }); + // @gate favorSafetyOverHydrationPerf it("falls back to client rendering when there's a text mismatch (text child with siblings)", async () => { function Sibling() { return 'Sibling'; diff --git a/packages/react-dom/src/__tests__/ReactRenderDocument-test.js b/packages/react-dom/src/__tests__/ReactRenderDocument-test.js index 238cc420ada74..8467ceb3a1deb 100644 --- a/packages/react-dom/src/__tests__/ReactRenderDocument-test.js +++ b/packages/react-dom/src/__tests__/ReactRenderDocument-test.js @@ -276,6 +276,9 @@ describe('rendering React components at document', () => { ); const testDocument = getTestDocument(markup); + const favorSafetyOverHydrationPerf = gate( + flags => flags.favorSafetyOverHydrationPerf, + ); expect(() => { ReactDOM.flushSync(() => { ReactDOMClient.hydrateRoot( @@ -291,19 +294,29 @@ describe('rendering React components at document', () => { ); }); }).toErrorDev( - [ - 'Warning: An error occurred during hydration. The server HTML was replaced with client content.', - ], + favorSafetyOverHydrationPerf + ? [ + 'Warning: An error occurred during hydration. The server HTML was replaced with client content.', + ] + : [ + "Warning: A tree hydrated but some attributes of the server rendered HTML didn't match the client properties.", + ], { withoutStack: 1, }, ); - assertLog([ - "Log recoverable error: Hydration failed because the server rendered HTML didn't match the client.", - 'Log recoverable error: There was an error while hydrating.', - ]); - expect(testDocument.body.innerHTML).toBe('Hello world'); + assertLog( + favorSafetyOverHydrationPerf + ? [ + "Log recoverable error: Hydration failed because the server rendered HTML didn't match the client.", + 'Log recoverable error: There was an error while hydrating.', + ] + : [], + ); + expect(testDocument.body.innerHTML).toBe( + favorSafetyOverHydrationPerf ? 'Hello world' : 'Goodbye world', + ); }); it('should render w/ no markup to full document', async () => { diff --git a/packages/react-dom/src/__tests__/ReactServerRenderingHydration-test.js b/packages/react-dom/src/__tests__/ReactServerRenderingHydration-test.js index 5f234795e120e..4772be4f01862 100644 --- a/packages/react-dom/src/__tests__/ReactServerRenderingHydration-test.js +++ b/packages/react-dom/src/__tests__/ReactServerRenderingHydration-test.js @@ -123,6 +123,9 @@ describe('ReactDOMServerHydration', () => { // Now simulate a situation where the app is not idempotent. React should // warn but do the right thing. element.innerHTML = lastMarkup; + const favorSafetyOverHydrationPerf = gate( + flags => flags.favorSafetyOverHydrationPerf, + ); await expect(async () => { root = await act(() => { return ReactDOMClient.hydrateRoot( @@ -139,14 +142,22 @@ describe('ReactDOMServerHydration', () => { ); }); }).toErrorDev( - [ - 'An error occurred during hydration. The server HTML was replaced with client content.', - ], + favorSafetyOverHydrationPerf + ? [ + 'An error occurred during hydration. The server HTML was replaced with client content.', + ] + : [ + " A tree hydrated but some attributes of the server rendered HTML didn't match the client properties.", + ], {withoutStack: 1}, ); expect(mountCount).toEqual(4); expect(element.innerHTML.length > 0).toBe(true); - expect(element.innerHTML).not.toEqual(lastMarkup); + if (favorSafetyOverHydrationPerf) { + expect(element.innerHTML).not.toEqual(lastMarkup); + } else { + expect(element.innerHTML).toEqual(lastMarkup); + } // Ensure the events system works after markup mismatch. expect(numClicks).toEqual(1); @@ -212,6 +223,9 @@ describe('ReactDOMServerHydration', () => { const onFocusAfterHydration = jest.fn(); element.firstChild.focus = onFocusBeforeHydration; + const favorSafetyOverHydrationPerf = gate( + flags => flags.favorSafetyOverHydrationPerf, + ); await expect(async () => { await act(() => { ReactDOMClient.hydrateRoot( @@ -223,9 +237,13 @@ describe('ReactDOMServerHydration', () => { ); }); }).toErrorDev( - [ - 'An error occurred during hydration. The server HTML was replaced with client content.', - ], + favorSafetyOverHydrationPerf + ? [ + 'An error occurred during hydration. The server HTML was replaced with client content.', + ] + : [ + "A tree hydrated but some attributes of the server rendered HTML didn't match the client properties.", + ], {withoutStack: 1}, ); @@ -514,6 +532,9 @@ describe('ReactDOMServerHydration', () => { ); domElement.innerHTML = markup; + const favorSafetyOverHydrationPerf = gate( + flags => flags.favorSafetyOverHydrationPerf, + ); await expect(async () => { await act(() => { ReactDOMClient.hydrateRoot( @@ -524,14 +545,22 @@ describe('ReactDOMServerHydration', () => { {onRecoverableError: error => {}}, ); }); - - expect(domElement.innerHTML).not.toEqual(markup); }).toErrorDev( - [ - 'An error occurred during hydration. The server HTML was replaced with client content.', - ], + favorSafetyOverHydrationPerf + ? [ + 'An error occurred during hydration. The server HTML was replaced with client content.', + ] + : [ + " A tree hydrated but some attributes of the server rendered HTML didn't match the client properties.", + ], {withoutStack: 1}, ); + + if (favorSafetyOverHydrationPerf) { + expect(domElement.innerHTML).not.toEqual(markup); + } else { + expect(domElement.innerHTML).toEqual(markup); + } }); it('should warn if innerHTML mismatches with dangerouslySetInnerHTML=undefined on the client', async () => { diff --git a/packages/react-dom/src/__tests__/utils/ReactDOMServerIntegrationTestUtils.js b/packages/react-dom/src/__tests__/utils/ReactDOMServerIntegrationTestUtils.js index 2f872492fa022..d882e52e08998 100644 --- a/packages/react-dom/src/__tests__/utils/ReactDOMServerIntegrationTestUtils.js +++ b/packages/react-dom/src/__tests__/utils/ReactDOMServerIntegrationTestUtils.js @@ -17,10 +17,12 @@ module.exports = function (initModules) { let ReactDOMClient; let ReactDOMServer; let act; + let ReactFeatureFlags; function resetModules() { ({ReactDOM, ReactDOMClient, ReactDOMServer} = initModules()); act = require('internal-test-utils').act; + ReactFeatureFlags = require('shared/ReactFeatureFlags'); } function shouldUseDocument(reactElement) { @@ -276,8 +278,10 @@ module.exports = function (initModules) { const cleanTextContent = (cleanContainer.lastChild && cleanContainer.lastChild.textContent) || ''; - // The only guarantee is that text content has been patched up if needed. - expect(hydratedTextContent).toBe(cleanTextContent); + if (ReactFeatureFlags.favorSafetyOverHydrationPerf) { + // The only guarantee is that text content has been patched up if needed. + expect(hydratedTextContent).toBe(cleanTextContent); + } // Abort any further expects. All bets are off at this point. throw new BadMarkupExpected(); diff --git a/packages/react-reconciler/src/ReactFiberHydrationContext.js b/packages/react-reconciler/src/ReactFiberHydrationContext.js index 2ddc7d2514321..ce7f4be96e42a 100644 --- a/packages/react-reconciler/src/ReactFiberHydrationContext.js +++ b/packages/react-reconciler/src/ReactFiberHydrationContext.js @@ -27,6 +27,7 @@ import { HostRoot, SuspenseComponent, } from './ReactWorkTags'; +import {favorSafetyOverHydrationPerf} from 'shared/ReactFeatureFlags'; import {createFiberFromDehydratedFragment} from './ReactFiber'; import { @@ -472,7 +473,7 @@ function prepareToHydrateHostInstance( hostContext, fiber, ); - if (!didHydrate) { + if (!didHydrate && favorSafetyOverHydrationPerf) { throwOnHydrationMismatch(fiber); } } @@ -538,7 +539,7 @@ function prepareToHydrateHostTextInstance(fiber: Fiber): void { fiber, parentProps, ); - if (!didHydrate) { + if (!didHydrate && favorSafetyOverHydrationPerf) { throwOnHydrationMismatch(fiber); } } diff --git a/packages/shared/ReactFeatureFlags.js b/packages/shared/ReactFeatureFlags.js index fc4c241c2a238..065391c504437 100644 --- a/packages/shared/ReactFeatureFlags.js +++ b/packages/shared/ReactFeatureFlags.js @@ -30,6 +30,7 @@ export const enableComponentStackLocations = true; // ----------------------------------------------------------------------------- // TODO: Finish rolling out in www +export const favorSafetyOverHydrationPerf = true; export const enableAsyncActions = true; // Need to remove didTimeout argument from Scheduler before landing diff --git a/packages/shared/forks/ReactFeatureFlags.native-fb.js b/packages/shared/forks/ReactFeatureFlags.native-fb.js index 642af8be74d28..7231aa51da3b0 100644 --- a/packages/shared/forks/ReactFeatureFlags.native-fb.js +++ b/packages/shared/forks/ReactFeatureFlags.native-fb.js @@ -65,6 +65,7 @@ export const enableSuspenseAvoidThisFallbackFizz = false; export const enableCPUSuspense = true; export const enableUseMemoCacheHook = true; export const enableUseEffectEventHook = false; +export const favorSafetyOverHydrationPerf = true; export const enableLegacyFBSupport = false; export const enableFilterEmptyStringAttributesDOM = true; export const enableGetInspectorDataForInstanceInProduction = true; diff --git a/packages/shared/forks/ReactFeatureFlags.native-oss.js b/packages/shared/forks/ReactFeatureFlags.native-oss.js index c03dd5ac125d8..bb771e65603df 100644 --- a/packages/shared/forks/ReactFeatureFlags.native-oss.js +++ b/packages/shared/forks/ReactFeatureFlags.native-oss.js @@ -87,6 +87,7 @@ export const disableTextareaChildren = false; export const enableSuspenseAvoidThisFallback = false; export const enableSuspenseAvoidThisFallbackFizz = false; export const enableUseEffectEventHook = false; +export const favorSafetyOverHydrationPerf = true; export const enableLegacyFBSupport = false; export const enableFilterEmptyStringAttributesDOM = true; export const enableGetInspectorDataForInstanceInProduction = false; diff --git a/packages/shared/forks/ReactFeatureFlags.test-renderer.js b/packages/shared/forks/ReactFeatureFlags.test-renderer.js index d1143a36ba998..b4bbcc58fcec1 100644 --- a/packages/shared/forks/ReactFeatureFlags.test-renderer.js +++ b/packages/shared/forks/ReactFeatureFlags.test-renderer.js @@ -39,6 +39,7 @@ export const enableSuspenseAvoidThisFallbackFizz = false; export const enableCPUSuspense = false; export const enableUseMemoCacheHook = true; export const enableUseEffectEventHook = false; +export const favorSafetyOverHydrationPerf = true; export const enableComponentStackLocations = true; export const enableLegacyFBSupport = false; export const enableFilterEmptyStringAttributesDOM = true; diff --git a/packages/shared/forks/ReactFeatureFlags.test-renderer.native.js b/packages/shared/forks/ReactFeatureFlags.test-renderer.native.js index b61b27ea3afb7..f6b042b852557 100644 --- a/packages/shared/forks/ReactFeatureFlags.test-renderer.native.js +++ b/packages/shared/forks/ReactFeatureFlags.test-renderer.native.js @@ -45,6 +45,7 @@ export const enableSuspenseAvoidThisFallbackFizz = false; export const enableCPUSuspense = false; export const enableUseMemoCacheHook = true; export const enableUseEffectEventHook = false; +export const favorSafetyOverHydrationPerf = true; export const enableUseRefAccessWarning = false; export const enableInfiniteRenderLoopDetection = false; export const enableRenderableContext = false; diff --git a/packages/shared/forks/ReactFeatureFlags.test-renderer.www.js b/packages/shared/forks/ReactFeatureFlags.test-renderer.www.js index 7cb09b6a616aa..418c3389f8da7 100644 --- a/packages/shared/forks/ReactFeatureFlags.test-renderer.www.js +++ b/packages/shared/forks/ReactFeatureFlags.test-renderer.www.js @@ -41,6 +41,7 @@ export const enableSuspenseAvoidThisFallbackFizz = false; export const enableCPUSuspense = false; export const enableUseMemoCacheHook = true; export const enableUseEffectEventHook = false; +export const favorSafetyOverHydrationPerf = true; export const enableComponentStackLocations = true; export const enableLegacyFBSupport = false; export const enableFilterEmptyStringAttributesDOM = true; diff --git a/packages/shared/forks/ReactFeatureFlags.www.js b/packages/shared/forks/ReactFeatureFlags.www.js index fc839d73ce3ef..54ce253de705f 100644 --- a/packages/shared/forks/ReactFeatureFlags.www.js +++ b/packages/shared/forks/ReactFeatureFlags.www.js @@ -60,6 +60,8 @@ export const enableUseEffectEventHook = true; export const enableFilterEmptyStringAttributesDOM = true; export const enableAsyncActions = true; +export const favorSafetyOverHydrationPerf = false; + // Logs additional User Timing API marks for use with an experimental profiling tool. export const enableSchedulingProfiler: boolean = __PROFILE__ && dynamicFeatureFlags.enableSchedulingProfiler;