Skip to content

Commit 415092e

Browse files
rickhanloniiSebastian Silbermann
authored andcommitted
Convert ReactRenderDocument to hydrateRoot (facebook#28153)
Co-authored-by: Sebastian Silbermann <sebastian.silbermann@klarna.com>
1 parent e9efab4 commit 415092e

File tree

1 file changed

+168
-40
lines changed

1 file changed

+168
-40
lines changed

packages/react-dom/src/__tests__/ReactRenderDocument-test.js

+168-40
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,11 @@
1111

1212
let React;
1313
let ReactDOM;
14+
let ReactDOMClient;
1415
let ReactDOMServer;
16+
let act;
17+
let Scheduler;
18+
let assertLog;
1519

1620
function getTestDocument(markup) {
1721
const doc = document.implementation.createHTMLDocument('');
@@ -28,11 +32,15 @@ describe('rendering React components at document', () => {
2832
beforeEach(() => {
2933
React = require('react');
3034
ReactDOM = require('react-dom');
35+
ReactDOMClient = require('react-dom/client');
3136
ReactDOMServer = require('react-dom/server');
37+
act = require('internal-test-utils').act;
38+
assertLog = require('internal-test-utils').assertLog;
39+
Scheduler = require('scheduler');
3240
});
3341

3442
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 () => {
3644
class Root extends React.Component {
3745
render() {
3846
return (
@@ -51,16 +59,21 @@ describe('rendering React components at document', () => {
5159
const testDocument = getTestDocument(markup);
5260
const body = testDocument.body;
5361

54-
ReactDOM.hydrate(<Root hello="world" />, testDocument);
62+
let root;
63+
await act(() => {
64+
root = ReactDOMClient.hydrateRoot(testDocument, <Root hello="world" />);
65+
});
5566
expect(testDocument.body.innerHTML).toBe('Hello world');
5667

57-
ReactDOM.hydrate(<Root hello="moon" />, testDocument);
68+
await act(() => {
69+
root.render(<Root hello="moon" />);
70+
});
5871
expect(testDocument.body.innerHTML).toBe('Hello moon');
5972

6073
expect(body === testDocument.body).toBe(true);
6174
});
6275

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 () => {
6477
class Root extends React.Component {
6578
render() {
6679
return (
@@ -76,23 +89,26 @@ describe('rendering React components at document', () => {
7689

7790
const markup = ReactDOMServer.renderToString(<Root />);
7891
const testDocument = getTestDocument(markup);
79-
ReactDOM.hydrate(<Root />, testDocument);
92+
let root;
93+
await act(() => {
94+
root = ReactDOMClient.hydrateRoot(testDocument, <Root />);
95+
});
8096
expect(testDocument.body.innerHTML).toBe('Hello world');
8197

8298
const originalDocEl = testDocument.documentElement;
8399
const originalHead = testDocument.head;
84100
const originalBody = testDocument.body;
85101

86102
// When we unmount everything is removed except the singleton nodes of html, head, and body
87-
ReactDOM.unmountComponentAtNode(testDocument);
103+
root.unmount();
88104
expect(testDocument.firstChild).toBe(originalDocEl);
89105
expect(testDocument.head).toBe(originalHead);
90106
expect(testDocument.body).toBe(originalBody);
91107
expect(originalBody.firstChild).toEqual(null);
92108
expect(originalHead.firstChild).toEqual(null);
93109
});
94110

95-
it('should not be able to switch root constructors', () => {
111+
it('should not be able to switch root constructors', async () => {
96112
class Component extends React.Component {
97113
render() {
98114
return (
@@ -122,17 +138,21 @@ describe('rendering React components at document', () => {
122138
const markup = ReactDOMServer.renderToString(<Component />);
123139
const testDocument = getTestDocument(markup);
124140

125-
ReactDOM.hydrate(<Component />, testDocument);
141+
let root;
142+
await act(() => {
143+
root = ReactDOMClient.hydrateRoot(testDocument, <Component />);
144+
});
126145

127146
expect(testDocument.body.innerHTML).toBe('Hello world');
128147

129-
// This works but is probably a bad idea.
130-
ReactDOM.hydrate(<Component2 />, testDocument);
148+
await act(() => {
149+
root.render(<Component2 />);
150+
});
131151

132152
expect(testDocument.body.innerHTML).toBe('Goodbye world');
133153
});
134154

135-
it('should be able to mount into document', () => {
155+
it('should be able to mount into document', async () => {
136156
class Component extends React.Component {
137157
render() {
138158
return (
@@ -151,40 +171,80 @@ describe('rendering React components at document', () => {
151171
);
152172
const testDocument = getTestDocument(markup);
153173

154-
ReactDOM.hydrate(<Component text="Hello world" />, testDocument);
174+
await act(() => {
175+
ReactDOMClient.hydrateRoot(
176+
testDocument,
177+
<Component text="Hello world" />,
178+
);
179+
});
155180

156181
expect(testDocument.body.innerHTML).toBe('Hello world');
157182
});
158183

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 () => {
160185
const container = document.createElement('div');
161186
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},
164202
);
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+
165209
// This creates an unfortunate double text case.
166-
expect(container.textContent).toBe('potatoparsnip');
210+
expect(container.textContent).toBe('parsnip');
167211
});
168212

169-
it('renders over an existing nested text child without throwing', () => {
213+
it('renders over an existing nested text child without throwing', async () => {
170214
const container = document.createElement('div');
171215
const wrapper = document.createElement('div');
172216
wrapper.textContent = 'potato';
173217
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},
183238
);
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+
]);
184244
expect(container.textContent).toBe('parsnip');
185245
});
186246

187-
it('should give helpful errors on state desync', () => {
247+
it('should give helpful errors on state desync', async () => {
188248
class Component extends React.Component {
189249
render() {
190250
return (
@@ -203,13 +263,45 @@ describe('rendering React components at document', () => {
203263
);
204264
const testDocument = getTestDocument(markup);
205265

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+
);
209301
expect(testDocument.body.innerHTML).toBe('Hello world');
210302
});
211303

212-
it('should render w/ no markup to full document', () => {
304+
it('should render w/ no markup to full document', async () => {
213305
const testDocument = getTestDocument();
214306

215307
class Component extends React.Component {
@@ -227,23 +319,59 @@ describe('rendering React components at document', () => {
227319

228320
if (gate(flags => flags.enableFloat)) {
229321
// 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},
234340
);
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+
]);
235345
} else {
236346
// 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},
241365
);
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+
]);
242370
}
243371
expect(testDocument.body.innerHTML).toBe('Hello world');
244372
});
245373

246-
it('supports findDOMNode on full-page components', () => {
374+
it('supports findDOMNode on full-page components in legacy mode', () => {
247375
const tree = (
248376
<html>
249377
<head>

0 commit comments

Comments
 (0)