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__/ReactDOMServerIntegrationAttributes-test.js b/packages/react-dom/src/__tests__/ReactDOMServerIntegrationAttributes-test.js index 1613ef66994ef..2a911b3d501e7 100644 --- a/packages/react-dom/src/__tests__/ReactDOMServerIntegrationAttributes-test.js +++ b/packages/react-dom/src/__tests__/ReactDOMServerIntegrationAttributes-test.js @@ -372,10 +372,12 @@ describe('ReactDOMServerIntegration', () => { expect(e.getAttribute('ref')).toBe(null); }); - itRenders('no children attribute', async render => { - const e = await render(React.createElement('div', {}, 'foo')); - expect(e.getAttribute('children')).toBe(null); - }); + if (ReactFeatureFlags.favorSafetyOverHydrationPerf) { + itRenders('no children attribute', async render => { + const e = await render(React.createElement('div', {}, 'foo')); + expect(e.getAttribute('children')).toBe(null); + }); + } itRenders('no key attribute', async render => { const e = await render(
); diff --git a/packages/react-dom/src/__tests__/ReactDOMServerIntegrationBasic-test.js b/packages/react-dom/src/__tests__/ReactDOMServerIntegrationBasic-test.js index 266bae3d5eeaa..c0ffcb2adf989 100644 --- a/packages/react-dom/src/__tests__/ReactDOMServerIntegrationBasic-test.js +++ b/packages/react-dom/src/__tests__/ReactDOMServerIntegrationBasic-test.js @@ -11,6 +11,7 @@ 'use strict'; const ReactDOMServerIntegrationUtils = require('./utils/ReactDOMServerIntegrationTestUtils'); +const ReactFeatureFlags = require('shared/ReactFeatureFlags'); const TEXT_NODE_TYPE = 3; @@ -82,11 +83,13 @@ describe('ReactDOMServerIntegration', () => { } }); - itRenders('an array with one child', async render => { - const e = await render([
text1
]); - const parent = e.parentNode; - expect(parent.childNodes[0].tagName).toBe('DIV'); - }); + if (ReactFeatureFlags.favorSafetyOverHydrationPerf) { + itRenders('an array with one child', async render => { + const e = await render([
text1
]); + const parent = e.parentNode; + expect(parent.childNodes[0].tagName).toBe('DIV'); + }); + } itRenders('an array with several children', async render => { const Header = props => { diff --git a/packages/react-dom/src/__tests__/ReactDOMServerIntegrationElements-test.js b/packages/react-dom/src/__tests__/ReactDOMServerIntegrationElements-test.js index a66cd12cd9178..7621f1bbaf4dc 100644 --- a/packages/react-dom/src/__tests__/ReactDOMServerIntegrationElements-test.js +++ b/packages/react-dom/src/__tests__/ReactDOMServerIntegrationElements-test.js @@ -18,6 +18,7 @@ let React; let ReactDOM; let ReactDOMClient; let ReactDOMServer; +const ReactFeatureFlags = require('shared/ReactFeatureFlags'); function initModules() { jest.resetModules(); @@ -60,19 +61,23 @@ describe('ReactDOMServerIntegration', () => { } describe('text children', function () { - itRenders('a div with text', async render => { - const e = await render(
Text
); - expect(e.tagName).toBe('DIV'); - expect(e.childNodes.length).toBe(1); - expectNode(e.firstChild, TEXT_NODE_TYPE, 'Text'); - }); + if (ReactFeatureFlags.favorSafetyOverHydrationPerf) { + itRenders('a div with text', async render => { + const e = await render(
Text
); + expect(e.tagName).toBe('DIV'); + expect(e.childNodes.length).toBe(1); + expectNode(e.firstChild, TEXT_NODE_TYPE, 'Text'); + }); + } - itRenders('a div with text with flanking whitespace', async render => { - // prettier-ignore - const e = await render(
Text
); - expect(e.childNodes.length).toBe(1); - expectNode(e.childNodes[0], TEXT_NODE_TYPE, ' Text '); - }); + if (ReactFeatureFlags.favorSafetyOverHydrationPerf) { + itRenders('a div with text with flanking whitespace', async render => { + // prettier-ignore + const e = await render(
Text
); + expect(e.childNodes.length).toBe(1); + expectNode(e.childNodes[0], TEXT_NODE_TYPE, ' Text '); + }); + } itRenders('a div with an empty text child', async render => { const e = await render(
{''}
); @@ -272,16 +277,20 @@ describe('ReactDOMServerIntegration', () => { }); describe('number children', function () { - itRenders('a number as single child', async render => { - const e = await render(
{3}
); - expect(e.textContent).toBe('3'); - }); + if (ReactFeatureFlags.favorSafetyOverHydrationPerf) { + itRenders('a number as single child', async render => { + const e = await render(
{3}
); + expect(e.textContent).toBe('3'); + }); + } // zero is falsey, so it could look like no children if the code isn't careful. - itRenders('zero as single child', async render => { - const e = await render(
{0}
); - expect(e.textContent).toBe('0'); - }); + if (ReactFeatureFlags.favorSafetyOverHydrationPerf) { + itRenders('zero as single child', async render => { + const e = await render(
{0}
); + expect(e.textContent).toBe('0'); + }); + } itRenders('an element with number and text children', async render => { const e = await render( @@ -601,10 +610,15 @@ describe('ReactDOMServerIntegration', () => { expect(e.textContent).toBe('\nHello'); }, ); - itRenders('a normal tag with content starting with \\n', async render => { - const e = await render(
{'\nHello'}
); - expect(e.textContent).toBe('\nHello'); - }); + if (ReactFeatureFlags.favorSafetyOverHydrationPerf) { + itRenders( + 'a normal tag with content starting with \\n', + async render => { + const e = await render(
{'\nHello'}
); + expect(e.textContent).toBe('\nHello'); + }, + ); + } }); describe('different component implementations', function () { @@ -613,19 +627,21 @@ describe('ReactDOMServerIntegration', () => { expectNode(e.firstChild, TEXT_NODE_TYPE, 'foo'); } - itRenders('stateless components', async render => { - const FunctionComponent = () =>
foo
; - checkFooDiv(await render()); - }); + if (ReactFeatureFlags.favorSafetyOverHydrationPerf) { + itRenders('stateless components', async render => { + const FunctionComponent = () =>
foo
; + checkFooDiv(await render()); + }); - itRenders('ES6 class components', async render => { - class ClassComponent extends React.Component { - render() { - return
foo
; + itRenders('ES6 class components', async render => { + class ClassComponent extends React.Component { + render() { + return
foo
; + } } - } - checkFooDiv(await render()); - }); + checkFooDiv(await render()); + }); + } if (require('shared/ReactFeatureFlags').disableModulePatternComponents) { itThrowsWhenRendering( @@ -798,11 +814,13 @@ describe('ReactDOMServerIntegration', () => { }); describe('escaping >, <, and &', function () { - itRenders('>,<, and & as single child', async render => { - const e = await render(
{'Text"'}
); - expect(e.childNodes.length).toBe(1); - expectNode(e.firstChild, TEXT_NODE_TYPE, 'Text"'); - }); + if (ReactFeatureFlags.favorSafetyOverHydrationPerf) { + itRenders('>,<, and & as single child', async render => { + const e = await render(
{'Text"'}
); + expect(e.childNodes.length).toBe(1); + expectNode(e.firstChild, TEXT_NODE_TYPE, 'Text"'); + }); + } itRenders('>,<, and & as multiple children', async render => { const e = await render( @@ -834,32 +852,34 @@ describe('ReactDOMServerIntegration', () => { // If we have a mismatch, it might be caused by that (and should not be reported). // We won't be patching up in this case as that matches our past behavior. - itRenders( - 'an element with one text child with special characters', - async render => { - const e = await render(
{'foo\rbar\r\nbaz\nqux\u0000'}
); - if ( - render === serverRender || - render === streamRender || - render === clientRenderOnServerString - ) { - expect(e.childNodes.length).toBe(1); - // Everything becomes LF when parsed from server HTML or hydrated. - // Null character is ignored. - expectNode(e.childNodes[0], TEXT_NODE_TYPE, 'foo\nbar\nbaz\nqux'); - } else { - expect(e.childNodes.length).toBe(1); - // Client rendering uses JS value with CR. - // Null character stays. - - expectNode( - e.childNodes[0], - TEXT_NODE_TYPE, - 'foo\rbar\r\nbaz\nqux\u0000', - ); - } - }, - ); + if (ReactFeatureFlags.favorSafetyOverHydrationPerf) { + itRenders( + 'an element with one text child with special characters', + async render => { + const e = await render(
{'foo\rbar\r\nbaz\nqux\u0000'}
); + if ( + render === serverRender || + render === streamRender || + render === clientRenderOnServerString + ) { + expect(e.childNodes.length).toBe(1); + // Everything becomes LF when parsed from server HTML or hydrated. + // Null character is ignored. + expectNode(e.childNodes[0], TEXT_NODE_TYPE, 'foo\nbar\nbaz\nqux'); + } else { + expect(e.childNodes.length).toBe(1); + // Client rendering uses JS value with CR. + // Null character stays. + + expectNode( + e.childNodes[0], + TEXT_NODE_TYPE, + 'foo\rbar\r\nbaz\nqux\u0000', + ); + } + }, + ); + } itRenders( 'an element with two text children with special characters', diff --git a/packages/react-dom/src/__tests__/ReactDOMServerIntegrationSpecialTypes-test.js b/packages/react-dom/src/__tests__/ReactDOMServerIntegrationSpecialTypes-test.js index d86fd2fe2c964..1afe558ba5232 100644 --- a/packages/react-dom/src/__tests__/ReactDOMServerIntegrationSpecialTypes-test.js +++ b/packages/react-dom/src/__tests__/ReactDOMServerIntegrationSpecialTypes-test.js @@ -20,6 +20,7 @@ let memo; let yieldedValues; let log; let clearLog; +const ReactFeatureFlags = require('shared/ReactFeatureFlags'); function initModules() { // Reset warning cache. @@ -54,35 +55,37 @@ describe('ReactDOMServerIntegration', () => { resetModules(); }); - itRenders('a forwardedRef component and its children', async render => { - const FunctionComponent = ({label, forwardedRef}) => ( -
{label}
- ); - const WrappedFunctionComponent = forwardRef((props, ref) => ( - - )); + if (ReactFeatureFlags.favorSafetyOverHydrationPerf) { + itRenders('a forwardedRef component and its children', async render => { + const FunctionComponent = ({label, forwardedRef}) => ( +
{label}
+ ); + const WrappedFunctionComponent = forwardRef((props, ref) => ( + + )); - const ref = React.createRef(); - const element = await render( - , - ); - const parent = element.parentNode; - const div = parent.childNodes[0]; - expect(div.tagName).toBe('DIV'); - expect(div.textContent).toBe('Test'); - }); + const ref = React.createRef(); + const element = await render( + , + ); + const parent = element.parentNode; + const div = parent.childNodes[0]; + expect(div.tagName).toBe('DIV'); + expect(div.textContent).toBe('Test'); + }); - itRenders('a Profiler component and its children', async render => { - const element = await render( - -
Test
-
, - ); - const parent = element.parentNode; - const div = parent.childNodes[0]; - expect(div.tagName).toBe('DIV'); - expect(div.textContent).toBe('Test'); - }); + itRenders('a Profiler component and its children', async render => { + const element = await render( + +
Test
+
, + ); + const parent = element.parentNode; + const div = parent.childNodes[0]; + expect(div.tagName).toBe('DIV'); + expect(div.textContent).toBe('Test'); + }); + } describe('memoized function components', () => { beforeEach(() => { 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-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;