From ab6a13ee387564073a6af55cb5133af5e48a3b72 Mon Sep 17 00:00:00 2001 From: Sebastian Silbermann Date: Mon, 26 Feb 2024 20:39:34 +0100 Subject: [PATCH 1/2] Convert ReactServerRenderingHydration to createRoot --- .../ReactServerRenderingHydration-test.js | 278 ++++++++++++------ 1 file changed, 184 insertions(+), 94 deletions(-) diff --git a/packages/react-dom/src/__tests__/ReactServerRenderingHydration-test.js b/packages/react-dom/src/__tests__/ReactServerRenderingHydration-test.js index c7416b255d471..7929cd1e27aa4 100644 --- a/packages/react-dom/src/__tests__/ReactServerRenderingHydration-test.js +++ b/packages/react-dom/src/__tests__/ReactServerRenderingHydration-test.js @@ -34,7 +34,7 @@ describe('ReactDOMServerHydration', () => { act = InternalTestUtils.act; }); - it('should have the correct mounting behavior (new hydrate API)', () => { + it('should have the correct mounting behavior', async () => { let mountCount = 0; let numClicks = 0; @@ -61,20 +61,29 @@ describe('ReactDOMServerHydration', () => { const element = document.createElement('div'); document.body.appendChild(element); try { - ReactDOM.render(, element); + let root = ReactDOMClient.createRoot(element); + await act(() => { + root.render(); + }); let lastMarkup = element.innerHTML; // Exercise the update path. Markup should not change, // but some lifecycle methods should be run again. - ReactDOM.render(, element); + await act(() => { + root.render(); + }); expect(mountCount).toEqual(1); // Unmount and remount. We should get another mount event and // we should get different markup, as the IDs are unique each time. - ReactDOM.unmountComponentAtNode(element); + root.unmount(); expect(element.innerHTML).toEqual(''); - ReactDOM.render(, element); + root = ReactDOMClient.createRoot(element); + await act(() => { + root.render(); + }); + expect(mountCount).toEqual(2); expect(element.innerHTML).not.toEqual(lastMarkup); @@ -82,13 +91,22 @@ describe('ReactDOMServerHydration', () => { // we used server rendering. We should mount again, but the markup should // be unchanged. We will append a sentinel at the end of innerHTML to be // sure that innerHTML was not changed. - ReactDOM.unmountComponentAtNode(element); + await act(() => { + root.unmount(); + }); expect(element.innerHTML).toEqual(''); lastMarkup = ReactDOMServer.renderToString(); element.innerHTML = lastMarkup; - let instance = ReactDOM.hydrate(, element); + let instance; + + root = await act(() => { + return ReactDOMClient.hydrateRoot( + element, + (instance = current)} />, + ); + }); expect(mountCount).toEqual(3); expect(element.innerHTML).toBe(lastMarkup); @@ -97,15 +115,36 @@ describe('ReactDOMServerHydration', () => { instance.spanRef.current.click(); expect(numClicks).toEqual(1); - ReactDOM.unmountComponentAtNode(element); + await act(() => { + root.unmount(); + }); expect(element.innerHTML).toEqual(''); // Now simulate a situation where the app is not idempotent. React should // warn but do the right thing. element.innerHTML = lastMarkup; - expect(() => { - instance = ReactDOM.hydrate(, element); - }).toErrorDev('Text content did not match. Server: "x" Client: "y"'); + await expect(async () => { + root = await act(() => { + return ReactDOMClient.hydrateRoot( + element, + { + instance = current; + }} + />, + { + onRecoverableError: error => {}, + }, + ); + }); + }).toErrorDev( + [ + 'Text content did not match. Server: "x" Client: "y"', + 'An error occurred during hydration. The server HTML was replaced with client content in
.', + ], + {withoutStack: 1}, + ); expect(mountCount).toEqual(4); expect(element.innerHTML.length > 0).toBe(true); expect(element.innerHTML).not.toEqual(lastMarkup); @@ -164,25 +203,39 @@ describe('ReactDOMServerHydration', () => { }); // Regression test for https://github.com/facebook/react/issues/11726 - it('should not focus on either server or client with autofocus={false} even if there is a markup mismatch', () => { + it('should not focus on either server or client with autofocus={false} even if there is a markup mismatch', async () => { const element = document.createElement('div'); element.innerHTML = ReactDOMServer.renderToString( , ); expect(element.firstChild.autofocus).toBe(false); - - element.firstChild.focus = jest.fn(); - - expect(() => - ReactDOM.hydrate(, element), - ).toErrorDev( - 'Warning: Text content did not match. Server: "server" Client: "client"', + const onFocusBeforeHydration = jest.fn(); + const onFocusAfterHydration = jest.fn(); + element.firstChild.focus = onFocusBeforeHydration; + + await expect(async () => { + await act(() => { + ReactDOMClient.hydrateRoot( + element, + , + {onRecoverableError: error => {}}, + ); + }); + }).toErrorDev( + [ + 'An error occurred during hydration. The server HTML was replaced with client content in
.', + 'Warning: Text content did not match. Server: "server" Client: "client"', + ], + {withoutStack: 1}, ); - expect(element.firstChild.focus).not.toHaveBeenCalled(); + expect(onFocusBeforeHydration).not.toHaveBeenCalled(); + expect(onFocusAfterHydration).not.toHaveBeenCalled(); }); - it('should warn when the style property differs', () => { + it('should warn when the style property differs', async () => { const element = document.createElement('div'); element.innerHTML = ReactDOMServer.renderToString(
, @@ -190,26 +243,27 @@ describe('ReactDOMServerHydration', () => { expect(element.firstChild.style.textDecoration).toBe('none'); expect(element.firstChild.style.color).toBe('black'); - expect(() => - ReactDOM.hydrate( -
, - element, - ), - ).toErrorDev( + await expect(async () => { + await act(() => { + ReactDOMClient.hydrateRoot( + element, +
, + ); + }); + }).toErrorDev( 'Warning: Prop `style` did not match. Server: ' + '"text-decoration:none;color:black;height:10px" Client: ' + '"text-decoration:none;color:white;height:10px"', ); }); - // @gate !disableIEWorkarounds || !__DEV__ - it('should not warn when the style property differs on whitespace or order in IE', () => { + it('should not warn when the style property differs on whitespace or order in IE', async () => { document.documentMode = 11; jest.resetModules(); React = require('react'); - ReactDOM = require('react-dom'); + ReactDOMClient = require('react-dom/client'); ReactDOMServer = require('react-dom/server'); try { const element = document.createElement('div'); @@ -219,33 +273,35 @@ describe('ReactDOMServerHydration', () => { element.innerHTML = '
'; - // We don't expect to see false positive warnings. - // https://github.com/facebook/react/issues/11807 - ReactDOM.hydrate( -
, - element, - ); + await act(() => { + ReactDOMClient.hydrateRoot( + element, +
, + ); + }); } finally { delete document.documentMode; } }); - it('should warn when the style property differs on whitespace in non-IE browsers', () => { + it('should warn when the style property differs on whitespace in non-IE browsers', async () => { const element = document.createElement('div'); element.innerHTML = '
'; - expect(() => - ReactDOM.hydrate( -
, - element, - ), - ).toErrorDev( + await expect(async () => { + await act(() => { + ReactDOMClient.hydrateRoot( + element, +
, + ); + }); + }).toErrorDev( 'Warning: Prop `style` did not match. Server: ' + '"text-decoration: none; color: black; height: 10px;" Client: ' + '"text-decoration:none;color:black;height:10px"', @@ -264,7 +320,7 @@ describe('ReactDOMServerHydration', () => { ); }); - it('should be able to render and hydrate Mode components', () => { + it('should be able to render and hydrate Mode components', async () => { class ComponentWithWarning extends React.Component { componentWillMount() { // Expected warning @@ -286,15 +342,17 @@ describe('ReactDOMServerHydration', () => { }).toWarnDev('componentWillMount has been renamed'); expect(element.textContent).toBe('Hi'); - expect(() => { - ReactDOM.hydrate(markup, element); + await expect(async () => { + await act(() => { + ReactDOMClient.hydrateRoot(element, markup); + }); }).toWarnDev('componentWillMount has been renamed', { withoutStack: true, }); expect(element.textContent).toBe('Hi'); }); - it('should be able to render and hydrate forwardRef components', () => { + it('should be able to render and hydrate forwardRef components', async () => { const FunctionComponent = ({label, forwardedRef}) => (
{label}
); @@ -310,12 +368,14 @@ describe('ReactDOMServerHydration', () => { expect(element.textContent).toBe('Hi'); expect(ref.current).toBe(null); - ReactDOM.hydrate(markup, element); + await act(() => { + ReactDOMClient.hydrateRoot(element, markup); + }); expect(element.textContent).toBe('Hi'); expect(ref.current.tagName).toBe('DIV'); }); - it('should be able to render and hydrate Profiler components', () => { + it('should be able to render and hydrate Profiler components', async () => { const callback = jest.fn(); const markup = ( @@ -328,7 +388,9 @@ describe('ReactDOMServerHydration', () => { expect(element.textContent).toBe('Hi'); expect(callback).not.toHaveBeenCalled(); - ReactDOM.hydrate(markup, element); + await act(() => { + ReactDOMClient.hydrateRoot(element, markup); + }); expect(element.textContent).toBe('Hi'); if (__DEV__) { expect(callback).toHaveBeenCalledTimes(1); @@ -341,7 +403,7 @@ describe('ReactDOMServerHydration', () => { }); // Regression test for https://github.com/facebook/react/issues/11423 - it('should ignore noscript content on the client and not warn about mismatches', () => { + it('should ignore noscript content on the client and not warn about mismatches', async () => { const callback = jest.fn(); const TestComponent = ({onRender}) => { onRender(); @@ -360,10 +422,9 @@ describe('ReactDOMServerHydration', () => { '
Enable JavaScript to run this app.
', ); - // On the client we want to keep the existing markup, but not render the - // actual elements for performance reasons and to avoid for example - // downloading images. This should also not warn for hydration mismatches. - ReactDOM.hydrate(markup, element); + await act(() => { + ReactDOMClient.hydrateRoot(element, markup); + }); expect(callback).toHaveBeenCalledTimes(1); expect(element.textContent).toBe( '
Enable JavaScript to run this app.
', @@ -371,18 +432,17 @@ describe('ReactDOMServerHydration', () => { }); it('should be able to use lazy components after hydrating', async () => { + let resolveLazy; const Lazy = React.lazy( () => new Promise(resolve => { - setTimeout( - () => - resolve({ - default: function World() { - return 'world'; - }, - }), - 1000, - ); + resolveLazy = () => { + resolve({ + default: function World() { + return 'world'; + }, + }); + }; }), ); class HelloWorld extends React.Component { @@ -410,11 +470,13 @@ describe('ReactDOMServerHydration', () => { element.innerHTML = ReactDOMServer.renderToString(); expect(element.textContent).toBe('Hello '); - ReactDOM.hydrate(, element); + await act(() => { + ReactDOMClient.hydrateRoot(element, ); + }); expect(element.textContent).toBe('Hello loading'); // Resolve Lazy component - await act(() => jest.runAllTimers()); + await act(() => resolveLazy()); expect(element.textContent).toBe('Hello world'); }); @@ -497,7 +559,7 @@ describe('ReactDOMServerHydration', () => { }); // regression test for https://github.com/facebook/react/issues/17170 - it('should not warn if dangerouslySetInnerHtml=undefined', () => { + it('should not warn if dangerouslySetInnerHtml=undefined', async () => { const domElement = document.createElement('div'); const reactElement = (
@@ -507,49 +569,68 @@ describe('ReactDOMServerHydration', () => { const markup = ReactDOMServer.renderToStaticMarkup(reactElement); domElement.innerHTML = markup; - ReactDOM.hydrate(reactElement, domElement); + await act(() => { + ReactDOMClient.hydrateRoot(domElement, reactElement); + }); expect(domElement.innerHTML).toEqual(markup); }); - it('should warn if innerHTML mismatches with dangerouslySetInnerHTML=undefined and children on the client', () => { + it('should warn if innerHTML mismatches with dangerouslySetInnerHTML=undefined and children on the client', async () => { const domElement = document.createElement('div'); const markup = ReactDOMServer.renderToStaticMarkup(
server

'}} />, ); domElement.innerHTML = markup; - expect(() => { - ReactDOM.hydrate( -
-

client

-
, - domElement, - ); + await expect(async () => { + await act(() => { + ReactDOMClient.hydrateRoot( + domElement, +
+

client

+
, + {onRecoverableError: error => {}}, + ); + }); expect(domElement.innerHTML).not.toEqual(markup); }).toErrorDev( - 'Warning: Text content did not match. Server: "server" Client: "client"', + [ + 'An error occurred during hydration. The server HTML was replaced with client content in
.', + 'Warning: Text content did not match. Server: "server" Client: "client"', + ], + {withoutStack: 1}, ); }); - it('should warn if innerHTML mismatches with dangerouslySetInnerHTML=undefined on the client', () => { + it('should warn if innerHTML mismatches with dangerouslySetInnerHTML=undefined on the client', async () => { const domElement = document.createElement('div'); const markup = ReactDOMServer.renderToStaticMarkup(
server

'}} />, ); domElement.innerHTML = markup; - expect(() => { - ReactDOM.hydrate(
, domElement); + await expect(async () => { + await act(() => { + ReactDOMClient.hydrateRoot( + domElement, +
, + {onRecoverableError: error => {}}, + ); + }); expect(domElement.innerHTML).not.toEqual(markup); }).toErrorDev( - 'Warning: Did not expect server HTML to contain a

in

', + [ + 'An error occurred during hydration. The server HTML was replaced with client content in
.', + 'Warning: Did not expect server HTML to contain a

in

.', + ], + {withoutStack: 1}, ); }); - it('should warn when hydrating read-only properties', () => { + it('should warn when hydrating read-only properties', async () => { const readOnlyProperties = [ 'offsetParent', 'offsetTop', @@ -560,24 +641,31 @@ describe('ReactDOMServerHydration', () => { 'outerText', 'outerHTML', ]; - readOnlyProperties.forEach(readOnlyProperty => { + // eslint-disable-next-line no-for-of-loops/no-for-of-loops + for (const readOnlyProperty of readOnlyProperties) { const props = {}; props[readOnlyProperty] = 'hello'; const jsx = React.createElement('my-custom-element', props); const element = document.createElement('div'); element.innerHTML = ReactDOMServer.renderToString(jsx); if (gate(flags => flags.enableCustomElementPropertySupport)) { - expect(() => ReactDOM.hydrate(jsx, element)).toErrorDev( + await expect(async () => { + await act(() => { + ReactDOMClient.hydrateRoot(element, jsx); + }); + }).toErrorDev( `Warning: Assignment to read-only property will result in a no-op: \`${readOnlyProperty}\``, ); } else { - ReactDOM.hydrate(jsx, element); + await act(() => { + ReactDOMClient.hydrateRoot(element, jsx); + }); } - }); + } }); // @gate enableCustomElementPropertySupport - it('should not re-assign properties on hydration', () => { + it('should not re-assign properties on hydration', async () => { const container = document.createElement('div'); document.body.appendChild(container); @@ -607,7 +695,9 @@ describe('ReactDOMServerHydration', () => { }, }); - ReactDOM.hydrate(jsx, container); + await act(() => { + ReactDOMClient.hydrateRoot(container, jsx); + }); expect(customElement.getAttribute('str')).toBe('string'); expect(customElement.getAttribute('obj')).toBe(null); From d8bb04eb777ac1f153b60e77b26740d11d332460 Mon Sep 17 00:00:00 2001 From: Sebastian Silbermann Date: Mon, 26 Feb 2024 20:52:23 +0100 Subject: [PATCH 2/2] New warnings are from enableClientRenderFallbackOnTextMismatch --- .../ReactServerRenderingHydration-test.js | 49 +++++++++++++------ 1 file changed, 34 insertions(+), 15 deletions(-) diff --git a/packages/react-dom/src/__tests__/ReactServerRenderingHydration-test.js b/packages/react-dom/src/__tests__/ReactServerRenderingHydration-test.js index 7929cd1e27aa4..982166b40545e 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 enableClientRenderFallbackOnTextMismatch = gate( + flags => flags.enableClientRenderFallbackOnTextMismatch, + ); await expect(async () => { root = await act(() => { return ReactDOMClient.hydrateRoot( @@ -139,11 +142,13 @@ describe('ReactDOMServerHydration', () => { ); }); }).toErrorDev( - [ - 'Text content did not match. Server: "x" Client: "y"', - 'An error occurred during hydration. The server HTML was replaced with client content in
.', - ], - {withoutStack: 1}, + enableClientRenderFallbackOnTextMismatch + ? [ + 'An error occurred during hydration. The server HTML was replaced with client content in
.', + 'Text content did not match. Server: "x" Client: "y"', + ] + : ['Text content did not match. Server: "x" Client: "y"'], + {withoutStack: enableClientRenderFallbackOnTextMismatch ? 1 : 0}, ); expect(mountCount).toEqual(4); expect(element.innerHTML.length > 0).toBe(true); @@ -213,6 +218,9 @@ describe('ReactDOMServerHydration', () => { const onFocusAfterHydration = jest.fn(); element.firstChild.focus = onFocusBeforeHydration; + const enableClientRenderFallbackOnTextMismatch = gate( + flags => flags.enableClientRenderFallbackOnTextMismatch, + ); await expect(async () => { await act(() => { ReactDOMClient.hydrateRoot( @@ -224,11 +232,15 @@ describe('ReactDOMServerHydration', () => { ); }); }).toErrorDev( - [ - 'An error occurred during hydration. The server HTML was replaced with client content in
.', - 'Warning: Text content did not match. Server: "server" Client: "client"', - ], - {withoutStack: 1}, + enableClientRenderFallbackOnTextMismatch + ? [ + 'An error occurred during hydration. The server HTML was replaced with client content in
.', + 'Warning: Text content did not match. Server: "server" Client: "client"', + ] + : [ + 'Warning: Text content did not match. Server: "server" Client: "client"', + ], + {withoutStack: enableClientRenderFallbackOnTextMismatch ? 1 : 0}, ); expect(onFocusBeforeHydration).not.toHaveBeenCalled(); @@ -583,6 +595,9 @@ describe('ReactDOMServerHydration', () => { ); domElement.innerHTML = markup; + const enableClientRenderFallbackOnTextMismatch = gate( + flags => flags.enableClientRenderFallbackOnTextMismatch, + ); await expect(async () => { await act(() => { ReactDOMClient.hydrateRoot( @@ -596,11 +611,15 @@ describe('ReactDOMServerHydration', () => { expect(domElement.innerHTML).not.toEqual(markup); }).toErrorDev( - [ - 'An error occurred during hydration. The server HTML was replaced with client content in
.', - 'Warning: Text content did not match. Server: "server" Client: "client"', - ], - {withoutStack: 1}, + enableClientRenderFallbackOnTextMismatch + ? [ + 'An error occurred during hydration. The server HTML was replaced with client content in
.', + 'Warning: Text content did not match. Server: "server" Client: "client"', + ] + : [ + 'Warning: Text content did not match. Server: "server" Client: "client"', + ], + {withoutStack: enableClientRenderFallbackOnTextMismatch ? 1 : 0}, ); });