Convert ReactServerRenderingHydration to createRoot (#28447)
Sebastian Silbermann committed
Feb 26, 2024 at 20:59 UTC
f637f6a4736a9a02ab18067ef4b1c5e033c8d6f4
1 file changed
+202
-93
packages/react-dom/src/__tests__/ReactServerRenderingHydration-test.js
+202
-93
@@ -34,7 +34,7 @@ describe('ReactDOMServerHydration', () => {
34
act = InternalTestUtils.act;
35
});
36
37
- it('should have the correct mounting behavior (new hydrate API)', () => {
37
+ it('should have the correct mounting behavior', async () => {
38
let mountCount = 0;
39
let numClicks = 0;
40
@@ -61,20 +61,29 @@ describe('ReactDOMServerHydration', () => {
61
const element = document.createElement('div');
62
document.body.appendChild(element);
63
try {
64
- ReactDOM.render(<TestComponent />, element);
64
+ let root = ReactDOMClient.createRoot(element);
65
+ await act(() => {
66
+ root.render(<TestComponent />);
67
+ });
68
69
let lastMarkup = element.innerHTML;
70
71
// Exercise the update path. Markup should not change,
72
// but some lifecycle methods should be run again.
70
- ReactDOM.render(<TestComponent name="x" />, element);
73
+ await act(() => {
74
+ root.render(<TestComponent name="x" />);
75
+ });
76
expect(mountCount).toEqual(1);
77
78
// Unmount and remount. We should get another mount event and
79
// we should get different markup, as the IDs are unique each time.
75
- ReactDOM.unmountComponentAtNode(element);
80
+ root.unmount();
81
expect(element.innerHTML).toEqual('');
77
- ReactDOM.render(<TestComponent name="x" />, element);
82
+ root = ReactDOMClient.createRoot(element);
83
+ await act(() => {
84
+ root.render(<TestComponent name="x" />);
85
+ });
86
+
87
expect(mountCount).toEqual(2);
88
expect(element.innerHTML).not.toEqual(lastMarkup);
89
@@ -82,13 +91,22 @@ describe('ReactDOMServerHydration', () => {
91
// we used server rendering. We should mount again, but the markup should
92
// be unchanged. We will append a sentinel at the end of innerHTML to be
93
// sure that innerHTML was not changed.
85
- ReactDOM.unmountComponentAtNode(element);
94
+ await act(() => {
95
+ root.unmount();
96
+ });
97
expect(element.innerHTML).toEqual('');
98
99
lastMarkup = ReactDOMServer.renderToString(<TestComponent name="x" />);
100
element.innerHTML = lastMarkup;
101
91
- let instance = ReactDOM.hydrate(<TestComponent name="x" />, element);
102
+ let instance;
103
+
104
+ root = await act(() => {
105
+ return ReactDOMClient.hydrateRoot(
106
+ element,
107
+ <TestComponent name="x" ref={current => (instance = current)} />,
108
+ );
109
+ });
110
expect(mountCount).toEqual(3);
111
expect(element.innerHTML).toBe(lastMarkup);
112
@@ -97,15 +115,41 @@ describe('ReactDOMServerHydration', () => {
115
instance.spanRef.current.click();
116
expect(numClicks).toEqual(1);
117
100
- ReactDOM.unmountComponentAtNode(element);
118
+ await act(() => {
119
+ root.unmount();
120
+ });
121
expect(element.innerHTML).toEqual('');
122
123
// Now simulate a situation where the app is not idempotent. React should
124
// warn but do the right thing.
125
element.innerHTML = lastMarkup;
106
- expect(() => {
107
- instance = ReactDOM.hydrate(<TestComponent name="y" />, element);
108
- }).toErrorDev('Text content did not match. Server: "x" Client: "y"');
126
+ const enableClientRenderFallbackOnTextMismatch = gate(
127
+ flags => flags.enableClientRenderFallbackOnTextMismatch,
128
+ );
129
+ await expect(async () => {
130
+ root = await act(() => {
131
+ return ReactDOMClient.hydrateRoot(
132
+ element,
133
+ <TestComponent
134
+ name="y"
135
+ ref={current => {
136
+ instance = current;
137
+ }}
138
+ />,
139
+ {
140
+ onRecoverableError: error => {},
141
+ },
142
+ );
143
+ });
144
+ }).toErrorDev(
145
+ enableClientRenderFallbackOnTextMismatch
146
+ ? [
147
+ 'An error occurred during hydration. The server HTML was replaced with client content in <div>.',
148
+ 'Text content did not match. Server: "x" Client: "y"',
149
+ ]
150
+ : ['Text content did not match. Server: "x" Client: "y"'],
151
+ {withoutStack: enableClientRenderFallbackOnTextMismatch ? 1 : 0},
152
+ );
153
expect(mountCount).toEqual(4);
154
expect(element.innerHTML.length > 0).toBe(true);
155
expect(element.innerHTML).not.toEqual(lastMarkup);
@@ -164,25 +208,46 @@ describe('ReactDOMServerHydration', () => {
208
});
209
210
// Regression test for https://github.com/facebook/react/issues/11726
167
- it('should not focus on either server or client with autofocus={false} even if there is a markup mismatch', () => {
211
+ it('should not focus on either server or client with autofocus={false} even if there is a markup mismatch', async () => {
212
const element = document.createElement('div');
213
element.innerHTML = ReactDOMServer.renderToString(
214
<button autoFocus={false}>server</button>,
215
);
216
expect(element.firstChild.autofocus).toBe(false);
217
+ const onFocusBeforeHydration = jest.fn();
218
+ const onFocusAfterHydration = jest.fn();
219
+ element.firstChild.focus = onFocusBeforeHydration;
220
174
- element.firstChild.focus = jest.fn();
175
-
176
- expect(() =>
177
- ReactDOM.hydrate(<button autoFocus={false}>client</button>, element),
178
- ).toErrorDev(
179
- 'Warning: Text content did not match. Server: "server" Client: "client"',
221
+ const enableClientRenderFallbackOnTextMismatch = gate(
222
+ flags => flags.enableClientRenderFallbackOnTextMismatch,
223
+ );
224
+ await expect(async () => {
225
+ await act(() => {
226
+ ReactDOMClient.hydrateRoot(
227
+ element,
228
+ <button autoFocus={false} onFocus={onFocusAfterHydration}>
229
+ client
230
+ </button>,
231
+ {onRecoverableError: error => {}},
232
+ );
233
+ });
234
+ }).toErrorDev(
235
+ enableClientRenderFallbackOnTextMismatch
236
+ ? [
237
+ 'An error occurred during hydration. The server HTML was replaced with client content in <div>.',
238
+ 'Warning: Text content did not match. Server: "server" Client: "client"',
239
+ ]
240
+ : [
241
+ 'Warning: Text content did not match. Server: "server" Client: "client"',
242
+ ],
243
+ {withoutStack: enableClientRenderFallbackOnTextMismatch ? 1 : 0},
244
);
245
182
- expect(element.firstChild.focus).not.toHaveBeenCalled();
246
+ expect(onFocusBeforeHydration).not.toHaveBeenCalled();
247
+ expect(onFocusAfterHydration).not.toHaveBeenCalled();
248
});
249
185
- it('should warn when the style property differs', () => {
250
+ it('should warn when the style property differs', async () => {
251
const element = document.createElement('div');
252
element.innerHTML = ReactDOMServer.renderToString(
253
<div style={{textDecoration: 'none', color: 'black', height: '10px'}} />,
@@ -190,26 +255,27 @@ describe('ReactDOMServerHydration', () => {
255
expect(element.firstChild.style.textDecoration).toBe('none');
256
expect(element.firstChild.style.color).toBe('black');
257
193
- expect(() =>
194
- ReactDOM.hydrate(
195
- <div
196
- style={{textDecoration: 'none', color: 'white', height: '10px'}}
197
- />,
198
- element,
199
- ),
200
- ).toErrorDev(
258
+ await expect(async () => {
259
+ await act(() => {
260
+ ReactDOMClient.hydrateRoot(
261
+ element,
262
+ <div
263
+ style={{textDecoration: 'none', color: 'white', height: '10px'}}
264
+ />,
265
+ );
266
+ });
267
+ }).toErrorDev(
268
'Warning: Prop `style` did not match. Server: ' +
269
'"text-decoration:none;color:black;height:10px" Client: ' +
270
'"text-decoration:none;color:white;height:10px"',
271
);
272
});
273
207
- // @gate !disableIEWorkarounds || !__DEV__
208
- it('should not warn when the style property differs on whitespace or order in IE', () => {
274
+ it('should not warn when the style property differs on whitespace or order in IE', async () => {
275
document.documentMode = 11;
276
jest.resetModules();
277
React = require('react');
212
- ReactDOM = require('react-dom');
278
+ ReactDOMClient = require('react-dom/client');
279
ReactDOMServer = require('react-dom/server');
280
try {
281
const element = document.createElement('div');
@@ -219,33 +285,35 @@ describe('ReactDOMServerHydration', () => {
285
element.innerHTML =
286
'<div style="height: 10px; color: black; text-decoration: none;"></div>';
287
222
- // We don't expect to see false positive warnings.
223
- // https://github.com/facebook/react/issues/11807
224
- ReactDOM.hydrate(
225
- <div
226
- style={{textDecoration: 'none', color: 'black', height: '10px'}}
227
- />,
228
- element,
229
- );
288
+ await act(() => {
289
+ ReactDOMClient.hydrateRoot(
290
+ element,
291
+ <div
292
+ style={{textDecoration: 'none', color: 'black', height: '10px'}}
293
+ />,
294
+ );
295
+ });
296
} finally {
297
delete document.documentMode;
298
}
299
});
300
235
- it('should warn when the style property differs on whitespace in non-IE browsers', () => {
301
+ it('should warn when the style property differs on whitespace in non-IE browsers', async () => {
302
const element = document.createElement('div');
303
304
element.innerHTML =
305
'<div style="text-decoration: none; color: black; height: 10px;"></div>';
306
241
- expect(() =>
242
- ReactDOM.hydrate(
243
- <div
244
- style={{textDecoration: 'none', color: 'black', height: '10px'}}
245
- />,
246
- element,
247
- ),
248
- ).toErrorDev(
307
+ await expect(async () => {
308
+ await act(() => {
309
+ ReactDOMClient.hydrateRoot(
310
+ element,
311
+ <div
312
+ style={{textDecoration: 'none', color: 'black', height: '10px'}}
313
+ />,
314
+ );
315
+ });
316
+ }).toErrorDev(
317
'Warning: Prop `style` did not match. Server: ' +
318
'"text-decoration: none; color: black; height: 10px;" Client: ' +
319
'"text-decoration:none;color:black;height:10px"',
@@ -264,7 +332,7 @@ describe('ReactDOMServerHydration', () => {
332
);
333
});
334
267
- it('should be able to render and hydrate Mode components', () => {
335
+ it('should be able to render and hydrate Mode components', async () => {
336
class ComponentWithWarning extends React.Component {
337
componentWillMount() {
338
// Expected warning
@@ -286,15 +354,17 @@ describe('ReactDOMServerHydration', () => {
354
}).toWarnDev('componentWillMount has been renamed');
355
expect(element.textContent).toBe('Hi');
356
289
- expect(() => {
290
- ReactDOM.hydrate(markup, element);
357
+ await expect(async () => {
358
+ await act(() => {
359
+ ReactDOMClient.hydrateRoot(element, markup);
360
+ });
361
}).toWarnDev('componentWillMount has been renamed', {
362
withoutStack: true,
363
});
364
expect(element.textContent).toBe('Hi');
365
});
366
297
- it('should be able to render and hydrate forwardRef components', () => {
367
+ it('should be able to render and hydrate forwardRef components', async () => {
368
const FunctionComponent = ({label, forwardedRef}) => (
369
<div ref={forwardedRef}>{label}</div>
370
);
@@ -310,12 +380,14 @@ describe('ReactDOMServerHydration', () => {
380
expect(element.textContent).toBe('Hi');
381
expect(ref.current).toBe(null);
382
313
- ReactDOM.hydrate(markup, element);
383
+ await act(() => {
384
+ ReactDOMClient.hydrateRoot(element, markup);
385
+ });
386
expect(element.textContent).toBe('Hi');
387
expect(ref.current.tagName).toBe('DIV');
388
});
389
318
- it('should be able to render and hydrate Profiler components', () => {
390
+ it('should be able to render and hydrate Profiler components', async () => {
391
const callback = jest.fn();
392
const markup = (
393
<React.Profiler id="profiler" onRender={callback}>
@@ -328,7 +400,9 @@ describe('ReactDOMServerHydration', () => {
400
expect(element.textContent).toBe('Hi');
401
expect(callback).not.toHaveBeenCalled();
402
331
- ReactDOM.hydrate(markup, element);
403
+ await act(() => {
404
+ ReactDOMClient.hydrateRoot(element, markup);
405
+ });
406
expect(element.textContent).toBe('Hi');
407
if (__DEV__) {
408
expect(callback).toHaveBeenCalledTimes(1);
@@ -341,7 +415,7 @@ describe('ReactDOMServerHydration', () => {
415
});
416
417
// Regression test for https://github.com/facebook/react/issues/11423
344
- it('should ignore noscript content on the client and not warn about mismatches', () => {
418
+ it('should ignore noscript content on the client and not warn about mismatches', async () => {
419
const callback = jest.fn();
420
const TestComponent = ({onRender}) => {
421
onRender();
@@ -360,10 +434,9 @@ describe('ReactDOMServerHydration', () => {
434
'<div>Enable JavaScript to run this app.</div>',
435
);
436
363
- // On the client we want to keep the existing markup, but not render the
364
- // actual elements for performance reasons and to avoid for example
365
- // downloading images. This should also not warn for hydration mismatches.
366
- ReactDOM.hydrate(markup, element);
437
+ await act(() => {
438
+ ReactDOMClient.hydrateRoot(element, markup);
439
+ });
440
expect(callback).toHaveBeenCalledTimes(1);
441
expect(element.textContent).toBe(
442
'<div>Enable JavaScript to run this app.</div>',
@@ -371,18 +444,17 @@ describe('ReactDOMServerHydration', () => {
444
});
445
446
it('should be able to use lazy components after hydrating', async () => {
447
+ let resolveLazy;
448
const Lazy = React.lazy(
449
() =>
450
new Promise(resolve => {
377
- setTimeout(
378
- () =>
379
- resolve({
380
- default: function World() {
381
- return 'world';
382
- },
383
- }),
384
- 1000,
385
- );
451
+ resolveLazy = () => {
452
+ resolve({
453
+ default: function World() {
454
+ return 'world';
455
+ },
456
+ });
457
+ };
458
}),
459
);
460
class HelloWorld extends React.Component {
@@ -410,11 +482,13 @@ describe('ReactDOMServerHydration', () => {
482
element.innerHTML = ReactDOMServer.renderToString(<HelloWorld />);
483
expect(element.textContent).toBe('Hello ');
484
413
- ReactDOM.hydrate(<HelloWorld />, element);
485
+ await act(() => {
486
+ ReactDOMClient.hydrateRoot(element, <HelloWorld />);
487
+ });
488
expect(element.textContent).toBe('Hello loading');
489
490
// Resolve Lazy component
417
- await act(() => jest.runAllTimers());
491
+ await act(() => resolveLazy());
492
expect(element.textContent).toBe('Hello world');
493
});
494
@@ -497,7 +571,7 @@ describe('ReactDOMServerHydration', () => {
571
});
572
573
// regression test for https://github.com/facebook/react/issues/17170
500
- it('should not warn if dangerouslySetInnerHtml=undefined', () => {
574
+ it('should not warn if dangerouslySetInnerHtml=undefined', async () => {
575
const domElement = document.createElement('div');
576
const reactElement = (
577
<div dangerouslySetInnerHTML={undefined}>
@@ -507,49 +581,75 @@ describe('ReactDOMServerHydration', () => {
581
const markup = ReactDOMServer.renderToStaticMarkup(reactElement);
582
domElement.innerHTML = markup;
583
510
- ReactDOM.hydrate(reactElement, domElement);
584
+ await act(() => {
585
+ ReactDOMClient.hydrateRoot(domElement, reactElement);
586
+ });
587
588
expect(domElement.innerHTML).toEqual(markup);
589
});
590
515
- it('should warn if innerHTML mismatches with dangerouslySetInnerHTML=undefined and children on the client', () => {
591
+ it('should warn if innerHTML mismatches with dangerouslySetInnerHTML=undefined and children on the client', async () => {
592
const domElement = document.createElement('div');
593
const markup = ReactDOMServer.renderToStaticMarkup(
594
<div dangerouslySetInnerHTML={{__html: '<p>server</p>'}} />,
595
);
596
domElement.innerHTML = markup;
597
522
- expect(() => {
523
- ReactDOM.hydrate(
524
- <div dangerouslySetInnerHTML={undefined}>
525
- <p>client</p>
526
- </div>,
527
- domElement,
528
- );
598
+ const enableClientRenderFallbackOnTextMismatch = gate(
599
+ flags => flags.enableClientRenderFallbackOnTextMismatch,
600
+ );
601
+ await expect(async () => {
602
+ await act(() => {
603
+ ReactDOMClient.hydrateRoot(
604
+ domElement,
605
+ <div dangerouslySetInnerHTML={undefined}>
606
+ <p>client</p>
607
+ </div>,
608
+ {onRecoverableError: error => {}},
609
+ );
610
+ });
611
612
expect(domElement.innerHTML).not.toEqual(markup);
613
}).toErrorDev(
532
- 'Warning: Text content did not match. Server: "server" Client: "client"',
614
+ enableClientRenderFallbackOnTextMismatch
615
+ ? [
616
+ 'An error occurred during hydration. The server HTML was replaced with client content in <div>.',
617
+ 'Warning: Text content did not match. Server: "server" Client: "client"',
618
+ ]
619
+ : [
620
+ 'Warning: Text content did not match. Server: "server" Client: "client"',
621
+ ],
622
+ {withoutStack: enableClientRenderFallbackOnTextMismatch ? 1 : 0},
623
);
624
});
625
536
- it('should warn if innerHTML mismatches with dangerouslySetInnerHTML=undefined on the client', () => {
626
+ it('should warn if innerHTML mismatches with dangerouslySetInnerHTML=undefined on the client', async () => {
627
const domElement = document.createElement('div');
628
const markup = ReactDOMServer.renderToStaticMarkup(
629
<div dangerouslySetInnerHTML={{__html: '<p>server</p>'}} />,
630
);
631
domElement.innerHTML = markup;
632
543
- expect(() => {
544
- ReactDOM.hydrate(<div dangerouslySetInnerHTML={undefined} />, domElement);
633
+ await expect(async () => {
634
+ await act(() => {
635
+ ReactDOMClient.hydrateRoot(
636
+ domElement,
637
+ <div dangerouslySetInnerHTML={undefined} />,
638
+ {onRecoverableError: error => {}},
639
+ );
640
+ });
641
642
expect(domElement.innerHTML).not.toEqual(markup);
643
}).toErrorDev(
548
- 'Warning: Did not expect server HTML to contain a <p> in <div>',
644
+ [
645
+ 'An error occurred during hydration. The server HTML was replaced with client content in <div>.',
646
+ 'Warning: Did not expect server HTML to contain a <p> in <div>.',
647
+ ],
648
+ {withoutStack: 1},
649
);
650
});
651
552
- it('should warn when hydrating read-only properties', () => {
652
+ it('should warn when hydrating read-only properties', async () => {
653
const readOnlyProperties = [
654
'offsetParent',
655
'offsetTop',
@@ -560,24 +660,31 @@ describe('ReactDOMServerHydration', () => {
660
'outerText',
661
'outerHTML',
662
];
563
- readOnlyProperties.forEach(readOnlyProperty => {
663
+ // eslint-disable-next-line no-for-of-loops/no-for-of-loops
664
+ for (const readOnlyProperty of readOnlyProperties) {
665
const props = {};
666
props[readOnlyProperty] = 'hello';
667
const jsx = React.createElement('my-custom-element', props);
668
const element = document.createElement('div');
669
element.innerHTML = ReactDOMServer.renderToString(jsx);
670
if (gate(flags => flags.enableCustomElementPropertySupport)) {
570
- expect(() => ReactDOM.hydrate(jsx, element)).toErrorDev(
671
+ await expect(async () => {
672
+ await act(() => {
673
+ ReactDOMClient.hydrateRoot(element, jsx);
674
+ });
675
+ }).toErrorDev(
676
`Warning: Assignment to read-only property will result in a no-op: \`${readOnlyProperty}\``,
677
);
678
} else {
574
- ReactDOM.hydrate(jsx, element);
679
+ await act(() => {
680
+ ReactDOMClient.hydrateRoot(element, jsx);
681
+ });
682
}
576
- });
683
+ }
684
});
685
686
// @gate enableCustomElementPropertySupport
580
- it('should not re-assign properties on hydration', () => {
687
+ it('should not re-assign properties on hydration', async () => {
688
const container = document.createElement('div');
689
document.body.appendChild(container);
690
@@ -607,7 +714,9 @@ describe('ReactDOMServerHydration', () => {
714
},
715
});
716
610
- ReactDOM.hydrate(jsx, container);
717
+ await act(() => {
718
+ ReactDOMClient.hydrateRoot(container, jsx);
719
+ });
720
721
expect(customElement.getAttribute('str')).toBe('string');
722
expect(customElement.getAttribute('obj')).toBe(null);