Skip to content

Commit 5420c4e

Browse files
authored
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`.
1 parent 11c9fd0 commit 5420c4e

File tree

3 files changed

+263
-140
lines changed

3 files changed

+263
-140
lines changed

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

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -113,6 +113,43 @@ describe('ReactDOMServerHydration', () => {
113113
}
114114
});
115115

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+
116153
// @gate __DEV__
117154
it('warns when client and server render different html', () => {
118155
function Mismatch({isClient}) {

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

Lines changed: 93 additions & 140 deletions
Original file line numberDiff line numberDiff line change
@@ -175,27 +175,6 @@ describe('ReactDOMRoot', () => {
175175
);
176176
});
177177

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-
199178
it('clears existing children', async () => {
200179
container.innerHTML = '<div>a</div><div>b</div>';
201180
const root = ReactDOMClient.createRoot(container);
@@ -223,122 +202,6 @@ describe('ReactDOMRoot', () => {
223202
}).toThrow('createRoot(...): Target container is not a DOM element.');
224203
});
225204

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-
342205
it('warns when creating two roots managing the same container', () => {
343206
ReactDOMClient.createRoot(container);
344207
expect(() => {
@@ -399,6 +262,80 @@ describe('ReactDOMRoot', () => {
399262
}
400263
});
401264

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+
402339
it('throws if unmounting a root that has had its contents removed', async () => {
403340
const root = ReactDOMClient.createRoot(container);
404341
await act(() => {
@@ -514,9 +451,6 @@ describe('ReactDOMRoot', () => {
514451
expect(() => ReactDOMClient.hydrateRoot(commentNode)).toThrow(
515452
'hydrateRoot(...): Target container is not a DOM element.',
516453
);
517-
518-
// Still works in the legacy API
519-
ReactDOM.render(<div />, commentNode);
520454
});
521455

522456
it('warn if no children passed to hydrateRoot', async () => {
@@ -539,4 +473,23 @@ describe('ReactDOMRoot', () => {
539473
},
540474
);
541475
});
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+
});
542495
});

0 commit comments

Comments
 (0)