@samitouri / QOS-React / commits / 2cd19ed1dd

Convert ReactRenderDocument to hydrateRoot (#28153)

Co-authored-by: Sebastian Silbermann <sebastian.silbermann@klarna.com>

Ricky committed Feb 7, 2024 at 09:54 UTC 2cd19ed1dd86889bc8038fb8b541699e3965ea5c
1 file changed +168 -40
packages/react-dom/src/__tests__/ReactRenderDocument-test.js
+168 -40
@@ -11,7 +11,11 @@
11
12 let React;
13 let ReactDOM;
14 +let ReactDOMClient;
15 let ReactDOMServer;
16 +let act;
17 +let Scheduler;
18 +let assertLog;
19
20 function getTestDocument(markup) {
21 const doc = document.implementation.createHTMLDocument('');
@@ -28,11 +32,15 @@ describe('rendering React components at document', () => {
32 beforeEach(() => {
33 React = require('react');
34 ReactDOM = require('react-dom');
35 + ReactDOMClient = require('react-dom/client');
36 ReactDOMServer = require('react-dom/server');
37 + act = require('internal-test-utils').act;
38 + assertLog = require('internal-test-utils').assertLog;
39 + Scheduler = require('scheduler');
40 });
41
42 describe('with new explicit hydration API', () => {
35 - it('should be able to adopt server markup', () => {
43 + it('should be able to adopt server markup', async () => {
44 class Root extends React.Component {
45 render() {
46 return (
@@ -51,16 +59,21 @@ describe('rendering React components at document', () => {
59 const testDocument = getTestDocument(markup);
60 const body = testDocument.body;
61
54 - ReactDOM.hydrate(<Root hello="world" />, testDocument);
62 + let root;
63 + await act(() => {
64 + root = ReactDOMClient.hydrateRoot(testDocument, <Root hello="world" />);
65 + });
66 expect(testDocument.body.innerHTML).toBe('Hello world');
67
57 - ReactDOM.hydrate(<Root hello="moon" />, testDocument);
68 + await act(() => {
69 + root.render(<Root hello="moon" />);
70 + });
71 expect(testDocument.body.innerHTML).toBe('Hello moon');
72
73 expect(body === testDocument.body).toBe(true);
74 });
75
63 - it('should be able to unmount component from document node, but leaves singleton nodes intact', () => {
76 + it('should be able to unmount component from document node, but leaves singleton nodes intact', async () => {
77 class Root extends React.Component {
78 render() {
79 return (
@@ -76,7 +89,10 @@ describe('rendering React components at document', () => {
89
90 const markup = ReactDOMServer.renderToString(<Root />);
91 const testDocument = getTestDocument(markup);
79 - ReactDOM.hydrate(<Root />, testDocument);
92 + let root;
93 + await act(() => {
94 + root = ReactDOMClient.hydrateRoot(testDocument, <Root />);
95 + });
96 expect(testDocument.body.innerHTML).toBe('Hello world');
97
98 const originalDocEl = testDocument.documentElement;
@@ -84,7 +100,7 @@ describe('rendering React components at document', () => {
100 const originalBody = testDocument.body;
101
102 // When we unmount everything is removed except the singleton nodes of html, head, and body
87 - ReactDOM.unmountComponentAtNode(testDocument);
103 + root.unmount();
104 expect(testDocument.firstChild).toBe(originalDocEl);
105 expect(testDocument.head).toBe(originalHead);
106 expect(testDocument.body).toBe(originalBody);
@@ -92,7 +108,7 @@ describe('rendering React components at document', () => {
108 expect(originalHead.firstChild).toEqual(null);
109 });
110
95 - it('should not be able to switch root constructors', () => {
111 + it('should not be able to switch root constructors', async () => {
112 class Component extends React.Component {
113 render() {
114 return (
@@ -122,17 +138,21 @@ describe('rendering React components at document', () => {
138 const markup = ReactDOMServer.renderToString(<Component />);
139 const testDocument = getTestDocument(markup);
140
125 - ReactDOM.hydrate(<Component />, testDocument);
141 + let root;
142 + await act(() => {
143 + root = ReactDOMClient.hydrateRoot(testDocument, <Component />);
144 + });
145
146 expect(testDocument.body.innerHTML).toBe('Hello world');
147
129 - // This works but is probably a bad idea.
130 - ReactDOM.hydrate(<Component2 />, testDocument);
148 + await act(() => {
149 + root.render(<Component2 />);
150 + });
151
152 expect(testDocument.body.innerHTML).toBe('Goodbye world');
153 });
154
135 - it('should be able to mount into document', () => {
155 + it('should be able to mount into document', async () => {
156 class Component extends React.Component {
157 render() {
158 return (
@@ -151,40 +171,80 @@ describe('rendering React components at document', () => {
171 );
172 const testDocument = getTestDocument(markup);
173
154 - ReactDOM.hydrate(<Component text="Hello world" />, testDocument);
174 + await act(() => {
175 + ReactDOMClient.hydrateRoot(
176 + testDocument,
177 + <Component text="Hello world" />,
178 + );
179 + });
180
181 expect(testDocument.body.innerHTML).toBe('Hello world');
182 });
183
159 - it('cannot render over an existing text child at the root', () => {
184 + it('cannot render over an existing text child at the root', async () => {
185 const container = document.createElement('div');
186 container.textContent = 'potato';
162 - expect(() => ReactDOM.hydrate(<div>parsnip</div>, container)).toErrorDev(
163 - 'Expected server HTML to contain a matching <div> in <div>.',
187 +
188 + expect(() => {
189 + ReactDOM.flushSync(() => {
190 + ReactDOMClient.hydrateRoot(container, <div>parsnip</div>, {
191 + onRecoverableError: error => {
192 + Scheduler.log('Log recoverable error: ' + error.message);
193 + },
194 + });
195 + });
196 + }).toErrorDev(
197 + [
198 + 'Warning: An error occurred during hydration. The server HTML was replaced with client content in <div>.',
199 + 'Expected server HTML to contain a matching <div> in <div>.',
200 + ],
201 + {withoutStack: 1},
202 );
203 +
204 + assertLog([
205 + 'Log recoverable error: Hydration failed because the initial UI does not match what was rendered on the server.',
206 + '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.',
207 + ]);
208 +
209 // This creates an unfortunate double text case.
166 - expect(container.textContent).toBe('potatoparsnip');
210 + expect(container.textContent).toBe('parsnip');
211 });
212
169 - it('renders over an existing nested text child without throwing', () => {
213 + it('renders over an existing nested text child without throwing', async () => {
214 const container = document.createElement('div');
215 const wrapper = document.createElement('div');
216 wrapper.textContent = 'potato';
217 container.appendChild(wrapper);
174 - expect(() =>
175 - ReactDOM.hydrate(
176 - <div>
177 - <div>parsnip</div>
178 - </div>,
179 - container,
180 - ),
181 - ).toErrorDev(
182 - 'Expected server HTML to contain a matching <div> in <div>.',
218 + expect(() => {
219 + ReactDOM.flushSync(() => {
220 + ReactDOMClient.hydrateRoot(
221 + container,
222 + <div>
223 + <div>parsnip</div>
224 + </div>,
225 + {
226 + onRecoverableError: error => {
227 + Scheduler.log('Log recoverable error: ' + error.message);
228 + },
229 + },
230 + );
231 + });
232 + }).toErrorDev(
233 + [
234 + 'Warning: An error occurred during hydration. The server HTML was replaced with client content in <div>.',
235 + 'Expected server HTML to contain a matching <div> in <div>.',
236 + ],
237 + {withoutStack: 1},
238 );
239 +
240 + assertLog([
241 + 'Log recoverable error: Hydration failed because the initial UI does not match what was rendered on the server.',
242 + '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.',
243 + ]);
244 expect(container.textContent).toBe('parsnip');
245 });
246
187 - it('should give helpful errors on state desync', () => {
247 + it('should give helpful errors on state desync', async () => {
248 class Component extends React.Component {
249 render() {
250 return (
@@ -203,13 +263,45 @@ describe('rendering React components at document', () => {
263 );
264 const testDocument = getTestDocument(markup);
265
206 - expect(() =>
207 - ReactDOM.hydrate(<Component text="Hello world" />, testDocument),
208 - ).toErrorDev('Warning: Text content did not match.');
266 + const enableClientRenderFallbackOnTextMismatch = gate(
267 + flags => flags.enableClientRenderFallbackOnTextMismatch,
268 + );
269 + expect(() => {
270 + ReactDOM.flushSync(() => {
271 + ReactDOMClient.hydrateRoot(
272 + testDocument,
273 + <Component text="Hello world" />,
274 + {
275 + onRecoverableError: error => {
276 + Scheduler.log('Log recoverable error: ' + error.message);
277 + },
278 + },
279 + );
280 + });
281 + }).toErrorDev(
282 + enableClientRenderFallbackOnTextMismatch
283 + ? [
284 + 'Warning: An error occurred during hydration. The server HTML was replaced with client content in <#document>.',
285 + 'Warning: Text content did not match.',
286 + ]
287 + : ['Warning: Text content did not match.'],
288 + {
289 + withoutStack: enableClientRenderFallbackOnTextMismatch ? 1 : 0,
290 + },
291 + );
292 +
293 + assertLog(
294 + enableClientRenderFallbackOnTextMismatch
295 + ? [
296 + 'Log recoverable error: Text content does not match server-rendered HTML.',
297 + '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.',
298 + ]
299 + : [],
300 + );
301 expect(testDocument.body.innerHTML).toBe('Hello world');
302 });
303
212 - it('should render w/ no markup to full document', () => {
304 + it('should render w/ no markup to full document', async () => {
305 const testDocument = getTestDocument();
306
307 class Component extends React.Component {
@@ -227,23 +319,59 @@ describe('rendering React components at document', () => {
319
320 if (gate(flags => flags.enableFloat)) {
321 // with float the title no longer is a hydration mismatch so we get an error on the body mismatch
230 - expect(() =>
231 - ReactDOM.hydrate(<Component text="Hello world" />, testDocument),
232 - ).toErrorDev(
233 - 'Expected server HTML to contain a matching text node for "Hello world" in <body>',
322 + expect(() => {
323 + ReactDOM.flushSync(() => {
324 + ReactDOMClient.hydrateRoot(
325 + testDocument,
326 + <Component text="Hello world" />,
327 + {
328 + onRecoverableError: error => {
329 + Scheduler.log('Log recoverable error: ' + error.message);
330 + },
331 + },
332 + );
333 + });
334 + }).toErrorDev(
335 + [
336 + 'Warning: An error occurred during hydration. The server HTML was replaced with client content in <#document>.',
337 + 'Expected server HTML to contain a matching text node for "Hello world" in <body>',
338 + ],
339 + {withoutStack: 1},
340 );
341 + assertLog([
342 + 'Log recoverable error: Hydration failed because the initial UI does not match what was rendered on the server.',
343 + '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.',
344 + ]);
345 } else {
346 // getTestDocument() has an extra <meta> that we didn't render.
237 - expect(() =>
238 - ReactDOM.hydrate(<Component text="Hello world" />, testDocument),
239 - ).toErrorDev(
240 - 'Did not expect server HTML to contain a <meta> in <head>.',
347 + expect(() => {
348 + ReactDOM.flushSync(() => {
349 + ReactDOMClient.hydrateRoot(
350 + testDocument,
351 + <Component text="Hello world" />,
352 + {
353 + onRecoverableError: error => {
354 + Scheduler.log('Log recoverable error: ' + error.message);
355 + },
356 + },
357 + );
358 + });
359 + }).toErrorDev(
360 + [
361 + 'Warning: An error occurred during hydration. The server HTML was replaced with client content in <#document>.',
362 + 'Warning: Text content did not match. Server: "test doc" Client: "Hello World"',
363 + ],
364 + {withoutStack: 1},
365 );
366 + assertLog([
367 + 'Log recoverable error: Text content does not match server-rendered HTML.',
368 + '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.',
369 + ]);
370 }
371 expect(testDocument.body.innerHTML).toBe('Hello world');
372 });
373
246 - it('supports findDOMNode on full-page components', () => {
374 + it('supports findDOMNode on full-page components in legacy mode', () => {
375 const tree = (
376 <html>
377 <head>