@samitouri / QOS-React-2 / commits / 5420c4ea08

Convert ReactMount to createRoot (#28075)

To convert this file, I started replacing all the calls in line, and quickly realized that we already have most of these tests covered in other files. So I found the test that we didn't already have for `create/hydrateRoot` and added them, then renamed `ReactMount` to `ReactLegacyMount`.

Ricky committed Jan 24, 2024 at 23:55 UTC 5420c4ea08a4ccf06b04f0c0abdf1f5e8d84223a
3 files changed +263 -140
packages/react-dom/src/__tests__/ReactDOMHydrationDiff-test.js
+37
@@ -113,6 +113,43 @@ describe('ReactDOMServerHydration', () => {
113 }
114 });
115
116 + // @gate __DEV__
117 + it('warns when escaping on a checksum mismatch', () => {
118 + function Mismatch({isClient}) {
119 + if (isClient) {
120 + return (
121 + <div>This markup contains an nbsp entity: &nbsp; client text</div>
122 + );
123 + }
124 + return (
125 + <div>This markup contains an nbsp entity: &nbsp; server text</div>
126 + );
127 + }
128 +
129 + /* eslint-disable no-irregular-whitespace */
130 + if (gate(flags => flags.enableClientRenderFallbackOnTextMismatch)) {
131 + expect(testMismatch(Mismatch)).toMatchInlineSnapshot(`
132 + [
133 + "Warning: Text content did not match. Server: "This markup contains an nbsp entity:   server text" Client: "This markup contains an nbsp entity:   client text"
134 + in div (at **)
135 + in Mismatch (at **)",
136 + "Warning: An error occurred during hydration. The server HTML was replaced with client content in <div>.",
137 + "Caught [Text content does not match server-rendered HTML.]",
138 + "Caught [There was an error while hydrating. Because the error happened outside of a Suspense boundary, the entire root will switch to client rendering.]",
139 + ]
140 + `);
141 + } else {
142 + expect(testMismatch(Mismatch)).toMatchInlineSnapshot(`
143 + [
144 + "Warning: Text content did not match. Server: "This markup contains an nbsp entity:   server text" Client: "This markup contains an nbsp entity:   client text"
145 + in div (at **)
146 + in Mismatch (at **)",
147 + ]
148 + `);
149 + }
150 + /* eslint-enable no-irregular-whitespace */
151 + });
152 +
153 // @gate __DEV__
154 it('warns when client and server render different html', () => {
155 function Mismatch({isClient}) {
packages/react-dom/src/__tests__/ReactDOMRoot-test.js
+93 -140
@@ -175,27 +175,6 @@ describe('ReactDOMRoot', () => {
175 );
176 });
177
178 - it('clears existing children with legacy API', async () => {
179 - container.innerHTML = '<div>a</div><div>b</div>';
180 - ReactDOM.render(
181 - <div>
182 - <span>c</span>
183 - <span>d</span>
184 - </div>,
185 - container,
186 - );
187 - expect(container.textContent).toEqual('cd');
188 - ReactDOM.render(
189 - <div>
190 - <span>d</span>
191 - <span>c</span>
192 - </div>,
193 - container,
194 - );
195 - await waitForAll([]);
196 - expect(container.textContent).toEqual('dc');
197 - });
198 -
178 it('clears existing children', async () => {
179 container.innerHTML = '<div>a</div><div>b</div>';
180 const root = ReactDOMClient.createRoot(container);
@@ -223,122 +202,6 @@ describe('ReactDOMRoot', () => {
202 }).toThrow('createRoot(...): Target container is not a DOM element.');
203 });
204
226 - it('warns when rendering with legacy API into createRoot() container', async () => {
227 - const root = ReactDOMClient.createRoot(container);
228 - root.render(<div>Hi</div>);
229 - await waitForAll([]);
230 - expect(container.textContent).toEqual('Hi');
231 - expect(() => {
232 - ReactDOM.render(<div>Bye</div>, container);
233 - }).toErrorDev(
234 - [
235 - // We care about this warning:
236 - 'You are calling ReactDOM.render() on a container that was previously ' +
237 - 'passed to ReactDOMClient.createRoot(). This is not supported. ' +
238 - 'Did you mean to call root.render(element)?',
239 - // This is more of a symptom but restructuring the code to avoid it isn't worth it:
240 - 'Replacing React-rendered children with a new root component.',
241 - ],
242 - {withoutStack: true},
243 - );
244 - await waitForAll([]);
245 - // This works now but we could disallow it:
246 - expect(container.textContent).toEqual('Bye');
247 - });
248 -
249 - it('warns when hydrating with legacy API into createRoot() container', async () => {
250 - const root = ReactDOMClient.createRoot(container);
251 - root.render(<div>Hi</div>);
252 - await waitForAll([]);
253 - expect(container.textContent).toEqual('Hi');
254 - expect(() => {
255 - ReactDOM.hydrate(<div>Hi</div>, container);
256 - }).toErrorDev(
257 - [
258 - // We care about this warning:
259 - 'You are calling ReactDOM.hydrate() on a container that was previously ' +
260 - 'passed to ReactDOMClient.createRoot(). This is not supported. ' +
261 - 'Did you mean to call hydrateRoot(container, element)?',
262 - // This is more of a symptom but restructuring the code to avoid it isn't worth it:
263 - 'Replacing React-rendered children with a new root component.',
264 - ],
265 - {withoutStack: true},
266 - );
267 - });
268 -
269 - it('callback passed to legacy hydrate() API', () => {
270 - container.innerHTML = '<div>Hi</div>';
271 - ReactDOM.hydrate(<div>Hi</div>, container, () => {
272 - Scheduler.log('callback');
273 - });
274 - expect(container.textContent).toEqual('Hi');
275 - assertLog(['callback']);
276 - });
277 -
278 - it('warns when unmounting with legacy API (no previous content)', async () => {
279 - const root = ReactDOMClient.createRoot(container);
280 - root.render(<div>Hi</div>);
281 - await waitForAll([]);
282 - expect(container.textContent).toEqual('Hi');
283 - let unmounted = false;
284 - expect(() => {
285 - unmounted = ReactDOM.unmountComponentAtNode(container);
286 - }).toErrorDev(
287 - [
288 - // We care about this warning:
289 - 'You are calling ReactDOM.unmountComponentAtNode() on a container that was previously ' +
290 - 'passed to ReactDOMClient.createRoot(). This is not supported. Did you mean to call root.unmount()?',
291 - // This is more of a symptom but restructuring the code to avoid it isn't worth it:
292 - "The node you're attempting to unmount was rendered by React and is not a top-level container.",
293 - ],
294 - {withoutStack: true},
295 - );
296 - expect(unmounted).toBe(false);
297 - await waitForAll([]);
298 - expect(container.textContent).toEqual('Hi');
299 - root.unmount();
300 - await waitForAll([]);
301 - expect(container.textContent).toEqual('');
302 - });
303 -
304 - it('warns when unmounting with legacy API (has previous content)', async () => {
305 - // Currently createRoot().render() doesn't clear this.
306 - container.appendChild(document.createElement('div'));
307 - // The rest is the same as test above.
308 - const root = ReactDOMClient.createRoot(container);
309 - root.render(<div>Hi</div>);
310 - await waitForAll([]);
311 - expect(container.textContent).toEqual('Hi');
312 - let unmounted = false;
313 - expect(() => {
314 - unmounted = ReactDOM.unmountComponentAtNode(container);
315 - }).toErrorDev(
316 - [
317 - 'Did you mean to call root.unmount()?',
318 - // This is more of a symptom but restructuring the code to avoid it isn't worth it:
319 - "The node you're attempting to unmount was rendered by React and is not a top-level container.",
320 - ],
321 - {withoutStack: true},
322 - );
323 - expect(unmounted).toBe(false);
324 - await waitForAll([]);
325 - expect(container.textContent).toEqual('Hi');
326 - root.unmount();
327 - await waitForAll([]);
328 - expect(container.textContent).toEqual('');
329 - });
330 -
331 - it('warns when passing legacy container to createRoot()', () => {
332 - ReactDOM.render(<div>Hi</div>, container);
333 - expect(() => {
334 - ReactDOMClient.createRoot(container);
335 - }).toErrorDev(
336 - 'You are calling ReactDOMClient.createRoot() on a container that was previously ' +
337 - 'passed to ReactDOM.render(). This is not supported.',
338 - {withoutStack: true},
339 - );
340 - });
341 -
205 it('warns when creating two roots managing the same container', () => {
206 ReactDOMClient.createRoot(container);
207 expect(() => {
@@ -399,6 +262,80 @@ describe('ReactDOMRoot', () => {
262 }
263 });
264
265 + it('should render different components in same root', async () => {
266 + document.body.appendChild(container);
267 + const root = ReactDOMClient.createRoot(container);
268 +
269 + await act(() => {
270 + root.render(<div />);
271 + });
272 + expect(container.firstChild.nodeName).toBe('DIV');
273 +
274 + await act(() => {
275 + root.render(<span />);
276 + });
277 + expect(container.firstChild.nodeName).toBe('SPAN');
278 + });
279 +
280 + it('should not warn if mounting into non-empty node', async () => {
281 + container.innerHTML = '<div></div>';
282 + const root = ReactDOMClient.createRoot(container);
283 + await act(() => {
284 + root.render(<div />);
285 + });
286 +
287 + expect(true).toBe(true);
288 + });
289 +
290 + it('should reuse markup if rendering to the same target twice', async () => {
291 + const root = ReactDOMClient.createRoot(container);
292 + await act(() => {
293 + root.render(<div />);
294 + });
295 + const firstElm = container.firstChild;
296 + await act(() => {
297 + root.render(<div />);
298 + });
299 +
300 + expect(firstElm).toBe(container.firstChild);
301 + });
302 +
303 + it('should unmount and remount if the key changes', async () => {
304 + function Component({text}) {
305 + useEffect(() => {
306 + Scheduler.log('Mount');
307 +
308 + return () => {
309 + Scheduler.log('Unmount');
310 + };
311 + }, []);
312 +
313 + return <span>{text}</span>;
314 + }
315 +
316 + const root = ReactDOMClient.createRoot(container);
317 +
318 + await act(() => {
319 + root.render(<Component text="orange" key="A" />);
320 + });
321 + expect(container.firstChild.innerHTML).toBe('orange');
322 + assertLog(['Mount']);
323 +
324 + // If we change the key, the component is unmounted and remounted
325 + await act(() => {
326 + root.render(<Component text="green" key="B" />);
327 + });
328 + expect(container.firstChild.innerHTML).toBe('green');
329 + assertLog(['Unmount', 'Mount']);
330 +
331 + // But if we don't change the key, the component instance is reused
332 + await act(() => {
333 + root.render(<Component text="blue" key="B" />);
334 + });
335 + expect(container.firstChild.innerHTML).toBe('blue');
336 + assertLog([]);
337 + });
338 +
339 it('throws if unmounting a root that has had its contents removed', async () => {
340 const root = ReactDOMClient.createRoot(container);
341 await act(() => {
@@ -514,9 +451,6 @@ describe('ReactDOMRoot', () => {
451 expect(() => ReactDOMClient.hydrateRoot(commentNode)).toThrow(
452 'hydrateRoot(...): Target container is not a DOM element.',
453 );
517 -
518 - // Still works in the legacy API
519 - ReactDOM.render(<div />, commentNode);
454 });
455
456 it('warn if no children passed to hydrateRoot', async () => {
@@ -539,4 +473,23 @@ describe('ReactDOMRoot', () => {
473 },
474 );
475 });
476 +
477 + it('warns when given a function', () => {
478 + function Component() {
479 + return <div />;
480 + }
481 +
482 + const root = ReactDOMClient.createRoot(document.createElement('div'));
483 +
484 + expect(() => {
485 + ReactDOM.flushSync(() => {
486 + root.render(Component);
487 + });
488 + }).toErrorDev(
489 + 'Functions are not valid as a React child. ' +
490 + 'This may happen if you return a Component instead of <Component /> from render. ' +
491 + 'Or maybe you meant to call this function rather than return it.',
492 + {withoutStack: true},
493 + );
494 + });
495 });
packages/react-dom/src/__tests__/ReactLegacyMount-test.js renamed
+133
@@ -15,6 +15,10 @@ let React;
15 let ReactDOM;
16 let ReactDOMServer;
17 let ReactTestUtils;
18 +let Scheduler;
19 +let ReactDOMClient;
20 +let assertLog;
21 +let waitForAll;
22
23 describe('ReactMount', () => {
24 beforeEach(() => {
@@ -22,8 +26,14 @@ describe('ReactMount', () => {
26
27 React = require('react');
28 ReactDOM = require('react-dom');
29 + ReactDOMClient = require('react-dom/client');
30 ReactDOMServer = require('react-dom/server');
31 ReactTestUtils = require('react-dom/test-utils');
32 + Scheduler = require('scheduler');
33 +
34 + const InternalTestUtils = require('internal-test-utils');
35 + assertLog = InternalTestUtils.assertLog;
36 + waitForAll = InternalTestUtils.waitForAll;
37 });
38
39 describe('unmountComponentAtNode', () => {
@@ -337,4 +347,127 @@ describe('ReactMount', () => {
347 );
348 });
349 });
350 +
351 + it('clears existing children with legacy API', async () => {
352 + const container = document.createElement('div');
353 + container.innerHTML = '<div>a</div><div>b</div>';
354 + ReactDOM.render(
355 + <div>
356 + <span>c</span>
357 + <span>d</span>
358 + </div>,
359 + container,
360 + );
361 + expect(container.textContent).toEqual('cd');
362 + ReactDOM.render(
363 + <div>
364 + <span>d</span>
365 + <span>c</span>
366 + </div>,
367 + container,
368 + );
369 + await waitForAll([]);
370 + expect(container.textContent).toEqual('dc');
371 + });
372 +
373 + it('warns when rendering with legacy API into createRoot() container', async () => {
374 + const container = document.createElement('div');
375 + const root = ReactDOMClient.createRoot(container);
376 + root.render(<div>Hi</div>);
377 + await waitForAll([]);
378 + expect(container.textContent).toEqual('Hi');
379 + expect(() => {
380 + ReactDOM.render(<div>Bye</div>, container);
381 + }).toErrorDev(
382 + [
383 + // We care about this warning:
384 + 'You are calling ReactDOM.render() on a container that was previously ' +
385 + 'passed to ReactDOMClient.createRoot(). This is not supported. ' +
386 + 'Did you mean to call root.render(element)?',
387 + // This is more of a symptom but restructuring the code to avoid it isn't worth it:
388 + 'Replacing React-rendered children with a new root component.',
389 + ],
390 + {withoutStack: true},
391 + );
392 + await waitForAll([]);
393 + // This works now but we could disallow it:
394 + expect(container.textContent).toEqual('Bye');
395 + });
396 +
397 + it('callback passed to legacy hydrate() API', () => {
398 + const container = document.createElement('div');
399 + container.innerHTML = '<div>Hi</div>';
400 + ReactDOM.hydrate(<div>Hi</div>, container, () => {
401 + Scheduler.log('callback');
402 + });
403 + expect(container.textContent).toEqual('Hi');
404 + assertLog(['callback']);
405 + });
406 +
407 + it('warns when unmounting with legacy API (no previous content)', async () => {
408 + const container = document.createElement('div');
409 + const root = ReactDOMClient.createRoot(container);
410 + root.render(<div>Hi</div>);
411 + await waitForAll([]);
412 + expect(container.textContent).toEqual('Hi');
413 + let unmounted = false;
414 + expect(() => {
415 + unmounted = ReactDOM.unmountComponentAtNode(container);
416 + }).toErrorDev(
417 + [
418 + // We care about this warning:
419 + 'You are calling ReactDOM.unmountComponentAtNode() on a container that was previously ' +
420 + 'passed to ReactDOMClient.createRoot(). This is not supported. Did you mean to call root.unmount()?',
421 + // This is more of a symptom but restructuring the code to avoid it isn't worth it:
422 + "The node you're attempting to unmount was rendered by React and is not a top-level container.",
423 + ],
424 + {withoutStack: true},
425 + );
426 + expect(unmounted).toBe(false);
427 + await waitForAll([]);
428 + expect(container.textContent).toEqual('Hi');
429 + root.unmount();
430 + await waitForAll([]);
431 + expect(container.textContent).toEqual('');
432 + });
433 +
434 + it('warns when unmounting with legacy API (has previous content)', async () => {
435 + const container = document.createElement('div');
436 + // Currently createRoot().render() doesn't clear this.
437 + container.appendChild(document.createElement('div'));
438 + // The rest is the same as test above.
439 + const root = ReactDOMClient.createRoot(container);
440 + root.render(<div>Hi</div>);
441 + await waitForAll([]);
442 + expect(container.textContent).toEqual('Hi');
443 + let unmounted = false;
444 + expect(() => {
445 + unmounted = ReactDOM.unmountComponentAtNode(container);
446 + }).toErrorDev(
447 + [
448 + 'Did you mean to call root.unmount()?',
449 + // This is more of a symptom but restructuring the code to avoid it isn't worth it:
450 + "The node you're attempting to unmount was rendered by React and is not a top-level container.",
451 + ],
452 + {withoutStack: true},
453 + );
454 + expect(unmounted).toBe(false);
455 + await waitForAll([]);
456 + expect(container.textContent).toEqual('Hi');
457 + root.unmount();
458 + await waitForAll([]);
459 + expect(container.textContent).toEqual('');
460 + });
461 +
462 + it('warns when passing legacy container to createRoot()', () => {
463 + const container = document.createElement('div');
464 + ReactDOM.render(<div>Hi</div>, container);
465 + expect(() => {
466 + ReactDOMClient.createRoot(container);
467 + }).toErrorDev(
468 + 'You are calling ReactDOMClient.createRoot() on a container that was previously ' +
469 + 'passed to ReactDOM.render(). This is not supported.',
470 + {withoutStack: true},
471 + );
472 + });
473 });