mirror of
https://github.com/facebook/react.git
synced 2025-11-01 09:12:30 +00:00
Convert ReactRenderDocument to hydrateRoot (#28153)
Co-authored-by: Sebastian Silbermann <sebastian.silbermann@klarna.com>
This commit is contained in:
co-authored by
Sebastian Silbermann
parent
bfdc12c191
commit
2cd19ed1dd
+168
-40
@@ -11,7 +11,11 @@
|
||||
|
||||
let React;
|
||||
let ReactDOM;
|
||||
let ReactDOMClient;
|
||||
let ReactDOMServer;
|
||||
let act;
|
||||
let Scheduler;
|
||||
let assertLog;
|
||||
|
||||
function getTestDocument(markup) {
|
||||
const doc = document.implementation.createHTMLDocument('');
|
||||
@@ -28,11 +32,15 @@ describe('rendering React components at document', () => {
|
||||
beforeEach(() => {
|
||||
React = require('react');
|
||||
ReactDOM = require('react-dom');
|
||||
ReactDOMClient = require('react-dom/client');
|
||||
ReactDOMServer = require('react-dom/server');
|
||||
act = require('internal-test-utils').act;
|
||||
assertLog = require('internal-test-utils').assertLog;
|
||||
Scheduler = require('scheduler');
|
||||
});
|
||||
|
||||
describe('with new explicit hydration API', () => {
|
||||
it('should be able to adopt server markup', () => {
|
||||
it('should be able to adopt server markup', async () => {
|
||||
class Root extends React.Component {
|
||||
render() {
|
||||
return (
|
||||
@@ -51,16 +59,21 @@ describe('rendering React components at document', () => {
|
||||
const testDocument = getTestDocument(markup);
|
||||
const body = testDocument.body;
|
||||
|
||||
ReactDOM.hydrate(<Root hello="world" />, testDocument);
|
||||
let root;
|
||||
await act(() => {
|
||||
root = ReactDOMClient.hydrateRoot(testDocument, <Root hello="world" />);
|
||||
});
|
||||
expect(testDocument.body.innerHTML).toBe('Hello world');
|
||||
|
||||
ReactDOM.hydrate(<Root hello="moon" />, testDocument);
|
||||
await act(() => {
|
||||
root.render(<Root hello="moon" />);
|
||||
});
|
||||
expect(testDocument.body.innerHTML).toBe('Hello moon');
|
||||
|
||||
expect(body === testDocument.body).toBe(true);
|
||||
});
|
||||
|
||||
it('should be able to unmount component from document node, but leaves singleton nodes intact', () => {
|
||||
it('should be able to unmount component from document node, but leaves singleton nodes intact', async () => {
|
||||
class Root extends React.Component {
|
||||
render() {
|
||||
return (
|
||||
@@ -76,7 +89,10 @@ describe('rendering React components at document', () => {
|
||||
|
||||
const markup = ReactDOMServer.renderToString(<Root />);
|
||||
const testDocument = getTestDocument(markup);
|
||||
ReactDOM.hydrate(<Root />, testDocument);
|
||||
let root;
|
||||
await act(() => {
|
||||
root = ReactDOMClient.hydrateRoot(testDocument, <Root />);
|
||||
});
|
||||
expect(testDocument.body.innerHTML).toBe('Hello world');
|
||||
|
||||
const originalDocEl = testDocument.documentElement;
|
||||
@@ -84,7 +100,7 @@ describe('rendering React components at document', () => {
|
||||
const originalBody = testDocument.body;
|
||||
|
||||
// When we unmount everything is removed except the singleton nodes of html, head, and body
|
||||
ReactDOM.unmountComponentAtNode(testDocument);
|
||||
root.unmount();
|
||||
expect(testDocument.firstChild).toBe(originalDocEl);
|
||||
expect(testDocument.head).toBe(originalHead);
|
||||
expect(testDocument.body).toBe(originalBody);
|
||||
@@ -92,7 +108,7 @@ describe('rendering React components at document', () => {
|
||||
expect(originalHead.firstChild).toEqual(null);
|
||||
});
|
||||
|
||||
it('should not be able to switch root constructors', () => {
|
||||
it('should not be able to switch root constructors', async () => {
|
||||
class Component extends React.Component {
|
||||
render() {
|
||||
return (
|
||||
@@ -122,17 +138,21 @@ describe('rendering React components at document', () => {
|
||||
const markup = ReactDOMServer.renderToString(<Component />);
|
||||
const testDocument = getTestDocument(markup);
|
||||
|
||||
ReactDOM.hydrate(<Component />, testDocument);
|
||||
let root;
|
||||
await act(() => {
|
||||
root = ReactDOMClient.hydrateRoot(testDocument, <Component />);
|
||||
});
|
||||
|
||||
expect(testDocument.body.innerHTML).toBe('Hello world');
|
||||
|
||||
// This works but is probably a bad idea.
|
||||
ReactDOM.hydrate(<Component2 />, testDocument);
|
||||
await act(() => {
|
||||
root.render(<Component2 />);
|
||||
});
|
||||
|
||||
expect(testDocument.body.innerHTML).toBe('Goodbye world');
|
||||
});
|
||||
|
||||
it('should be able to mount into document', () => {
|
||||
it('should be able to mount into document', async () => {
|
||||
class Component extends React.Component {
|
||||
render() {
|
||||
return (
|
||||
@@ -151,40 +171,80 @@ describe('rendering React components at document', () => {
|
||||
);
|
||||
const testDocument = getTestDocument(markup);
|
||||
|
||||
ReactDOM.hydrate(<Component text="Hello world" />, testDocument);
|
||||
await act(() => {
|
||||
ReactDOMClient.hydrateRoot(
|
||||
testDocument,
|
||||
<Component text="Hello world" />,
|
||||
);
|
||||
});
|
||||
|
||||
expect(testDocument.body.innerHTML).toBe('Hello world');
|
||||
});
|
||||
|
||||
it('cannot render over an existing text child at the root', () => {
|
||||
it('cannot render over an existing text child at the root', async () => {
|
||||
const container = document.createElement('div');
|
||||
container.textContent = 'potato';
|
||||
expect(() => ReactDOM.hydrate(<div>parsnip</div>, container)).toErrorDev(
|
||||
'Expected server HTML to contain a matching <div> in <div>.',
|
||||
|
||||
expect(() => {
|
||||
ReactDOM.flushSync(() => {
|
||||
ReactDOMClient.hydrateRoot(container, <div>parsnip</div>, {
|
||||
onRecoverableError: error => {
|
||||
Scheduler.log('Log recoverable error: ' + error.message);
|
||||
},
|
||||
});
|
||||
});
|
||||
}).toErrorDev(
|
||||
[
|
||||
'Warning: An error occurred during hydration. The server HTML was replaced with client content in <div>.',
|
||||
'Expected server HTML to contain a matching <div> in <div>.',
|
||||
],
|
||||
{withoutStack: 1},
|
||||
);
|
||||
|
||||
assertLog([
|
||||
'Log recoverable error: Hydration failed because the initial UI does not match what was rendered on the server.',
|
||||
'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.',
|
||||
]);
|
||||
|
||||
// This creates an unfortunate double text case.
|
||||
expect(container.textContent).toBe('potatoparsnip');
|
||||
expect(container.textContent).toBe('parsnip');
|
||||
});
|
||||
|
||||
it('renders over an existing nested text child without throwing', () => {
|
||||
it('renders over an existing nested text child without throwing', async () => {
|
||||
const container = document.createElement('div');
|
||||
const wrapper = document.createElement('div');
|
||||
wrapper.textContent = 'potato';
|
||||
container.appendChild(wrapper);
|
||||
expect(() =>
|
||||
ReactDOM.hydrate(
|
||||
<div>
|
||||
<div>parsnip</div>
|
||||
</div>,
|
||||
container,
|
||||
),
|
||||
).toErrorDev(
|
||||
'Expected server HTML to contain a matching <div> in <div>.',
|
||||
expect(() => {
|
||||
ReactDOM.flushSync(() => {
|
||||
ReactDOMClient.hydrateRoot(
|
||||
container,
|
||||
<div>
|
||||
<div>parsnip</div>
|
||||
</div>,
|
||||
{
|
||||
onRecoverableError: error => {
|
||||
Scheduler.log('Log recoverable error: ' + error.message);
|
||||
},
|
||||
},
|
||||
);
|
||||
});
|
||||
}).toErrorDev(
|
||||
[
|
||||
'Warning: An error occurred during hydration. The server HTML was replaced with client content in <div>.',
|
||||
'Expected server HTML to contain a matching <div> in <div>.',
|
||||
],
|
||||
{withoutStack: 1},
|
||||
);
|
||||
|
||||
assertLog([
|
||||
'Log recoverable error: Hydration failed because the initial UI does not match what was rendered on the server.',
|
||||
'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.',
|
||||
]);
|
||||
expect(container.textContent).toBe('parsnip');
|
||||
});
|
||||
|
||||
it('should give helpful errors on state desync', () => {
|
||||
it('should give helpful errors on state desync', async () => {
|
||||
class Component extends React.Component {
|
||||
render() {
|
||||
return (
|
||||
@@ -203,13 +263,45 @@ describe('rendering React components at document', () => {
|
||||
);
|
||||
const testDocument = getTestDocument(markup);
|
||||
|
||||
expect(() =>
|
||||
ReactDOM.hydrate(<Component text="Hello world" />, testDocument),
|
||||
).toErrorDev('Warning: Text content did not match.');
|
||||
const enableClientRenderFallbackOnTextMismatch = gate(
|
||||
flags => flags.enableClientRenderFallbackOnTextMismatch,
|
||||
);
|
||||
expect(() => {
|
||||
ReactDOM.flushSync(() => {
|
||||
ReactDOMClient.hydrateRoot(
|
||||
testDocument,
|
||||
<Component text="Hello world" />,
|
||||
{
|
||||
onRecoverableError: error => {
|
||||
Scheduler.log('Log recoverable error: ' + error.message);
|
||||
},
|
||||
},
|
||||
);
|
||||
});
|
||||
}).toErrorDev(
|
||||
enableClientRenderFallbackOnTextMismatch
|
||||
? [
|
||||
'Warning: An error occurred during hydration. The server HTML was replaced with client content in <#document>.',
|
||||
'Warning: Text content did not match.',
|
||||
]
|
||||
: ['Warning: Text content did not match.'],
|
||||
{
|
||||
withoutStack: enableClientRenderFallbackOnTextMismatch ? 1 : 0,
|
||||
},
|
||||
);
|
||||
|
||||
assertLog(
|
||||
enableClientRenderFallbackOnTextMismatch
|
||||
? [
|
||||
'Log recoverable error: Text content does not match server-rendered HTML.',
|
||||
'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.',
|
||||
]
|
||||
: [],
|
||||
);
|
||||
expect(testDocument.body.innerHTML).toBe('Hello world');
|
||||
});
|
||||
|
||||
it('should render w/ no markup to full document', () => {
|
||||
it('should render w/ no markup to full document', async () => {
|
||||
const testDocument = getTestDocument();
|
||||
|
||||
class Component extends React.Component {
|
||||
@@ -227,23 +319,59 @@ describe('rendering React components at document', () => {
|
||||
|
||||
if (gate(flags => flags.enableFloat)) {
|
||||
// with float the title no longer is a hydration mismatch so we get an error on the body mismatch
|
||||
expect(() =>
|
||||
ReactDOM.hydrate(<Component text="Hello world" />, testDocument),
|
||||
).toErrorDev(
|
||||
'Expected server HTML to contain a matching text node for "Hello world" in <body>',
|
||||
expect(() => {
|
||||
ReactDOM.flushSync(() => {
|
||||
ReactDOMClient.hydrateRoot(
|
||||
testDocument,
|
||||
<Component text="Hello world" />,
|
||||
{
|
||||
onRecoverableError: error => {
|
||||
Scheduler.log('Log recoverable error: ' + error.message);
|
||||
},
|
||||
},
|
||||
);
|
||||
});
|
||||
}).toErrorDev(
|
||||
[
|
||||
'Warning: An error occurred during hydration. The server HTML was replaced with client content in <#document>.',
|
||||
'Expected server HTML to contain a matching text node for "Hello world" in <body>',
|
||||
],
|
||||
{withoutStack: 1},
|
||||
);
|
||||
assertLog([
|
||||
'Log recoverable error: Hydration failed because the initial UI does not match what was rendered on the server.',
|
||||
'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.',
|
||||
]);
|
||||
} else {
|
||||
// getTestDocument() has an extra <meta> that we didn't render.
|
||||
expect(() =>
|
||||
ReactDOM.hydrate(<Component text="Hello world" />, testDocument),
|
||||
).toErrorDev(
|
||||
'Did not expect server HTML to contain a <meta> in <head>.',
|
||||
expect(() => {
|
||||
ReactDOM.flushSync(() => {
|
||||
ReactDOMClient.hydrateRoot(
|
||||
testDocument,
|
||||
<Component text="Hello world" />,
|
||||
{
|
||||
onRecoverableError: error => {
|
||||
Scheduler.log('Log recoverable error: ' + error.message);
|
||||
},
|
||||
},
|
||||
);
|
||||
});
|
||||
}).toErrorDev(
|
||||
[
|
||||
'Warning: An error occurred during hydration. The server HTML was replaced with client content in <#document>.',
|
||||
'Warning: Text content did not match. Server: "test doc" Client: "Hello World"',
|
||||
],
|
||||
{withoutStack: 1},
|
||||
);
|
||||
assertLog([
|
||||
'Log recoverable error: Text content does not match server-rendered HTML.',
|
||||
'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.',
|
||||
]);
|
||||
}
|
||||
expect(testDocument.body.innerHTML).toBe('Hello world');
|
||||
});
|
||||
|
||||
it('supports findDOMNode on full-page components', () => {
|
||||
it('supports findDOMNode on full-page components in legacy mode', () => {
|
||||
const tree = (
|
||||
<html>
|
||||
<head>
|
||||
|
||||
Reference in New Issue
Block a user