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: client text</div>
122
+ );
123
+ }
124
+ return (
125
+ <div>This markup contains an nbsp entity: 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
});