'use strict';
let React;
let ReactDOMClient;
let ReactDOMServer;
let act;
const util = require('util');
const realConsoleError = console.error;
describe('ReactDOMServerHydration', () => {
let container;
beforeEach(() => {
jest.resetModules();
React = require('react');
ReactDOMClient = require('react-dom/client');
ReactDOMServer = require('react-dom/server');
act = require('react-dom/test-utils').act;
console.error = jest.fn();
container = document.createElement('div');
document.body.appendChild(container);
});
afterEach(() => {
document.body.removeChild(container);
console.error = realConsoleError;
});
function normalizeCodeLocInfo(str) {
return (
typeof str === 'string' &&
str.replace(/\n +(?:at|in) ([\S]+)[^\n]*/g, function (m, name) {
return '\n in ' + name + ' (at **)';
})
);
}
function formatMessage(args) {
const [format, ...rest] = args;
if (format instanceof Error) {
return 'Caught [' + format.message + ']';
}
if (
format !== null &&
typeof format === 'object' &&
String(format).indexOf('Error: Uncaught [') === 0
) {
return null;
}
rest[rest.length - 1] = normalizeCodeLocInfo(rest[rest.length - 1]);
return util.format(format, ...rest);
}
function formatConsoleErrors() {
return console.error.mock.calls.map(formatMessage).filter(Boolean);
}
function testMismatch(Mismatch) {
const htmlString = ReactDOMServer.renderToString(
<Mismatch isClient={false} />,
);
container.innerHTML = htmlString;
act(() => {
ReactDOMClient.hydrateRoot(container, <Mismatch isClient={true} />);
});
return formatConsoleErrors();
}
describe('text mismatch', () => {
it('warns when client and server render different text', () => {
function Mismatch({isClient}) {
return (
<div className="parent">
<main className="child">{isClient ? 'client' : 'server'}</main>
</div>
);
}
if (gate(flags => flags.enableClientRenderFallbackOnTextMismatch)) {
expect(testMismatch(Mismatch)).toMatchInlineSnapshot(`
[
"Warning: Text content did not match. Server: "server" Client: "client"
in main (at **)
in div (at **)
in Mismatch (at **)",
"Warning: An error occurred during hydration. The server HTML was replaced with client content in <div>.",
"Caught [Text content does not match server-rendered HTML.]",
"Caught [There was an error while hydrating. Because the error happened outside of a Suspense boundary, the entire root will switch to client rendering.]",
]
`);
} else {
expect(testMismatch(Mismatch)).toMatchInlineSnapshot(`
[
"Warning: Text content did not match. Server: "server" Client: "client"
in main (at **)
in div (at **)
in Mismatch (at **)",
]
`);
}
});
it('warns when client and server render different html', () => {
function Mismatch({isClient}) {
return (
<div className="parent">
<main
className="child"
dangerouslySetInnerHTML={{
__html: isClient
? '<span>client</span>'
: '<span>server</span>',
}}
/>
</div>
);
}
expect(testMismatch(Mismatch)).toMatchInlineSnapshot(`
[
"Warning: Prop \`dangerouslySetInnerHTML\` did not match. Server: "<span>server</span>" Client: "<span>client</span>"
in main (at **)
in div (at **)
in Mismatch (at **)",
]
`);
});
});
describe('attribute mismatch', () => {
it('warns when client and server render different attributes', () => {
function Mismatch({isClient}) {
return (
<div className="parent">
<main
className={isClient ? 'child client' : 'child server'}
dir={isClient ? 'ltr' : 'rtl'}
/>
</div>
);
}
expect(testMismatch(Mismatch)).toMatchInlineSnapshot(`
[
"Warning: Prop \`className\` did not match. Server: "child server" Client: "child client"
in main (at **)
in div (at **)
in Mismatch (at **)",
]
`);
});
it('warns when client renders extra attributes', () => {
function Mismatch({isClient}) {
return (
<div className="parent">
<main
className="child"
tabIndex={isClient ? 1 : null}
dir={isClient ? 'ltr' : null}
/>
</div>
);
}
expect(testMismatch(Mismatch)).toMatchInlineSnapshot(`
[
"Warning: Prop \`tabIndex\` did not match. Server: "null" Client: "1"
in main (at **)
in div (at **)
in Mismatch (at **)",
]
`);
});
it('warns when server renders extra attributes', () => {
function Mismatch({isClient}) {
return (
<div className="parent">
<main
className="child"
tabIndex={isClient ? null : 1}
dir={isClient ? null : 'rtl'}
/>
</div>
);
}
expect(testMismatch(Mismatch)).toMatchInlineSnapshot(`
[
"Warning: Extra attributes from the server: tabindex,dir
in main (at **)
in div (at **)
in Mismatch (at **)",
]
`);
});
it('warns when both client and server render extra attributes', () => {
function Mismatch({isClient}) {
return (
<div className="parent">
<main
className="child"
tabIndex={isClient ? 1 : null}
dir={isClient ? null : 'rtl'}
/>
</div>
);
}
expect(testMismatch(Mismatch)).toMatchInlineSnapshot(`
[
"Warning: Prop \`tabIndex\` did not match. Server: "null" Client: "1"
in main (at **)
in div (at **)
in Mismatch (at **)",
]
`);
});
it('warns when client and server render different styles', () => {
function Mismatch({isClient}) {
return (
<div className="parent">
<main
className="child"
style={{
opacity: isClient ? 1 : 0,
}}
/>
</div>
);
}
expect(testMismatch(Mismatch)).toMatchInlineSnapshot(`
[
"Warning: Prop \`style\` did not match. Server: "opacity:0" Client: "opacity:1"
in main (at **)
in div (at **)
in Mismatch (at **)",
]
`);
});
});
describe('extra nodes on the client', () => {
describe('extra elements on the client', () => {
it('warns when client renders an extra element as only child', () => {
function Mismatch({isClient}) {
return (
<div className="parent">
{isClient && <main className="only" />}
</div>
);
}
expect(testMismatch(Mismatch)).toMatchInlineSnapshot(`
[
"Warning: Expected server HTML to contain a matching <main> in <div>.
in main (at **)
in div (at **)
in Mismatch (at **)",
"Warning: An error occurred during hydration. The server HTML was replaced with client content in <div>.",
"Caught [Hydration failed because the initial UI does not match what was rendered on the server.]",
"Caught [There was an error while hydrating. Because the error happened outside of a Suspense boundary, the entire root will switch to client rendering.]",
]
`);
});
it('warns when client renders an extra element in the beginning', () => {
function Mismatch({isClient}) {
return (
<div className="parent">
{isClient && <header className="1" />}
<main className="2" />
<footer className="3" />
</div>
);
}
expect(testMismatch(Mismatch)).toMatchInlineSnapshot(`
[
"Warning: Expected server HTML to contain a matching <header> in <div>.
in header (at **)
in div (at **)
in Mismatch (at **)",
"Warning: An error occurred during hydration. The server HTML was replaced with client content in <div>.",
"Caught [Hydration failed because the initial UI does not match what was rendered on the server.]",
"Caught [There was an error while hydrating. Because the error happened outside of a Suspense boundary, the entire root will switch to client rendering.]",
]
`);
});
it('warns when client renders an extra element in the middle', () => {
function Mismatch({isClient}) {
return (
<div className="parent">
<header className="1" />
{isClient && <main className="2" />}
<footer className="3" />
</div>
);
}
expect(testMismatch(Mismatch)).toMatchInlineSnapshot(`
[
"Warning: Expected server HTML to contain a matching <main> in <div>.
in main (at **)
in div (at **)
in Mismatch (at **)",
"Warning: An error occurred during hydration. The server HTML was replaced with client content in <div>.",
"Caught [Hydration failed because the initial UI does not match what was rendered on the server.]",
"Caught [There was an error while hydrating. Because the error happened outside of a Suspense boundary, the entire root will switch to client rendering.]",
]
`);
});
it('warns when client renders an extra element in the end', () => {
function Mismatch({isClient}) {
return (
<div className="parent">
<header className="1" />
<main className="2" />
{isClient && <footer className="3" />}
</div>
);
}
expect(testMismatch(Mismatch)).toMatchInlineSnapshot(`
[
"Warning: Expected server HTML to contain a matching <footer> in <div>.
in footer (at **)
in div (at **)
in Mismatch (at **)",
"Warning: An error occurred during hydration. The server HTML was replaced with client content in <div>.",
"Caught [Hydration failed because the initial UI does not match what was rendered on the server.]",
"Caught [There was an error while hydrating. Because the error happened outside of a Suspense boundary, the entire root will switch to client rendering.]",
]
`);
});
});
describe('extra text nodes on the client', () => {
it('warns when client renders an extra text node as only child', () => {
function Mismatch({isClient}) {
return <div className="parent">{isClient && 'only'}</div>;
}
if (gate(flags => flags.enableClientRenderFallbackOnTextMismatch)) {
expect(testMismatch(Mismatch)).toMatchInlineSnapshot(`
[
"Warning: Text content did not match. Server: "" Client: "only"
in div (at **)
in Mismatch (at **)",
"Warning: An error occurred during hydration. The server HTML was replaced with client content in <div>.",
"Caught [Text content does not match server-rendered HTML.]",
"Caught [There was an error while hydrating. Because the error happened outside of a Suspense boundary, the entire root will switch to client rendering.]",
]
`);
} else {
expect(testMismatch(Mismatch)).toMatchInlineSnapshot(`
[
"Warning: Text content did not match. Server: "" Client: "only"
in div (at **)
in Mismatch (at **)",
]
`);
}
});
it('warns when client renders an extra text node in the beginning', () => {
function Mismatch({isClient}) {
return (
<div className="parent">
<header className="1" />
{isClient && 'second'}
<footer className="3" />
</div>
);
}
expect(testMismatch(Mismatch)).toMatchInlineSnapshot(`
[
"Warning: Expected server HTML to contain a matching text node for "second" in <div>.
in div (at **)
in Mismatch (at **)",
"Warning: An error occurred during hydration. The server HTML was replaced with client content in <div>.",
"Caught [Hydration failed because the initial UI does not match what was rendered on the server.]",
"Caught [There was an error while hydrating. Because the error happened outside of a Suspense boundary, the entire root will switch to client rendering.]",
]
`);
});
it('warns when client renders an extra text node in the beginning', () => {
function Mismatch({isClient}) {
return (
<div className="parent">
{isClient && 'first'}
<main className="2" />
<footer className="3" />
</div>
);
}
expect(testMismatch(Mismatch)).toMatchInlineSnapshot(`
[
"Warning: Expected server HTML to contain a matching text node for "first" in <div>.
in div (at **)
in Mismatch (at **)",
"Warning: An error occurred during hydration. The server HTML was replaced with client content in <div>.",
"Caught [Hydration failed because the initial UI does not match what was rendered on the server.]",
"Caught [There was an error while hydrating. Because the error happened outside of a Suspense boundary, the entire root will switch to client rendering.]",
]
`);
});
it('warns when client renders an extra text node in the end', () => {
function Mismatch({isClient}) {
return (
<div className="parent">
<header className="1" />
<main className="2" />
{isClient && 'third'}
</div>
);
}
expect(testMismatch(Mismatch)).toMatchInlineSnapshot(`
[
"Warning: Expected server HTML to contain a matching text node for "third" in <div>.
in div (at **)
in Mismatch (at **)",
"Warning: An error occurred during hydration. The server HTML was replaced with client content in <div>.",
"Caught [Hydration failed because the initial UI does not match what was rendered on the server.]",
"Caught [There was an error while hydrating. Because the error happened outside of a Suspense boundary, the entire root will switch to client rendering.]",
]
`);
});
});
});
describe('extra nodes on the server', () => {
describe('extra elements on the server', () => {
it('warns when server renders an extra element as only child', () => {
function Mismatch({isClient}) {
return (
<div className="parent">
{!isClient && <main className="only" />}
</div>
);
}
expect(testMismatch(Mismatch)).toMatchInlineSnapshot(`
[
"Warning: Did not expect server HTML to contain a <main> in <div>.
in div (at **)
in Mismatch (at **)",
"Warning: An error occurred during hydration. The server HTML was replaced with client content in <div>.",
"Caught [Hydration failed because the initial UI does not match what was rendered on the server.]",
"Caught [There was an error while hydrating. Because the error happened outside of a Suspense boundary, the entire root will switch to client rendering.]",
]
`);
});
it('warns when server renders an extra element in the beginning', () => {
function Mismatch({isClient}) {
return (
<div className="parent">
{!isClient && <header className="1" />}
<main className="2" />
<footer className="3" />
</div>
);
}
expect(testMismatch(Mismatch)).toMatchInlineSnapshot(`
[
"Warning: Expected server HTML to contain a matching <main> in <div>.
in main (at **)
in div (at **)
in Mismatch (at **)",
"Warning: An error occurred during hydration. The server HTML was replaced with client content in <div>.",
"Caught [Hydration failed because the initial UI does not match what was rendered on the server.]",
"Caught [Hydration failed because the initial UI does not match what was rendered on the server.]",
"Caught [There was an error while hydrating. Because the error happened outside of a Suspense boundary, the entire root will switch to client rendering.]",
]
`);
});
it('warns when server renders an extra element in the middle', () => {
function Mismatch({isClient}) {
return (
<div className="parent">
<header className="1" />
{!isClient && <main className="2" />}
<footer className="3" />
</div>
);
}
expect(testMismatch(Mismatch)).toMatchInlineSnapshot(`
[
"Warning: Expected server HTML to contain a matching <footer> in <div>.
in footer (at **)
in div (at **)
in Mismatch (at **)",
"Warning: An error occurred during hydration. The server HTML was replaced with client content in <div>.",
"Caught [Hydration failed because the initial UI does not match what was rendered on the server.]",
"Caught [There was an error while hydrating. Because the error happened outside of a Suspense boundary, the entire root will switch to client rendering.]",
]
`);
});
it('warns when server renders an extra element in the end', () => {
function Mismatch({isClient}) {
return (
<div className="parent">
<header className="1" />
<main className="2" />
{!isClient && <footer className="3" />}
</div>
);
}
expect(testMismatch(Mismatch)).toMatchInlineSnapshot(`
[
"Warning: Did not expect server HTML to contain a <footer> in <div>.
in div (at **)
in Mismatch (at **)",
"Warning: An error occurred during hydration. The server HTML was replaced with client content in <div>.",
"Caught [Hydration failed because the initial UI does not match what was rendered on the server.]",
"Caught [There was an error while hydrating. Because the error happened outside of a Suspense boundary, the entire root will switch to client rendering.]",
]
`);
});
});
describe('extra text nodes on the server', () => {
it('warns when server renders an extra text node as only child', () => {
function Mismatch({isClient}) {
return <div className="parent">{!isClient && 'only'}</div>;
}
expect(testMismatch(Mismatch)).toMatchInlineSnapshot(`
[
"Warning: Did not expect server HTML to contain the text node "only" in <div>.
in div (at **)
in Mismatch (at **)",
"Warning: An error occurred during hydration. The server HTML was replaced with client content in <div>.",
"Caught [Hydration failed because the initial UI does not match what was rendered on the server.]",
"Caught [There was an error while hydrating. Because the error happened outside of a Suspense boundary, the entire root will switch to client rendering.]",
]
`);
});
it('warns when server renders an extra text node in the beginning', () => {
function Mismatch({isClient}) {
return (
<div className="parent">
{!isClient && 'first'}
<main className="2" />
<footer className="3" />
</div>
);
}
expect(testMismatch(Mismatch)).toMatchInlineSnapshot(`
[
"Warning: Expected server HTML to contain a matching <main> in <div>.
in main (at **)
in div (at **)
in Mismatch (at **)",
"Warning: An error occurred during hydration. The server HTML was replaced with client content in <div>.",
"Caught [Hydration failed because the initial UI does not match what was rendered on the server.]",
"Caught [Hydration failed because the initial UI does not match what was rendered on the server.]",
"Caught [There was an error while hydrating. Because the error happened outside of a Suspense boundary, the entire root will switch to client rendering.]",
]
`);
});
it('warns when server renders an extra text node in the middle', () => {
function Mismatch({isClient}) {
return (
<div className="parent">
<header className="1" />
{!isClient && 'second'}
<footer className="3" />
</div>
);
}
expect(testMismatch(Mismatch)).toMatchInlineSnapshot(`
[
"Warning: Expected server HTML to contain a matching <footer> in <div>.
in footer (at **)
in div (at **)
in Mismatch (at **)",
"Warning: An error occurred during hydration. The server HTML was replaced with client content in <div>.",
"Caught [Hydration failed because the initial UI does not match what was rendered on the server.]",
"Caught [There was an error while hydrating. Because the error happened outside of a Suspense boundary, the entire root will switch to client rendering.]",
]
`);
});
it('warns when server renders an extra text node in the end', () => {
function Mismatch({isClient}) {
return (
<div className="parent">
<header className="1" />
<main className="2" />
{!isClient && 'third'}
</div>
);
}
expect(testMismatch(Mismatch)).toMatchInlineSnapshot(`
[
"Warning: Did not expect server HTML to contain the text node "third" in <div>.
in div (at **)
in Mismatch (at **)",
"Warning: An error occurred during hydration. The server HTML was replaced with client content in <div>.",
"Caught [Hydration failed because the initial UI does not match what was rendered on the server.]",
"Caught [There was an error while hydrating. Because the error happened outside of a Suspense boundary, the entire root will switch to client rendering.]",
]
`);
});
});
});
describe('special nodes', () => {
describe('Suspense', () => {
function Never() {
throw new Promise(resolve => {});
}
it('warns when client renders an extra Suspense node in content mode', () => {
function Mismatch({isClient}) {
return (
<div className="parent">
{isClient && (
<React.Suspense fallback={<p>Loading...</p>}>
<main className="only" />
</React.Suspense>
)}
</div>
);
}
expect(testMismatch(Mismatch)).toMatchInlineSnapshot(`
[
"Warning: An error occurred during hydration. The server HTML was replaced with client content in <div>.",
"Caught [Hydration failed because the initial UI does not match what was rendered on the server.]",
"Caught [There was an error while hydrating. Because the error happened outside of a Suspense boundary, the entire root will switch to client rendering.]",
]
`);
});
it('warns when server renders an extra Suspense node in content mode', () => {
function Mismatch({isClient}) {
return (
<div className="parent">
{!isClient && (
<React.Suspense fallback={<p>Loading...</p>}>
<main className="only" />
</React.Suspense>
)}
</div>
);
}
expect(testMismatch(Mismatch)).toMatchInlineSnapshot(`
[
"Warning: Did not expect server HTML to contain a <main> in <div>.
in div (at **)
in Mismatch (at **)",
"Warning: An error occurred during hydration. The server HTML was replaced with client content in <div>.",
"Caught [Hydration failed because the initial UI does not match what was rendered on the server.]",
"Caught [There was an error while hydrating. Because the error happened outside of a Suspense boundary, the entire root will switch to client rendering.]",
]
`);
});
it('warns when client renders an extra Suspense node in fallback mode', () => {
function Mismatch({isClient}) {
return (
<div className="parent">
{isClient && (
<React.Suspense fallback={<p>Loading...</p>}>
<main className="only" />
<Never />
</React.Suspense>
)}
</div>
);
}
expect(testMismatch(Mismatch)).toMatchInlineSnapshot(`
[
"Warning: An error occurred during hydration. The server HTML was replaced with client content in <div>.",
"Caught [Hydration failed because the initial UI does not match what was rendered on the server.]",
"Caught [There was an error while hydrating. Because the error happened outside of a Suspense boundary, the entire root will switch to client rendering.]",
]
`);
});
it('warns when server renders an extra Suspense node in fallback mode', () => {
function Mismatch({isClient}) {
return (
<div className="parent">
{!isClient && (
<React.Suspense fallback={<p>Loading...</p>}>
<main className="only" />
<Never />
</React.Suspense>
)}
</div>
);
}
expect(testMismatch(Mismatch)).toMatchInlineSnapshot(`
[
"Warning: Did not expect server HTML to contain a <template> in <div>.
in div (at **)
in Mismatch (at **)",
"Warning: An error occurred during hydration. The server HTML was replaced with client content in <div>.",
"Caught [Hydration failed because the initial UI does not match what was rendered on the server.]",
"Caught [There was an error while hydrating. Because the error happened outside of a Suspense boundary, the entire root will switch to client rendering.]",
]
`);
});
it('warns when client renders an extra node inside Suspense content', () => {
function Mismatch({isClient}) {
return (
<div className="parent">
<React.Suspense fallback={<p>Loading...</p>}>
<header className="1" />
{isClient && <main className="second" />}
<footer className="3" />
</React.Suspense>
</div>
);
}
expect(testMismatch(Mismatch)).toMatchInlineSnapshot(`
[
"Warning: Expected server HTML to contain a matching <main> in <div>.
in main (at **)
in Suspense (at **)
in div (at **)
in Mismatch (at **)",
"Caught [Hydration failed because the initial UI does not match what was rendered on the server.]",
"Caught [There was an error while hydrating this Suspense boundary. Switched to client rendering.]",
]
`);
});
it('warns when server renders an extra node inside Suspense content', () => {
function Mismatch({isClient}) {
return (
<div className="parent">
<React.Suspense fallback={<p>Loading...</p>}>
<header className="1" />
{!isClient && <main className="second" />}
<footer className="3" />
</React.Suspense>
</div>
);
}
expect(testMismatch(Mismatch)).toMatchInlineSnapshot(`
[
"Warning: Expected server HTML to contain a matching <footer> in <div>.
in footer (at **)
in Suspense (at **)
in div (at **)
in Mismatch (at **)",
"Caught [Hydration failed because the initial UI does not match what was rendered on the server.]",
"Caught [There was an error while hydrating this Suspense boundary. Switched to client rendering.]",
]
`);
});
it('warns when client renders an extra node inside Suspense fallback', () => {
function Mismatch({isClient}) {
return (
<div className="parent">
<React.Suspense
fallback={
<>
<p>Loading...</p>
{isClient && <br />}
</>
}>
<main className="only" />
<Never />
</React.Suspense>
</div>
);
}
expect(testMismatch(Mismatch)).toMatchInlineSnapshot(`
[
"Caught [The server did not finish this Suspense boundary: The server used "renderToString" which does not support Suspense. If you intended for this Suspense boundary to render the fallback content on the server consider throwing an Error somewhere within the Suspense boundary. If you intended to have the server wait for the suspended component please switch to "renderToPipeableStream" which supports Suspense on the server]",
]
`);
});
it('warns when server renders an extra node inside Suspense fallback', () => {
function Mismatch({isClient}) {
return (
<div className="parent">
<React.Suspense
fallback={
<>
<p>Loading...</p>
{!isClient && <br />}
</>
}>
<main className="only" />
<Never />
</React.Suspense>
</div>
);
}
expect(testMismatch(Mismatch)).toMatchInlineSnapshot(`
[
"Caught [The server did not finish this Suspense boundary: The server used "renderToString" which does not support Suspense. If you intended for this Suspense boundary to render the fallback content on the server consider throwing an Error somewhere within the Suspense boundary. If you intended to have the server wait for the suspended component please switch to "renderToPipeableStream" which supports Suspense on the server]",
]
`);
});
});
describe('Fragment', () => {
it('warns when client renders an extra Fragment node', () => {
function Mismatch({isClient}) {
return (
<div className="parent">
{isClient && (
<>
<header className="1" />
<main className="2" />
<footer className="3" />
</>
)}
</div>
);
}
expect(testMismatch(Mismatch)).toMatchInlineSnapshot(`
[
"Warning: Expected server HTML to contain a matching <header> in <div>.
in header (at **)
in div (at **)
in Mismatch (at **)",
"Warning: An error occurred during hydration. The server HTML was replaced with client content in <div>.",
"Caught [Hydration failed because the initial UI does not match what was rendered on the server.]",
"Caught [Hydration failed because the initial UI does not match what was rendered on the server.]",
"Caught [Hydration failed because the initial UI does not match what was rendered on the server.]",
"Caught [There was an error while hydrating. Because the error happened outside of a Suspense boundary, the entire root will switch to client rendering.]",
]
`);
});
it('warns when server renders an extra Fragment node', () => {
function Mismatch({isClient}) {
return (
<div className="parent">
{!isClient && (
<>
<header className="1" />
<main className="2" />
<footer className="3" />
</>
)}
</div>
);
}
expect(testMismatch(Mismatch)).toMatchInlineSnapshot(`
[
"Warning: Did not expect server HTML to contain a <header> in <div>.
in div (at **)
in Mismatch (at **)",
"Warning: An error occurred during hydration. The server HTML was replaced with client content in <div>.",
"Caught [Hydration failed because the initial UI does not match what was rendered on the server.]",
"Caught [There was an error while hydrating. Because the error happened outside of a Suspense boundary, the entire root will switch to client rendering.]",
]
`);
});
});
});
describe('misc cases', () => {
it('warns when client renders an extra node deeper in the tree', () => {
function Mismatch({isClient}) {
return isClient ? <ProfileSettings /> : <MediaSettings />;
}
function ProfileSettings() {
return (
<div className="parent">
<input />
<Panel type="profile" />
</div>
);
}
function MediaSettings() {
return (
<div className="parent">
<input />
<Panel type="media" />
</div>
);
}
function Panel({type}) {
return (
<>
<header className="1" />
<main className="2" />
{type === 'profile' && <footer className="3" />}
</>
);
}
expect(testMismatch(Mismatch)).toMatchInlineSnapshot(`
[
"Warning: Expected server HTML to contain a matching <footer> in <div>.
in footer (at **)
in Panel (at **)
in div (at **)
in ProfileSettings (at **)
in Mismatch (at **)",
"Warning: An error occurred during hydration. The server HTML was replaced with client content in <div>.",
"Caught [Hydration failed because the initial UI does not match what was rendered on the server.]",
"Caught [There was an error while hydrating. Because the error happened outside of a Suspense boundary, the entire root will switch to client rendering.]",
]
`);
});
it('warns when server renders an extra node deeper in the tree', () => {
function Mismatch({isClient}) {
return isClient ? <ProfileSettings /> : <MediaSettings />;
}
function ProfileSettings() {
return (
<div className="parent">
<input />
<Panel type="profile" />
</div>
);
}
function MediaSettings() {
return (
<div className="parent">
<input />
<Panel type="media" />
</div>
);
}
function Panel({type}) {
return (
<>
<header className="1" />
<main className="2" />
{type !== 'profile' && <footer className="3" />}
</>
);
}
expect(testMismatch(Mismatch)).toMatchInlineSnapshot(`
[
"Warning: Did not expect server HTML to contain a <footer> in <div>.
in div (at **)
in ProfileSettings (at **)
in Mismatch (at **)",
"Warning: An error occurred during hydration. The server HTML was replaced with client content in <div>.",
"Caught [Hydration failed because the initial UI does not match what was rendered on the server.]",
"Caught [There was an error while hydrating. Because the error happened outside of a Suspense boundary, the entire root will switch to client rendering.]",
]
`);
});
});
});