/*** Copyright (c) Meta Platforms, Inc. and affiliates.** This source code is licensed under the MIT license found in the* LICENSE file in the root directory of this source tree.** @emails react-core* @jest-environment ./scripts/jest/ReactDOMServerIntegrationEnvironment*/'use strict';
let Activity;
let React = require('react');
let ReactDOM;
let ReactDOMClient;
let ReactDOMServer;
let ReactFeatureFlags;
let Scheduler;
let Suspense;
let SuspenseList;
let useSyncExternalStore;
let act;
let IdleEventPriority;
let waitForAll;
let waitFor;
let waitForPaint;
let assertLog;
function normalizeCodeLocInfo(strOrErr) {
if (strOrErr && strOrErr.replace) {
return strOrErr.replace(/\n +(?:at|in) ([\S]+)[^\n]*/g, function (m, name) {
return '\n in ' + name + ' (at **)';
});}return strOrErr;
}function dispatchMouseEvent(to, from) {
if (!to) {
to = null;
}if (!from) {
from = null;
}if (from) {
const mouseOutEvent = document.createEvent('MouseEvents');
mouseOutEvent.initMouseEvent(
'mouseout',
true,
true,
window,0,
50,
50,
50,
50,
false,
false,
false,
false,
0,
to,
);from.dispatchEvent(mouseOutEvent);
}if (to) {
const mouseOverEvent = document.createEvent('MouseEvents');
mouseOverEvent.initMouseEvent(
'mouseover',
true,
true,
window,0,
50,
50,
50,
50,
false,
false,
false,
false,
0,
from,
);to.dispatchEvent(mouseOverEvent);
}}class TestAppClass extends React.Component {
render() {
return (
<div>
<>{''}</>
<>{'Hello'}</>
</div>
);}}describe('ReactDOMServerPartialHydration', () => {beforeEach(() => {jest.resetModules();
ReactFeatureFlags = require('shared/ReactFeatureFlags');ReactFeatureFlags.enableSuspenseCallback = true;
ReactFeatureFlags.enableCreateEventHandleAPI = true;
React = require('react');ReactDOM = require('react-dom');ReactDOMClient = require('react-dom/client');act = require('internal-test-utils').act;
ReactDOMServer = require('react-dom/server');Scheduler = require('scheduler');Activity = React.unstable_Activity;
Suspense = React.Suspense;
useSyncExternalStore = React.useSyncExternalStore;
if (gate(flags => flags.enableSuspenseList)) {
SuspenseList = React.unstable_SuspenseList;
}const InternalTestUtils = require('internal-test-utils');waitForAll = InternalTestUtils.waitForAll;
assertLog = InternalTestUtils.assertLog;
waitForPaint = InternalTestUtils.waitForPaint;
waitFor = InternalTestUtils.waitFor;
IdleEventPriority = require('react-reconciler/constants').IdleEventPriority;
});// Note: This is based on a similar component we use in www. We can delete
// once the extra div wrapper is no longer necessary.
function LegacyHiddenDiv({children, mode}) {return (<div hidden={mode === 'hidden'}><React.unstable_LegacyHidden
mode={mode === 'hidden' ? 'unstable-defer-without-hiding' : mode}>
{children}</React.unstable_LegacyHidden>
</div>);}it('hydrates a parent even if a child Suspense boundary is blocked', async () => {let suspend = false;let resolve;const promise = new Promise(resolvePromise => (resolve = resolvePromise));const ref = React.createRef();
function Child() {if (suspend) {throw promise;} else {return 'Hello';}}function App() {return (<div><Suspense fallback="Loading...">
<span ref={ref}><Child /></span></Suspense></div>);}// First we render the final HTML. With the streaming renderer
// this may have suspense points on the server but here we want// to test the completed HTML. Don't suspend on the server.
suspend = false;const finalHTML = ReactDOMServer.renderToString(<App />);
const container = document.createElement('div');
container.innerHTML = finalHTML;
const span = container.getElementsByTagName('span')[0];
// On the client we don't have all data yet but we want to start// hydrating anyway.
suspend = true;ReactDOMClient.hydrateRoot(container, <App />);
await waitForAll([]);
expect(ref.current).toBe(null);
// Resolving the promise should continue hydrationsuspend = false;resolve();await promise;await waitForAll([]);
// We should now have hydrated with a ref on the existing span.
expect(ref.current).toBe(span);
});it('can hydrate siblings of a suspended component without errors', async () => {let suspend = false;let resolve;const promise = new Promise(resolvePromise => (resolve = resolvePromise));function Child() {if (suspend) {throw promise;} else {return 'Hello';}}function App() {return (<Suspense fallback="Loading...">
<Child /><Suspense fallback="Loading...">
<div>Hello</div></Suspense></Suspense>);}// First we render the final HTML. With the streaming renderer
// this may have suspense points on the server but here we want// to test the completed HTML. Don't suspend on the server.
suspend = false;const finalHTML = ReactDOMServer.renderToString(<App />);
const container = document.createElement('div');
container.innerHTML = finalHTML;
expect(container.textContent).toBe('HelloHello');
// On the client we don't have all data yet but we want to start// hydrating anyway.
suspend = true;ReactDOMClient.hydrateRoot(container, <App />, {
onRecoverableError(error) {Scheduler.log(error.message);
},});await waitForAll([]);
// Expect the server-generated HTML to stay intact.
expect(container.textContent).toBe('HelloHello');
// Resolving the promise should continue hydrationsuspend = false;resolve();await promise;await waitForAll([]);
// Hydration should not change anything.
expect(container.textContent).toBe('HelloHello');
});it('falls back to client rendering boundary on mismatch', async () => {// We can't use the toErrorDev helper here because this is async.
const originalConsoleError = console.error;
const mockError = jest.fn();
console.error = (...args) => {
mockError(...args.map(normalizeCodeLocInfo));
};let client = false;let suspend = false;let resolve;const promise = new Promise(resolvePromise => {resolve = () => {suspend = false;resolvePromise();};});function Child() {if (suspend) {Scheduler.log('Suspend');
throw promise;} else {Scheduler.log('Hello');
return 'Hello';}}function Component({shouldMismatch}) {Scheduler.log('Component');
if (shouldMismatch && client) {return <article>Mismatch</article>;}return <div>Component</div>;}function App() {return (<Suspense fallback="Loading...">
<Child /><Component /><Component /><Component /><Component shouldMismatch={true} /></Suspense>);}try {const finalHTML = ReactDOMServer.renderToString(<App />);
const container = document.createElement('section');
container.innerHTML = finalHTML;
assertLog(['Hello', 'Component', 'Component', 'Component', 'Component']);
expect(container.innerHTML).toBe(
'<!--$-->Hello<div>Component</div><div>Component</div><div>Component</div><div>Component</div><!--/$-->',
);suspend = true;client = true;ReactDOMClient.hydrateRoot(container, <App />, {
onRecoverableError(error) {Scheduler.log(error.message);
},});await waitForAll(['Suspend']);
jest.runAllTimers();
// Unchangedexpect(container.innerHTML).toBe(
'<!--$-->Hello<div>Component</div><div>Component</div><div>Component</div><div>Component</div><!--/$-->',
);suspend = false;resolve();await promise;await waitForAll([
// first pass, mismatches at end'Hello','Component','Component','Component','Component',// second pass as client render'Hello','Component','Component','Component','Component',// Hydration mismatch is logged'Hydration failed because the initial UI does not match what was rendered on the server.','There was an error while hydrating this Suspense boundary. Switched to client rendering.',]);
// Client rendered - suspense comment nodes removedexpect(container.innerHTML).toBe(
'Hello<div>Component</div><div>Component</div><div>Component</div><article>Mismatch</article>',);if (__DEV__) {const secondToLastCall =mockError.mock.calls[mockError.mock.calls.length - 2];
expect(secondToLastCall).toEqual([
'Warning: Expected server HTML to contain a matching <%s> in <%s>.%s','article','section','\n' +
' in article (at **)\n' +
' in Component (at **)\n' +
' in Suspense (at **)\n' +
' in App (at **)',]);
}} finally {console.error = originalConsoleError;
}});it('calls the hydration callbacks after hydration or deletion', async () => {let suspend = false;let resolve;const promise = new Promise(resolvePromise => (resolve = resolvePromise));function Child() {if (suspend) {throw promise;} else {return 'Hello';}}let suspend2 = false;const promise2 = new Promise(() => {});function Child2() {if (suspend2) {throw promise2;} else {return 'World';}}function App({value}) {return (<div><Suspense fallback="Loading...">
<Child /></Suspense><Suspense fallback="Loading...">
<Child2 value={value} /></Suspense></div>);}// First we render the final HTML. With the streaming renderer
// this may have suspense points on the server but here we want// to test the completed HTML. Don't suspend on the server.
suspend = false;suspend2 = false;const finalHTML = ReactDOMServer.renderToString(<App />);
const container = document.createElement('div');
container.innerHTML = finalHTML;
const hydrated = [];
const deleted = [];
// On the client we don't have all data yet but we want to start// hydrating anyway.
suspend = true;suspend2 = true;const root = ReactDOMClient.hydrateRoot(container, <App />, {
onHydrated(node) {hydrated.push(node);
},onDeleted(node) {deleted.push(node);
},onRecoverableError(error) {Scheduler.log(error.message);
},});await waitForAll([]);
expect(hydrated.length).toBe(0);
expect(deleted.length).toBe(0);
await act(async () => {// Resolving the promise should continue hydrationsuspend = false;resolve();await promise;});expect(hydrated.length).toBe(1);
expect(deleted.length).toBe(0);
// Performing an update should force it to delete the boundaryawait act(() => {root.render(<App value={true} />);
});expect(hydrated.length).toBe(1);
expect(deleted.length).toBe(1);
});it('hydrates an empty suspense boundary', async () => {function App() {return (<div><Suspense fallback="Loading..." />
<div>Sibling</div></div>);}const finalHTML = ReactDOMServer.renderToString(<App />);
const container = document.createElement('div');
container.innerHTML = finalHTML;
ReactDOMClient.hydrateRoot(container, <App />);
await waitForAll([]);
expect(container.innerHTML).toContain('<div>Sibling</div>');
});it('recovers with client render when server rendered additional nodes at suspense root', async () => {function CheckIfHydrating({children}) {// This is a trick to check whether we're hydrating or not, since React// doesn't expose that information currently except// via useSyncExternalStore.
let serverOrClient = '(unknown)';useSyncExternalStore(() => {},() => {serverOrClient = 'Client rendered';return null;},() => {serverOrClient = 'Server rendered';return null;},);Scheduler.log(serverOrClient);
return null;}const ref = React.createRef();
function App({hasB}) {return (<div><Suspense fallback="Loading...">
<span ref={ref}>A</span>{hasB ? <span>B</span> : null}
<CheckIfHydrating /></Suspense><div>Sibling</div></div>);}const finalHTML = ReactDOMServer.renderToString(<App hasB={true} />);
assertLog(['Server rendered']);
const container = document.createElement('div');
container.innerHTML = finalHTML;
const span = container.getElementsByTagName('span')[0];
expect(container.innerHTML).toContain('<span>A</span>');
expect(container.innerHTML).toContain('<span>B</span>');
expect(ref.current).toBe(null);
await expect(async () => {await act(() => {ReactDOMClient.hydrateRoot(container, <App hasB={false} />, {
onRecoverableError(error) {Scheduler.log(error.message);
},});});}).toErrorDev('Did not expect server HTML to contain a <span> in <div>');
expect(container.innerHTML).toContain('<span>A</span>');
expect(container.innerHTML).not.toContain('<span>B</span>');
assertLog([
'Server rendered','Client rendered','There was an error while hydrating this Suspense boundary. ' +'Switched to client rendering.',]);
expect(ref.current).not.toBe(span);
});it('recovers with client render when server rendered additional nodes at suspense root after unsuspending', async () => {// We can't use the toErrorDev helper here because this is async.
const originalConsoleError = console.error;
const mockError = jest.fn();
console.error = (...args) => {
mockError(...args.map(normalizeCodeLocInfo));
};const ref = React.createRef();
let shouldSuspend = false;let resolve;const promise = new Promise(res => {resolve = () => {shouldSuspend = false;res();};});function Suspender() {if (shouldSuspend) {throw promise;}return <></>;}function App({hasB}) {return (<div><Suspense fallback="Loading...">
<Suspender /><span ref={ref}>A</span>{hasB ? <span>B</span> : null}
</Suspense><div>Sibling</div></div>);}try {const finalHTML = ReactDOMServer.renderToString(<App hasB={true} />);
const container = document.createElement('div');
container.innerHTML = finalHTML;
const span = container.getElementsByTagName('span')[0];
expect(container.innerHTML).toContain('<span>A</span>');
expect(container.innerHTML).toContain('<span>B</span>');
expect(ref.current).toBe(null);
shouldSuspend = true;await act(() => {ReactDOMClient.hydrateRoot(container, <App hasB={false} />);
});await act(() => {resolve();});expect(container.innerHTML).toContain('<span>A</span>');
expect(container.innerHTML).not.toContain('<span>B</span>');
expect(ref.current).not.toBe(span);
if (__DEV__) {expect(mockError).toHaveBeenCalledWith(
'Warning: Did not expect server HTML to contain a <%s> in <%s>.%s',
'span','div','\n' +
' in Suspense (at **)\n' +
' in div (at **)\n' +
' in App (at **)',
);}} finally {console.error = originalConsoleError;
}});it('recovers with client render when server rendered additional nodes deep inside suspense root', async () => {const ref = React.createRef();
function App({hasB}) {return (<div><Suspense fallback="Loading...">
<div><span ref={ref}>A</span>{hasB ? <span>B</span> : null}
</div></Suspense><div>Sibling</div></div>);}const finalHTML = ReactDOMServer.renderToString(<App hasB={true} />);
const container = document.createElement('div');
container.innerHTML = finalHTML;
const span = container.getElementsByTagName('span')[0];
expect(container.innerHTML).toContain('<span>A</span>');
expect(container.innerHTML).toContain('<span>B</span>');
expect(ref.current).toBe(null);
await expect(async () => {await act(() => {ReactDOMClient.hydrateRoot(container, <App hasB={false} />, {
onRecoverableError(error) {Scheduler.log(error.message);
},});});}).toErrorDev('Did not expect server HTML to contain a <span> in <div>');
assertLog([
'Hydration failed because the initial UI does not match what was rendered on the server.','There was an error while hydrating this Suspense boundary. Switched to client rendering.',]);
expect(container.innerHTML).toContain('<span>A</span>');
expect(container.innerHTML).not.toContain('<span>B</span>');
expect(ref.current).not.toBe(span);
});it('calls the onDeleted hydration callback if the parent gets deleted', async () => {let suspend = false;const promise = new Promise(() => {});function Child() {if (suspend) {throw promise;} else {return 'Hello';}}function App({deleted}) {if (deleted) {return null;}return (<div><Suspense fallback="Loading...">
<Child /></Suspense></div>);}suspend = false;const finalHTML = ReactDOMServer.renderToString(<App />);
const container = document.createElement('div');
container.innerHTML = finalHTML;
const deleted = [];
// On the client we don't have all data yet but we want to start// hydrating anyway.
suspend = true;const root = await act(() => {return ReactDOMClient.hydrateRoot(container, <App />, {
onDeleted(node) {deleted.push(node);
},});});expect(deleted.length).toBe(0);
await act(() => {root.render(<App deleted={true} />);
});// The callback should have been invoked.
expect(deleted.length).toBe(1);
});it('warns and replaces the boundary content in legacy mode', async () => {let suspend = false;let resolve;const promise = new Promise(resolvePromise => (resolve = resolvePromise));const ref = React.createRef();
function Child() {if (suspend) {throw promise;} else {return 'Hello';}}function App() {return (<div><Suspense fallback="Loading...">
<span ref={ref}><Child /></span></Suspense></div>);}// Don't suspend on the server.
suspend = false;const finalHTML = ReactDOMServer.renderToString(<App />);
const container = document.createElement('div');
container.innerHTML = finalHTML;
const span = container.getElementsByTagName('span')[0];
// On the client we try to hydrate.
suspend = true;await expect(async () => {await act(() => {ReactDOM.hydrate(<App />, container);
});}).toErrorDev(
'Warning: Cannot hydrate Suspense in legacy mode. Switch from ' +
'ReactDOM.hydrate(element, container) to ' +
'ReactDOMClient.hydrateRoot(container, <App />)' +
'.render(element) or remove the Suspense components from the server ' +
'rendered components.' +
'\n in Suspense (at **)' +
'\n in div (at **)' +
'\n in App (at **)',
);// We're now in loading state.
expect(container.textContent).toBe('Loading...');
const span2 = container.getElementsByTagName('span')[0];
// This is a new node.
expect(span).not.toBe(span2);
if (gate(flags => flags.dfsEffectsRefactor)) {
// The effects list refactor causes this to be null because the Suspense Activity's child// is null. However, since we can't hydrate Suspense in legacy this change in behavior is ok
expect(ref.current).toBe(null);
} else {expect(ref.current).toBe(span2);
}// Resolving the promise should render the final content.
suspend = false;await act(() => resolve());// We should now have hydrated with a ref on the existing span.
expect(container.textContent).toBe('Hello');
});it('can insert siblings before the dehydrated boundary', async () => {let suspend = false;const promise = new Promise(() => {});let showSibling;function Child() {if (suspend) {throw promise;} else {return 'Second';}}function Sibling() {const [visible, setVisibilty] = React.useState(false);
showSibling = () => setVisibilty(true);if (visible) {return <div>First</div>;}return null;}function App() {return (<div><Sibling /><Suspense fallback="Loading...">
<span><Child /></span></Suspense></div>);}suspend = false;const finalHTML = ReactDOMServer.renderToString(<App />);
const container = document.createElement('div');
container.innerHTML = finalHTML;
// On the client we don't have all data yet but we want to start// hydrating anyway.
suspend = true;await act(() => {ReactDOMClient.hydrateRoot(container, <App />);
});expect(container.firstChild.firstChild.tagName).not.toBe('DIV');
// In this state, we can still update the siblings.
await act(() => showSibling());expect(container.firstChild.firstChild.tagName).toBe('DIV');
expect(container.firstChild.firstChild.textContent).toBe('First');
});it('can delete the dehydrated boundary before it is hydrated', async () => {let suspend = false;const promise = new Promise(() => {});let hideMiddle;function Child() {if (suspend) {throw promise;} else {return (<><div>Middle</div>Some text</>);}}function App() {const [visible, setVisibilty] = React.useState(true);
hideMiddle = () => setVisibilty(false);return (<div><div>Before</div>{visible ? (
<Suspense fallback="Loading...">
<Child /></Suspense>) : null}<div>After</div></div>);}suspend = false;const finalHTML = ReactDOMServer.renderToString(<App />);
const container = document.createElement('div');
container.innerHTML = finalHTML;
// On the client we don't have all data yet but we want to start// hydrating anyway.
suspend = true;await act(() => {ReactDOMClient.hydrateRoot(container, <App />);
});expect(container.firstChild.children[1].textContent).toBe('Middle');
// In this state, we can still delete the boundary.
await act(() => hideMiddle());expect(container.firstChild.children[1].textContent).toBe('After');
});it('blocks updates to hydrate the content first if props have changed', async () => {let suspend = false;let resolve;const promise = new Promise(resolvePromise => (resolve = resolvePromise));const ref = React.createRef();
function Child({text}) {if (suspend) {throw promise;} else {return text;}}function App({text, className}) {return (<div><Suspense fallback="Loading...">
<span ref={ref} className={className}><Child text={text} /></span></Suspense></div>);}suspend = false;const finalHTML = ReactDOMServer.renderToString(
<App text="Hello" className="hello" />,);const container = document.createElement('div');
container.innerHTML = finalHTML;
const span = container.getElementsByTagName('span')[0];
// On the client we don't have all data yet but we want to start// hydrating anyway.
suspend = true;const root = ReactDOMClient.hydrateRoot(
container,<App text="Hello" className="hello" />,);await waitForAll([]);
expect(ref.current).toBe(null);
expect(span.textContent).toBe('Hello');
// Render an update, which will be higher or the same priority as pinging the hydration.
root.render(<App text="Hi" className="hi" />);
// At the same time, resolving the promise so that rendering can complete.
// This should first complete the hydration and then flush the update onto the hydrated state.
await act(async () => {suspend = false;resolve();await promise;});// The new span should be the same since we should have successfully hydrated// before changing it.
const newSpan = container.getElementsByTagName('span')[0];
expect(span).toBe(newSpan);
// We should now have fully rendered with a ref on the new span.
expect(ref.current).toBe(span);
expect(span.textContent).toBe('Hi');
// If we ended up hydrating the existing content, we won't have properly// patched up the tree, which might mean we haven't patched the className.
expect(span.className).toBe('hi');
});// @gate experimental || www
it('blocks updates to hydrate the content first if props changed at idle priority', async () => {let suspend = false;let resolve;const promise = new Promise(resolvePromise => (resolve = resolvePromise));const ref = React.createRef();
function Child({text}) {if (suspend) {throw promise;} else {return text;}}function App({text, className}) {return (<div><Suspense fallback="Loading...">
<span ref={ref} className={className}><Child text={text} /></span></Suspense></div>);}suspend = false;const finalHTML = ReactDOMServer.renderToString(
<App text="Hello" className="hello" />,);const container = document.createElement('div');
container.innerHTML = finalHTML;
const span = container.getElementsByTagName('span')[0];
// On the client we don't have all data yet but we want to start// hydrating anyway.
suspend = true;const root = ReactDOMClient.hydrateRoot(
container,<App text="Hello" className="hello" />,);await waitForAll([]);
expect(ref.current).toBe(null);
expect(span.textContent).toBe('Hello');
// Schedule an update at idle priorityReactDOM.unstable_runWithPriority(IdleEventPriority, () => {
root.render(<App text="Hi" className="hi" />);
});// At the same time, resolving the promise so that rendering can complete.
suspend = false;resolve();await promise;// This should first complete the hydration and then flush the update onto the hydrated state.
await waitForAll([]);
// The new span should be the same since we should have successfully hydrated// before changing it.
const newSpan = container.getElementsByTagName('span')[0];
expect(span).toBe(newSpan);
// We should now have fully rendered with a ref on the new span.
expect(ref.current).toBe(span);
expect(span.textContent).toBe('Hi');
// If we ended up hydrating the existing content, we won't have properly// patched up the tree, which might mean we haven't patched the className.
expect(span.className).toBe('hi');
});it('shows the fallback if props have changed before hydration completes and is still suspended', async () => {let suspend = false;let resolve;const promise = new Promise(resolvePromise => (resolve = resolvePromise));const ref = React.createRef();
function Child({text}) {if (suspend) {throw promise;} else {return text;}}function App({text, className}) {return (<div><Suspense fallback="Loading...">
<span ref={ref} className={className}><Child text={text} /></span></Suspense></div>);}suspend = false;const finalHTML = ReactDOMServer.renderToString(
<App text="Hello" className="hello" />,);const container = document.createElement('div');
container.innerHTML = finalHTML;
// On the client we don't have all data yet but we want to start// hydrating anyway.
suspend = true;const root = ReactDOMClient.hydrateRoot(
container,<App text="Hello" className="hello" />,{onRecoverableError(error) {Scheduler.log(error.message);
},},);await waitForAll([]);
expect(ref.current).toBe(null);
// Render an update, but leave it still suspended.
await act(() => {root.render(<App text="Hi" className="hi" />);
});// Flushing now should delete the existing content and show the fallback.
expect(container.getElementsByTagName('span').length).toBe(0);
expect(ref.current).toBe(null);
expect(container.textContent).toBe('Loading...');
// Unsuspending shows the content.
await act(async () => {suspend = false;resolve();await promise;});const span = container.getElementsByTagName('span')[0];
expect(span.textContent).toBe('Hi');
expect(span.className).toBe('hi');
expect(ref.current).toBe(span);
expect(container.textContent).toBe('Hi');
});it('treats missing fallback the same as if it was defined', async () => {// This is the same exact test as above but with a nested Suspense without a fallback.
// This should be a noop.
let suspend = false;let resolve;const promise = new Promise(resolvePromise => (resolve = resolvePromise));const ref = React.createRef();
function Child({text}) {if (suspend) {throw promise;} else {return text;}}function App({text, className}) {return (<div><Suspense fallback="Loading...">
<span ref={ref} className={className}><Suspense><Child text={text} /></Suspense></span></Suspense></div>);}suspend = false;const finalHTML = ReactDOMServer.renderToString(
<App text="Hello" className="hello" />,);const container = document.createElement('div');
container.innerHTML = finalHTML;
// On the client we don't have all data yet but we want to start// hydrating anyway.
suspend = true;const root = ReactDOMClient.hydrateRoot(
container,<App text="Hello" className="hello" />,{onRecoverableError(error) {Scheduler.log(error.message);
},},);await waitForAll([]);
const span = container.getElementsByTagName('span')[0];
expect(ref.current).toBe(span);
// Render an update, but leave it still suspended.
// Flushing now should delete the existing content and show the fallback.
await act(() => {root.render(<App text="Hi" className="hi" />);
});expect(container.getElementsByTagName('span').length).toBe(1);
expect(ref.current).toBe(span);
expect(container.textContent).toBe('');
// Unsuspending shows the content.
await act(async () => {suspend = false;resolve();await promise;});expect(span.textContent).toBe('Hi');
expect(span.className).toBe('hi');
expect(ref.current).toBe(span);
expect(container.textContent).toBe('Hi');
});it('clears nested suspense boundaries if they did not hydrate yet', async () => {let suspend = false;let resolve;const promise = new Promise(resolvePromise => (resolve = resolvePromise));const ref = React.createRef();
function Child({text}) {if (suspend) {throw promise;} else {return text;}}function App({text, className}) {return (<div><Suspense fallback="Loading...">
<Suspense fallback="Never happens"><Child text={text} /></Suspense>{' '}<span ref={ref} className={className}><Child text={text} /></span></Suspense></div>);}suspend = false;const finalHTML = ReactDOMServer.renderToString(
<App text="Hello" className="hello" />,);const container = document.createElement('div');
container.innerHTML = finalHTML;
// On the client we don't have all data yet but we want to start// hydrating anyway.
suspend = true;const root = ReactDOMClient.hydrateRoot(
container,<App text="Hello" className="hello" />,{onRecoverableError(error) {Scheduler.log(error.message);
},},);await waitForAll([]);
expect(ref.current).toBe(null);
// Render an update, but leave it still suspended.
// Flushing now should delete the existing content and show the fallback.
await act(() => {root.render(<App text="Hi" className="hi" />);
});expect(container.getElementsByTagName('span').length).toBe(0);
expect(ref.current).toBe(null);
expect(container.textContent).toBe('Loading...');
// Unsuspending shows the content.
await act(async () => {suspend = false;resolve();await promise;});await waitForAll([]);
const span = container.getElementsByTagName('span')[0];
expect(span.textContent).toBe('Hi');
expect(span.className).toBe('hi');
expect(ref.current).toBe(span);
expect(container.textContent).toBe('Hi Hi');
});it('hydrates first if props changed but we are able to resolve within a timeout', async () => {let suspend = false;let resolve;const promise = new Promise(resolvePromise => (resolve = resolvePromise));const ref = React.createRef();
function Child({text}) {if (suspend) {throw promise;} else {return text;}}function App({text, className}) {return (<div><Suspense fallback="Loading...">
<span ref={ref} className={className}><Child text={text} /></span></Suspense></div>);}suspend = false;const finalHTML = ReactDOMServer.renderToString(
<App text="Hello" className="hello" />,);const container = document.createElement('div');
container.innerHTML = finalHTML;
const span = container.getElementsByTagName('span')[0];
// On the client we don't have all data yet but we want to start// hydrating anyway.
suspend = true;const root = ReactDOMClient.hydrateRoot(
container,<App text="Hello" className="hello" />,);await waitForAll([]);
expect(ref.current).toBe(null);
expect(container.textContent).toBe('Hello');
// Render an update with a long timeout.
React.startTransition(() => root.render(<App text="Hi" className="hi" />));
// This shouldn't force the fallback yet.
await waitForAll([]);
expect(ref.current).toBe(null);
expect(container.textContent).toBe('Hello');
// Resolving the promise so that rendering can complete.
// This should first complete the hydration and then flush the update onto the hydrated state.
suspend = false;await act(() => resolve());// The new span should be the same since we should have successfully hydrated// before changing it.
const newSpan = container.getElementsByTagName('span')[0];
expect(span).toBe(newSpan);
// We should now have fully rendered with a ref on the new span.
expect(ref.current).toBe(span);
expect(container.textContent).toBe('Hi');
// If we ended up hydrating the existing content, we won't have properly// patched up the tree, which might mean we haven't patched the className.
expect(span.className).toBe('hi');
});it('warns but works if setState is called before commit in a dehydrated component', async () => {let suspend = false;let resolve;const promise = new Promise(resolvePromise => (resolve = resolvePromise));let updateText;function Child() {const [state, setState] = React.useState('Hello');
updateText = setState;Scheduler.log('Child');
if (suspend) {throw promise;} else {return state;}}function Sibling() {Scheduler.log('Sibling');
return null;}function App() {return (<div><Suspense fallback="Loading...">
<Child /><Sibling /></Suspense></div>);}suspend = false;const finalHTML = ReactDOMServer.renderToString(<App />);
assertLog(['Child', 'Sibling']);
const container = document.createElement('div');
container.innerHTML = finalHTML;
ReactDOMClient.hydrateRoot(
container,<App text="Hello" className="hello" />,);await act(async () => {suspend = true;await waitFor(['Child']);
// While we're part way through the hydration, we update the state.
// This will schedule an update on the children of the suspense boundary.
expect(() => updateText('Hi')).toErrorDev(
"Can't perform a React state update on a component that hasn't mounted yet.",
);// This will throw it away and rerender.
await waitForAll(['Child']);
expect(container.textContent).toBe('Hello');
suspend = false;resolve();await promise;});assertLog(['Child', 'Sibling']);
expect(container.textContent).toBe('Hello');
});it('blocks the update to hydrate first if context has changed', async () => {let suspend = false;let resolve;const promise = new Promise(resolvePromise => (resolve = resolvePromise));const ref = React.createRef();
const Context = React.createContext(null);
function Child() {const {text, className} = React.useContext(Context);
if (suspend) {throw promise;} else {return (<span ref={ref} className={className}>{text}</span>);}}const App = React.memo(function App() {
return (<div><Suspense fallback="Loading...">
<Child /></Suspense></div>);});suspend = false;const finalHTML = ReactDOMServer.renderToString(
<Context.Provider value={{text: 'Hello', className: 'hello'}}>
<App /></Context.Provider>,
);const container = document.createElement('div');
container.innerHTML = finalHTML;
const span = container.getElementsByTagName('span')[0];
// On the client we don't have all data yet but we want to start// hydrating anyway.
suspend = true;const root = ReactDOMClient.hydrateRoot(
container,<Context.Provider value={{text: 'Hello', className: 'hello'}}>
<App /></Context.Provider>,
);await waitForAll([]);
expect(ref.current).toBe(null);
expect(span.textContent).toBe('Hello');
// Render an update, which will be higher or the same priority as pinging the hydration.
root.render(
<Context.Provider value={{text: 'Hi', className: 'hi'}}>
<App /></Context.Provider>,
);// At the same time, resolving the promise so that rendering can complete.
// This should first complete the hydration and then flush the update onto the hydrated state.
await act(async () => {suspend = false;resolve();await promise;});// Since this should have been hydrated, this should still be the same span.
const newSpan = container.getElementsByTagName('span')[0];
expect(newSpan).toBe(span);
// We should now have fully rendered with a ref on the new span.
expect(ref.current).toBe(span);
expect(span.textContent).toBe('Hi');
// If we ended up hydrating the existing content, we won't have properly// patched up the tree, which might mean we haven't patched the className.
expect(span.className).toBe('hi');
});it('shows the fallback if context has changed before hydration completes and is still suspended', async () => {let suspend = false;let resolve;const promise = new Promise(resolvePromise => (resolve = resolvePromise));const ref = React.createRef();
const Context = React.createContext(null);
function Child() {const {text, className} = React.useContext(Context);
if (suspend) {throw promise;} else {return (<span ref={ref} className={className}>{text}</span>);}}const App = React.memo(function App() {
return (<div><Suspense fallback="Loading...">
<Child /></Suspense></div>);});suspend = false;const finalHTML = ReactDOMServer.renderToString(
<Context.Provider value={{text: 'Hello', className: 'hello'}}>
<App /></Context.Provider>,
);const container = document.createElement('div');
container.innerHTML = finalHTML;
// On the client we don't have all data yet but we want to start// hydrating anyway.
suspend = true;const root = ReactDOMClient.hydrateRoot(
container,<Context.Provider value={{text: 'Hello', className: 'hello'}}>
<App /></Context.Provider>,
{onRecoverableError(error) {Scheduler.log(error.message);
},},);await waitForAll([]);
expect(ref.current).toBe(null);
// Render an update, but leave it still suspended.
// Flushing now should delete the existing content and show the fallback.
await act(() => {root.render(
<Context.Provider value={{text: 'Hi', className: 'hi'}}>
<App /></Context.Provider>,
);});expect(container.getElementsByTagName('span').length).toBe(0);
expect(ref.current).toBe(null);
expect(container.textContent).toBe('Loading...');
// Unsuspending shows the content.
await act(async () => {suspend = false;resolve();await promise;});const span = container.getElementsByTagName('span')[0];
expect(span.textContent).toBe('Hi');
expect(span.className).toBe('hi');
expect(ref.current).toBe(span);
expect(container.textContent).toBe('Hi');
});it('replaces the fallback with client content if it is not rendered by the server', async () => {let suspend = false;const promise = new Promise(resolvePromise => {});const ref = React.createRef();
function Child() {if (suspend) {throw promise;} else {return 'Hello';}}function App() {return (<div><Suspense fallback="Loading...">
<span ref={ref}><Child /></span></Suspense></div>);}// First we render the final HTML. With the streaming renderer
// this may have suspense points on the server but here we want// to test the completed HTML. Don't suspend on the server.
suspend = true;const finalHTML = ReactDOMServer.renderToString(<App />);
const container = document.createElement('div');
container.innerHTML = finalHTML;
expect(container.getElementsByTagName('span').length).toBe(0);
// On the client we have the data available quickly for some reason.
suspend = false;ReactDOMClient.hydrateRoot(container, <App />, {
onRecoverableError(error) {Scheduler.log(error.message);
},});if (__DEV__) {await waitForAll([
'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',]);
} else {await waitForAll([
'The server could not finish this Suspense boundary, likely due to ' +'an error during server rendering. Switched to client rendering.',]);
}jest.runAllTimers();
expect(container.textContent).toBe('Hello');
const span = container.getElementsByTagName('span')[0];
expect(ref.current).toBe(span);
});it('replaces the fallback within the suspended time if there is a nested suspense', async () => {let suspend = false;const promise = new Promise(resolvePromise => {});const ref = React.createRef();
function Child() {if (suspend) {throw promise;} else {return 'Hello';}}function InnerChild() {// Always suspends indefinitelythrow promise;}function App() {return (<div><Suspense fallback="Loading...">
<span ref={ref}><Child /></span><Suspense fallback={null}><InnerChild /></Suspense></Suspense></div>);}// First we render the final HTML. With the streaming renderer
// this may have suspense points on the server but here we want// to test the completed HTML. Don't suspend on the server.
suspend = true;const finalHTML = ReactDOMServer.renderToString(<App />);
const container = document.createElement('div');
container.innerHTML = finalHTML;
expect(container.getElementsByTagName('span').length).toBe(0);
// On the client we have the data available quickly for some reason.
suspend = false;ReactDOMClient.hydrateRoot(container, <App />, {
onRecoverableError(error) {Scheduler.log(error.message);
},});if (__DEV__) {await waitForAll([
'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',]);
} else {await waitForAll([
'The server could not finish this Suspense boundary, likely due to ' +'an error during server rendering. Switched to client rendering.',]);
}// This will have exceeded the suspended time so we should timeout.
jest.advanceTimersByTime(500);
// The boundary should longer be suspended for the middle content// even though the inner boundary is still suspended.
expect(container.textContent).toBe('Hello');
const span = container.getElementsByTagName('span')[0];
expect(ref.current).toBe(span);
});it('replaces the fallback within the suspended time if there is a nested suspense in a nested suspense', async () => {let suspend = false;const promise = new Promise(resolvePromise => {});const ref = React.createRef();
function Child() {if (suspend) {throw promise;} else {return 'Hello';}}function InnerChild() {// Always suspends indefinitelythrow promise;}function App() {return (<div><Suspense fallback="Another layer"><Suspense fallback="Loading...">
<span ref={ref}><Child /></span><Suspense fallback={null}><InnerChild /></Suspense></Suspense></Suspense></div>);}// First we render the final HTML. With the streaming renderer
// this may have suspense points on the server but here we want// to test the completed HTML. Don't suspend on the server.
suspend = true;const finalHTML = ReactDOMServer.renderToString(<App />);
const container = document.createElement('div');
container.innerHTML = finalHTML;
expect(container.getElementsByTagName('span').length).toBe(0);
// On the client we have the data available quickly for some reason.
suspend = false;ReactDOMClient.hydrateRoot(container, <App />, {
onRecoverableError(error) {Scheduler.log(error.message);
},});if (__DEV__) {await waitForAll([
'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',]);
} else {await waitForAll([
'The server could not finish this Suspense boundary, likely due to ' +'an error during server rendering. Switched to client rendering.',]);
}// This will have exceeded the suspended time so we should timeout.
jest.advanceTimersByTime(500);
// The boundary should longer be suspended for the middle content// even though the inner boundary is still suspended.
expect(container.textContent).toBe('Hello');
const span = container.getElementsByTagName('span')[0];
expect(ref.current).toBe(span);
});// @gate enableSuspenseListit('shows inserted items in a SuspenseList before content is hydrated', async () => {let suspend = false;let resolve;const promise = new Promise(resolvePromise => (resolve = resolvePromise));const ref = React.createRef();
function Child({children}) {if (suspend) {throw promise;} else {return children;}}// These are hoisted to avoid them from rerendering.
const a = (<Suspense fallback="Loading A"><Child><span>A</span></Child></Suspense>);const b = (<Suspense fallback="Loading B"><Child><span ref={ref}>B</span></Child></Suspense>);function App({showMore}) {return (<SuspenseList revealOrder="forwards">{a}{b}{showMore ? (
<Suspense fallback="Loading C"><span>C</span></Suspense>) : null}</SuspenseList>);}suspend = false;const html = ReactDOMServer.renderToString(<App showMore={false} />);
const container = document.createElement('div');
container.innerHTML = html;
const spanB = container.getElementsByTagName('span')[1];
suspend = true;const root = await act(() =>ReactDOMClient.hydrateRoot(container, <App showMore={false} />),
);// We're not hydrated yet.
expect(ref.current).toBe(null);
expect(container.textContent).toBe('AB');
// Add more rows before we've hydrated the first two.
await act(() => {root.render(<App showMore={true} />);
});// We're not hydrated yet.
expect(ref.current).toBe(null);
// Since the first two are already showing their final content// we should be able to show the real content.
expect(container.textContent).toBe('ABC');
suspend = false;await act(async () => {await resolve();});expect(container.textContent).toBe('ABC');
// We've hydrated the same span.
expect(ref.current).toBe(spanB);
});// @gate enableSuspenseListit('shows is able to hydrate boundaries even if others in a list are pending', async () => {let suspend = false;let resolve;const promise = new Promise(resolvePromise => (resolve = resolvePromise));const ref = React.createRef();
function Child({children}) {if (suspend) {throw promise;} else {return children;}}const promise2 = new Promise(() => {});function AlwaysSuspend() {throw promise2;}// This is hoisted to avoid them from rerendering.
const a = (<Suspense fallback="Loading A"><Child><span ref={ref}>A</span></Child></Suspense>);function App({showMore}) {return (<SuspenseList revealOrder="together">{a}{showMore ? (
<Suspense fallback="Loading B"><AlwaysSuspend /></Suspense>) : null}</SuspenseList>);}suspend = false;const html = ReactDOMServer.renderToString(<App showMore={false} />);
const container = document.createElement('div');
container.innerHTML = html;
const spanA = container.getElementsByTagName('span')[0];
suspend = true;const root = await act(() =>ReactDOMClient.hydrateRoot(container, <App showMore={false} />),
);// We're not hydrated yet.
expect(ref.current).toBe(null);
expect(container.textContent).toBe('A');
await act(async () => {// Add another row before we've hydrated the first one.
root.render(<App showMore={true} />);
// At the same time, we resolve the blocking promise.
suspend = false;await resolve();});// We should have been able to hydrate the first row.
expect(ref.current).toBe(spanA);
// Even though we're still slowing B.
expect(container.textContent).toBe('ALoading B');
});// @gate enableSuspenseListit('clears server boundaries when SuspenseList runs out of time hydrating', async () => {let suspend = false;let resolve;const promise = new Promise(resolvePromise => (resolve = resolvePromise));const ref = React.createRef();
function Child({children}) {if (suspend) {throw promise;} else {return children;}}function Before() {Scheduler.log('Before');
return null;}function After() {Scheduler.log('After');
return null;}function FirstRow() {return (<><Before /><Suspense fallback="Loading A"><span>A</span></Suspense><After /></>);}function App() {return (<Suspense fallback={null}><SuspenseList revealOrder="forwards" tail="hidden"><FirstRow /><Suspense fallback="Loading B"><Child><span ref={ref}>B</span></Child></Suspense></SuspenseList></Suspense>);}suspend = false;const html = ReactDOMServer.renderToString(<App />);
assertLog(['Before', 'After']);
const container = document.createElement('div');
container.innerHTML = html;
const b = container.getElementsByTagName('span')[1];
expect(b.textContent).toBe('B');
const root = ReactDOMClient.hydrateRoot(container, <App />);
// Increase hydration priority to higher than "offscreen".
root.unstable_scheduleHydration(b);
suspend = true;await act(async () => {if (gate(flags => flags.forceConcurrentByDefaultForTesting)) {
await waitFor(['Before']);
// This took a long time to render.
Scheduler.unstable_advanceTime(1000);
await waitFor(['After']);
} else {await waitFor(['Before', 'After']);
}// This will cause us to skip the second row completely.
});// We haven't hydrated the second child but the placeholder is still in the list.
expect(ref.current).toBe(null);
expect(container.textContent).toBe('AB');
suspend = false;await act(async () => {// Resolve the boundary to be in its resolved final state.
await resolve();});expect(container.textContent).toBe('AB');
expect(ref.current).toBe(b);
});// @gate enableSuspenseListit('clears server boundaries when SuspenseList suspends last row hydrating', async () => {let suspend = false;let resolve;const promise = new Promise(resolvePromise => (resolve = resolvePromise));function Child({children}) {if (suspend) {throw promise;} else {return children;}}function App() {return (<Suspense fallback={null}><SuspenseList revealOrder="forwards" tail="hidden"><Suspense fallback="Loading A"><span>A</span></Suspense><Suspense fallback="Loading B"><Child><span>B</span></Child></Suspense></SuspenseList></Suspense>);}suspend = true;const html = ReactDOMServer.renderToString(<App />);
const container = document.createElement('div');
container.innerHTML = html;
ReactDOMClient.hydrateRoot(container, <App />, {
onRecoverableError(error) {Scheduler.log(error.message);
},});suspend = true;if (__DEV__) {await waitForAll([
'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',]);
} else {await waitForAll([
'The server could not finish this Suspense boundary, likely due to ' +'an error during server rendering. Switched to client rendering.',]);
}// We haven't hydrated the second child but the placeholder is still in the list.
expect(container.textContent).toBe('ALoading B');
suspend = false;await act(async () => {// Resolve the boundary to be in its resolved final state.
await resolve();});expect(container.textContent).toBe('AB');
});it('can client render nested boundaries', async () => {let suspend = false;const promise = new Promise(() => {});const ref = React.createRef();
function Child() {if (suspend) {throw promise;} else {return 'Hello';}}function App() {return (<div><Suspensefallback={<><Suspense fallback="Loading...">
<Child /></Suspense><span>Inner Sibling</span></>}><Child /></Suspense><span ref={ref}>Sibling</span></div>);}suspend = true;const html = ReactDOMServer.renderToString(<App />);
const container = document.createElement('div');
container.innerHTML = html + '<!--unrelated comment-->';
const span = container.getElementsByTagName('span')[1];
suspend = false;ReactDOMClient.hydrateRoot(container, <App />, {
onRecoverableError(error) {Scheduler.log(error.message);
},});if (__DEV__) {await waitForAll([
'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',]);
} else {await waitForAll([
'The server could not finish this Suspense boundary, likely due to ' +'an error during server rendering. Switched to client rendering.',]);
}jest.runAllTimers();
expect(ref.current).toBe(span);
expect(span.parentNode).not.toBe(null);
// It leaves non-React comments alone.
expect(container.lastChild.nodeType).toBe(8);
expect(container.lastChild.data).toBe('unrelated comment');
});it('can hydrate TWO suspense boundaries', async () => {const ref1 = React.createRef();
const ref2 = React.createRef();
function App() {return (<div><Suspense fallback="Loading 1...">
<span ref={ref1}>1</span></Suspense><Suspense fallback="Loading 2...">
<span ref={ref2}>2</span></Suspense></div>);}// First we render the final HTML. With the streaming renderer
// this may have suspense points on the server but here we want// to test the completed HTML. Don't suspend on the server.
const finalHTML = ReactDOMServer.renderToString(<App />);
const container = document.createElement('div');
container.innerHTML = finalHTML;
const span1 = container.getElementsByTagName('span')[0];
const span2 = container.getElementsByTagName('span')[1];
// On the client we don't have all data yet but we want to start// hydrating anyway.
ReactDOMClient.hydrateRoot(container, <App />);
await waitForAll([]);
expect(ref1.current).toBe(span1);
expect(ref2.current).toBe(span2);
});it('regenerates if it cannot hydrate before changes to props/context expire', async () => {let suspend = false;const promise = new Promise(resolvePromise => {});const ref = React.createRef();
const ClassName = React.createContext(null);
function Child({text}) {const className = React.useContext(ClassName);
if (suspend && className !== 'hi' && text !== 'Hi') {// Never suspends on the newer data.
throw promise;} else {return (<span ref={ref} className={className}>{text}</span>);}}function App({text, className}) {return (<div><Suspense fallback="Loading...">
<Child text={text} /></Suspense></div>);}suspend = false;const finalHTML = ReactDOMServer.renderToString(
<ClassName.Provider value={'hello'}>
<App text="Hello" /></ClassName.Provider>,
);const container = document.createElement('div');
container.innerHTML = finalHTML;
const span = container.getElementsByTagName('span')[0];
// On the client we don't have all data yet but we want to start// hydrating anyway.
suspend = true;const root = ReactDOMClient.hydrateRoot(
container,<ClassName.Provider value={'hello'}>
<App text="Hello" /></ClassName.Provider>,
{onRecoverableError(error) {Scheduler.log(error.message);
},},);await waitForAll([]);
expect(ref.current).toBe(null);
expect(span.textContent).toBe('Hello');
// Render an update, which will be higher or the same priority as pinging the hydration.
// The new update doesn't suspend.
// Since we're still suspended on the original data, we can't hydrate.
// This will force all expiration times to flush.
await act(() => {root.render(
<ClassName.Provider value={'hi'}>
<App text="Hi" /></ClassName.Provider>,
);});// This will now be a new span because we weren't able to hydrate beforeconst newSpan = container.getElementsByTagName('span')[0];
expect(newSpan).not.toBe(span);
// We should now have fully rendered with a ref on the new span.
expect(ref.current).toBe(newSpan);
expect(newSpan.textContent).toBe('Hi');
// If we ended up hydrating the existing content, we won't have properly// patched up the tree, which might mean we haven't patched the className.
expect(newSpan.className).toBe('hi');
});it('does not invoke an event on a hydrated node until it commits', async () => {let suspend = false;let resolve;const promise = new Promise(resolvePromise => (resolve = resolvePromise));function Sibling({text}) {if (suspend) {throw promise;} else {return 'Hello';}}let clicks = 0;function Button() {const [clicked, setClicked] = React.useState(false);
if (clicked) {return null;}return (<aonClick={() => {setClicked(true);clicks++;
}}>Click me</a>);}function App() {return (<div><Suspense fallback="Loading...">
<Button /><Sibling /></Suspense></div>);}suspend = false;const finalHTML = ReactDOMServer.renderToString(<App />);
const container = document.createElement('div');
container.innerHTML = finalHTML;
// We need this to be in the document since we'll dispatch events on it.
document.body.appendChild(container);
const a = container.getElementsByTagName('a')[0];
// On the client we don't have all data yet but we want to start// hydrating anyway.
suspend = true;ReactDOMClient.hydrateRoot(container, <App />);
await waitForAll([]);
expect(container.textContent).toBe('Click meHello');
// We're now partially hydrated.
await act(() => {a.click();
});expect(clicks).toBe(0);
// Resolving the promise so that rendering can complete.
await act(async () => {suspend = false;resolve();await promise;});expect(clicks).toBe(0);
expect(container.textContent).toBe('Click meHello');
document.body.removeChild(container);
});// @gate wwwit('does not invoke an event on a hydrated event handle until it commits', async () => {const setClick = ReactDOM.unstable_createEventHandle('click');
let suspend = false;let isServerRendering = true;let resolve;const promise = new Promise(resolvePromise => (resolve = resolvePromise));function Sibling({text}) {if (suspend) {throw promise;} else {return 'Hello';}}const onEvent = jest.fn();
function Button() {const ref = React.useRef(null);
if (!isServerRendering) {React.useLayoutEffect(() => {
return setClick(ref.current, onEvent);
});}return <a ref={ref}>Click me</a>;}function App() {return (<div><Suspense fallback="Loading...">
<Button /><Sibling /></Suspense></div>);}suspend = false;const finalHTML = ReactDOMServer.renderToString(<App />);
const container = document.createElement('div');
container.innerHTML = finalHTML;
// We need this to be in the document since we'll dispatch events on it.
document.body.appendChild(container);
const a = container.getElementsByTagName('a')[0];
// On the client we don't have all data yet but we want to start// hydrating anyway.
suspend = true;isServerRendering = false;ReactDOMClient.hydrateRoot(container, <App />);
// We'll do one click before hydrating.
a.click();
// This should be delayed.
expect(onEvent).toHaveBeenCalledTimes(0);
await waitForAll([]);
// We're now partially hydrated.
await act(() => {a.click();
});// We should not have invoked the event yet because we're not// yet hydrated.
expect(onEvent).toHaveBeenCalledTimes(0);
// Resolving the promise so that rendering can complete.
await act(async () => {suspend = false;resolve();await promise;});expect(onEvent).toHaveBeenCalledTimes(0);
document.body.removeChild(container);
});it('invokes discrete events on nested suspense boundaries in a root (legacy system)', async () => {let suspend = false;let resolve;const promise = new Promise(resolvePromise => (resolve = resolvePromise));let clicks = 0;function Button() {return (<aonClick={() => {clicks++;
}}>Click me</a>);}function Child() {if (suspend) {throw promise;} else {return (<Suspense fallback="Loading...">
<Button /></Suspense>);}}function App() {return (<Suspense fallback="Loading...">
<Child /></Suspense>);}suspend = false;const finalHTML = ReactDOMServer.renderToString(<App />);
const container = document.createElement('div');
container.innerHTML = finalHTML;
// We need this to be in the document since we'll dispatch events on it.
document.body.appendChild(container);
const a = container.getElementsByTagName('a')[0];
// On the client we don't have all data yet but we want to start// hydrating anyway.
suspend = true;ReactDOMClient.hydrateRoot(container, <App />);
// We'll do one click before hydrating.
await act(() => {a.click();
});// This should be delayed.
expect(clicks).toBe(0);
await waitForAll([]);
// We're now partially hydrated.
await act(() => {a.click();
});expect(clicks).toBe(0);
// Resolving the promise so that rendering can complete.
await act(async () => {suspend = false;resolve();await promise;});expect(clicks).toBe(0);
document.body.removeChild(container);
});// @gate wwwit('invokes discrete events on nested suspense boundaries in a root (createEventHandle)', async () => {let suspend = false;let isServerRendering = true;let resolve;const promise = new Promise(resolvePromise => (resolve = resolvePromise));const onEvent = jest.fn();
const setClick = ReactDOM.unstable_createEventHandle('click');
function Button() {const ref = React.useRef(null);
if (!isServerRendering) {React.useLayoutEffect(() => {
return setClick(ref.current, onEvent);
});}return <a ref={ref}>Click me</a>;}function Child() {if (suspend) {throw promise;} else {return (<Suspense fallback="Loading...">
<Button /></Suspense>);}}function App() {return (<Suspense fallback="Loading...">
<Child /></Suspense>);}suspend = false;const finalHTML = ReactDOMServer.renderToString(<App />);
const container = document.createElement('div');
container.innerHTML = finalHTML;
// We need this to be in the document since we'll dispatch events on it.
document.body.appendChild(container);
const a = container.getElementsByTagName('a')[0];
// On the client we don't have all data yet but we want to start// hydrating anyway.
suspend = true;isServerRendering = false;ReactDOMClient.hydrateRoot(container, <App />);
// We'll do one click before hydrating.
a.click();
// This should be delayed.
expect(onEvent).toHaveBeenCalledTimes(0);
await waitForAll([]);
// We're now partially hydrated.
await act(() => {a.click();
});// We should not have invoked the event yet because we're not// yet hydrated.
expect(onEvent).toHaveBeenCalledTimes(0);
// Resolving the promise so that rendering can complete.
await act(async () => {suspend = false;resolve();await promise;});expect(onEvent).toHaveBeenCalledTimes(0);
document.body.removeChild(container);
});it('does not invoke the parent of dehydrated boundary event', async () => {let suspend = false;let resolve;const promise = new Promise(resolvePromise => (resolve = resolvePromise));let clicksOnParent = 0;let clicksOnChild = 0;function Child({text}) {if (suspend) {throw promise;} else {return (<spanonClick={e => {// The stopPropagation is showing an example why invoking// the event on only a parent might not be correct.
e.stopPropagation();
clicksOnChild++;
}}>Hello</span>);}}function App() {return (<div onClick={() => clicksOnParent++}>
<Suspense fallback="Loading...">
<Child /></Suspense></div>);}suspend = false;const finalHTML = ReactDOMServer.renderToString(<App />);
const container = document.createElement('div');
container.innerHTML = finalHTML;
// We need this to be in the document since we'll dispatch events on it.
document.body.appendChild(container);
const span = container.getElementsByTagName('span')[0];
// On the client we don't have all data yet but we want to start// hydrating anyway.
suspend = true;ReactDOMClient.hydrateRoot(container, <App />);
await waitForAll([]);
// We're now partially hydrated.
await act(() => {span.click();
});expect(clicksOnChild).toBe(0);
expect(clicksOnParent).toBe(0);
// Resolving the promise so that rendering can complete.
await act(async () => {suspend = false;resolve();await promise;});expect(clicksOnChild).toBe(0);
expect(clicksOnParent).toBe(0);
document.body.removeChild(container);
});it('does not invoke an event on a parent tree when a subtree is dehydrated', async () => {let suspend = false;let resolve;const promise = new Promise(resolvePromise => (resolve = resolvePromise));let clicks = 0;const childSlotRef = React.createRef();
function Parent() {return <div onClick={() => clicks++} ref={childSlotRef} />;
}function Child({text}) {if (suspend) {throw promise;} else {return <a>Click me</a>;}}function App() {// The root is a Suspense boundary.
return (<Suspense fallback="Loading...">
<Child /></Suspense>);}suspend = false;const finalHTML = ReactDOMServer.renderToString(<App />);
const parentContainer = document.createElement('div');
const childContainer = document.createElement('div');
// We need this to be in the document since we'll dispatch events on it.
document.body.appendChild(parentContainer);
// We're going to use a different root as a parent.
// This lets us detect whether an event goes through React's event system.
const parentRoot = ReactDOMClient.createRoot(parentContainer);
await act(() => parentRoot.render(<Parent />));
childSlotRef.current.appendChild(childContainer);
childContainer.innerHTML = finalHTML;
const a = childContainer.getElementsByTagName('a')[0];
suspend = true;// Hydrate asynchronously.
await act(() => ReactDOMClient.hydrateRoot(childContainer, <App />));
// The Suspense boundary is not yet hydrated.
await act(() => {a.click();
});expect(clicks).toBe(0);
// Resolving the promise so that rendering can complete.
await act(async () => {suspend = false;resolve();await promise;});expect(clicks).toBe(0);
document.body.removeChild(parentContainer);
});it('blocks only on the last continuous event (legacy system)', async () => {let suspend1 = false;let resolve1;const promise1 = new Promise(resolvePromise => (resolve1 = resolvePromise));let suspend2 = false;let resolve2;const promise2 = new Promise(resolvePromise => (resolve2 = resolvePromise));function First({text}) {if (suspend1) {throw promise1;} else {return 'Hello';}}function Second({text}) {if (suspend2) {throw promise2;} else {return 'World';}}const ops = [];
function App() {return (<div><Suspense fallback="Loading First...">
<spanonMouseEnter={() => ops.push('Mouse Enter First')}
onMouseLeave={() => ops.push('Mouse Leave First')}
/>{/* We suspend after to test what happens when we eager
attach the listener. */}
<First /></Suspense><Suspense fallback="Loading Second...">
<spanonMouseEnter={() => ops.push('Mouse Enter Second')}
onMouseLeave={() => ops.push('Mouse Leave Second')}>
<Second /></span></Suspense></div>);}const finalHTML = ReactDOMServer.renderToString(<App />);
const container = document.createElement('div');
container.innerHTML = finalHTML;
// We need this to be in the document since we'll dispatch events on it.
document.body.appendChild(container);
const appDiv = container.getElementsByTagName('div')[0];
const firstSpan = appDiv.getElementsByTagName('span')[0];
const secondSpan = appDiv.getElementsByTagName('span')[1];
expect(firstSpan.textContent).toBe('');
expect(secondSpan.textContent).toBe('World');
// On the client we don't have all data yet but we want to start// hydrating anyway.
suspend1 = true;suspend2 = true;ReactDOMClient.hydrateRoot(container, <App />);
await waitForAll([]);
dispatchMouseEvent(appDiv, null);dispatchMouseEvent(firstSpan, appDiv);dispatchMouseEvent(secondSpan, firstSpan);// Neither target is yet hydrated.
expect(ops).toEqual([]);
// Resolving the second promise so that rendering can complete.
suspend2 = false;resolve2();await promise2;await waitForAll([]);
// We've unblocked the current hover target so we should be// able to replay it now.
expect(ops).toEqual(['Mouse Enter Second']);
// Resolving the first promise has no effect now.
suspend1 = false;resolve1();await promise1;await waitForAll([]);
expect(ops).toEqual(['Mouse Enter Second']);
document.body.removeChild(container);
});it('finishes normal pri work before continuing to hydrate a retry', async () => {let suspend = false;let resolve;const promise = new Promise(resolvePromise => (resolve = resolvePromise));const ref = React.createRef();
function Child() {if (suspend) {throw promise;} else {Scheduler.log('Child');
return 'Hello';}}function Sibling() {Scheduler.log('Sibling');
React.useLayoutEffect(() => {
Scheduler.log('Commit Sibling');
});return 'World';}// Avoid rerendering the tree by hoisting it.
const tree = (<Suspense fallback="Loading...">
<span ref={ref}><Child /></span></Suspense>);function App({showSibling}) {return (<div>{tree}{showSibling ? <Sibling /> : null}
</div>);}suspend = false;const finalHTML = ReactDOMServer.renderToString(<App />);
assertLog(['Child']);
const container = document.createElement('div');
container.innerHTML = finalHTML;
suspend = true;const root = ReactDOMClient.hydrateRoot(
container,<App showSibling={false} />,);await waitForAll([]);
expect(ref.current).toBe(null);
expect(container.textContent).toBe('Hello');
// Resolving the promise should continue hydrationsuspend = false;resolve();await promise;Scheduler.unstable_advanceTime(100);
// Before we have a chance to flush it, we'll also render an update.
root.render(<App showSibling={true} />);
// When we flush we expect the Normal pri render to take priority// over hydration.
await waitFor(['Sibling', 'Commit Sibling']);
// We shouldn't have hydrated the child yet.
expect(ref.current).toBe(null);
// But we did have a chance to update the content.
expect(container.textContent).toBe('HelloWorld');
await waitForAll(['Child']);
// Now we're hydrated.
expect(ref.current).not.toBe(null);
});it('regression test: does not overfire non-bubbling browser events', async () => {let suspend = false;let resolve;const promise = new Promise(resolvePromise => (resolve = resolvePromise));function Sibling({text}) {if (suspend) {throw promise;} else {return 'Hello';}}let submits = 0;function Form() {const [submitted, setSubmitted] = React.useState(false);
if (submitted) {return null;}return (<formonSubmit={() => {setSubmitted(true);submits++;
}}>Click me</form>);}function App() {return (<div><Suspense fallback="Loading...">
<Form /><Sibling /></Suspense></div>);}suspend = false;const finalHTML = ReactDOMServer.renderToString(<App />);
const container = document.createElement('div');
container.innerHTML = finalHTML;
// We need this to be in the document since we'll dispatch events on it.
document.body.appendChild(container);
const form = container.getElementsByTagName('form')[0];
// On the client we don't have all data yet but we want to start// hydrating anyway.
suspend = true;ReactDOMClient.hydrateRoot(container, <App />);
await waitForAll([]);
expect(container.textContent).toBe('Click meHello');
// We're now partially hydrated.
await act(() => {form.dispatchEvent(
new window.Event('submit', {
bubbles: true,}),);});expect(submits).toBe(0);
// Resolving the promise so that rendering can complete.
await act(async () => {suspend = false;resolve();await promise;});// discrete event not replayedexpect(submits).toBe(0);
expect(container.textContent).toBe('Click meHello');
document.body.removeChild(container);
});// This test fails, in both forks. Without a boundary, the deferred tree won't
// re-enter hydration mode. It doesn't come up in practice because there's
// always a parent Suspense boundary. But it's still a bug. Leaving for a
// follow up.
//// @gate FIXMEit('hydrates a hidden subtree outside of a Suspense boundary', async () => {const ref = React.createRef();
function App() {return (<LegacyHiddenDiv mode="hidden"><span ref={ref}>Hidden child</span></LegacyHiddenDiv>);}const finalHTML = ReactDOMServer.renderToString(<App />);
const container = document.createElement('div');
container.innerHTML = finalHTML;
const span = container.getElementsByTagName('span')[0];
expect(span.innerHTML).toBe('Hidden child');
await act(() =>ReactDOMClient.hydrateRoot(container, <App />, {
onRecoverableError(error) {Scheduler.log('Log recoverable error: ' + error.message);
},}),);expect(ref.current).toBe(span);
expect(span.innerHTML).toBe('Hidden child');
});// @gate wwwit('renders a hidden LegacyHidden component inside a Suspense boundary', async () => {const ref = React.createRef();
function App() {return (<Suspense fallback="Loading...">
<LegacyHiddenDiv mode="hidden"><span ref={ref}>Hidden child</span></LegacyHiddenDiv></Suspense>);}const finalHTML = ReactDOMServer.renderToString(<App />);
const container = document.createElement('div');
container.innerHTML = finalHTML;
const span = container.getElementsByTagName('span')[0];
expect(span.innerHTML).toBe('Hidden child');
await act(() => ReactDOMClient.hydrateRoot(container, <App />));
expect(ref.current).toBe(span);
expect(span.innerHTML).toBe('Hidden child');
});// @gate wwwit('renders a visible LegacyHidden component', async () => {const ref = React.createRef();
function App() {return (<LegacyHiddenDiv mode="visible"><span ref={ref}>Hidden child</span></LegacyHiddenDiv>);}const finalHTML = ReactDOMServer.renderToString(<App />);
const container = document.createElement('div');
container.innerHTML = finalHTML;
const span = container.getElementsByTagName('span')[0];
await act(() => ReactDOMClient.hydrateRoot(container, <App />));
expect(ref.current).toBe(span);
expect(ref.current.innerHTML).toBe('Hidden child');
});// @gate enableActivityit('a visible Activity component acts like a fragment', async () => {const ref = React.createRef();
function App() {return (<Activity mode="visible"><span ref={ref}>Child</span></Activity>);}const finalHTML = ReactDOMServer.renderToString(<App />);
assertLog([]);
const container = document.createElement('div');
container.innerHTML = finalHTML;
// Visible Activity boundaries behave exactly like fragments: a// pure indirection.
expect(container).toMatchInlineSnapshot(`
<div><span>Child</span></div>`);const span = container.getElementsByTagName('span')[0];
// The tree successfully hydratesReactDOMClient.hydrateRoot(container, <App />);
await waitForAll([]);
expect(ref.current).toBe(span);
});// @gate enableActivityit('a hidden Activity component is skipped over during server rendering', async () => {const visibleRef = React.createRef();
function HiddenChild() {Scheduler.log('HiddenChild');
return <span>Hidden</span>;}function App() {Scheduler.log('App');
return (<><span ref={visibleRef}>Visible</span><Activity mode="hidden"><HiddenChild /></Activity></>);}// During server rendering, the Child component should not be evaluated,// because it's inside a hidden tree.
const finalHTML = ReactDOMServer.renderToString(<App />);
assertLog(['App']);
const container = document.createElement('div');
container.innerHTML = finalHTML;
// The hidden child is not part of the server rendered HTMLexpect(container).toMatchInlineSnapshot(`
<div><span>Visible</span></div>`);const visibleSpan = container.getElementsByTagName('span')[0];
// The visible span successfully hydratesReactDOMClient.hydrateRoot(container, <App />);
await waitForPaint(['App']);
expect(visibleRef.current).toBe(visibleSpan);
// Subsequently, the hidden child is prerendered on the clientawait waitForPaint(['HiddenChild']);
expect(container).toMatchInlineSnapshot(`
<div><span>Visible</span><spanstyle="display: none;">Hidden</span></div>`);});function itHydratesWithoutMismatch(msg, App) {it('hydrates without mismatch ' + msg, async () => {
const container = document.createElement('div');
document.body.appendChild(container);
const finalHTML = ReactDOMServer.renderToString(<App />);
container.innerHTML = finalHTML;
await act(() => ReactDOMClient.hydrateRoot(container, <App />));
});}itHydratesWithoutMismatch('an empty string with neighbors', function App() {return (<div><div id="test">Test</div>{'' && <div>Test</div>}{'Test'}</div>);});itHydratesWithoutMismatch('an empty string', function App() {return '';});itHydratesWithoutMismatch('an empty string simple in fragment',function App() {return (<>{''}{'sup'}</>);},);itHydratesWithoutMismatch('an empty string simple in suspense',function App() {return <Suspense>{'' && false}</Suspense>;},);itHydratesWithoutMismatch('an empty string in class component', TestAppClass);it('fallback to client render on hydration mismatch at root', async () => {let suspend = true;let resolve;const promise = new Promise((res, rej) => {resolve = () => {suspend = false;res();};});function App({isClient}) {return (<><Suspense fallback={<div>Loading</div>}><ChildThatSuspends id={1} isClient={isClient} />
</Suspense>{isClient ? <span>client</span> : <div>server</div>}
<Suspense fallback={<div>Loading</div>}><ChildThatSuspends id={2} isClient={isClient} />
</Suspense></>);}function ChildThatSuspends({id, isClient}) {if (isClient && suspend) {throw promise;}return <div>{id}</div>;}const finalHTML = ReactDOMServer.renderToString(<App isClient={false} />);
const container = document.createElement('div');
document.body.appendChild(container);
container.innerHTML = finalHTML;
await expect(async () => {await act(() => {ReactDOMClient.hydrateRoot(container, <App isClient={true} />, {
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>.','Warning: Expected server HTML to contain a matching <span> in <div>.\n' +
' in span (at **)\n' +
' in App (at **)',],
{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.',]);
// We show fallback state when mismatch happens at rootexpect(container.innerHTML).toEqual(
'<div>Loading</div><span>client</span><div>Loading</div>',);await act(async () => {resolve();await promise;});expect(container.innerHTML).toEqual(
'<div>1</div><span>client</span><div>2</div>',);});// @gate enableClientRenderFallbackOnTextMismatchit("falls back to client rendering when there's a text mismatch (direct text child)", async () => {function DirectTextChild({text}) {return <div>{text}</div>;}const container = document.createElement('div');
container.innerHTML = ReactDOMServer.renderToString(
<DirectTextChild text="good" />,);await expect(async () => {await act(() => {ReactDOMClient.hydrateRoot(container, <DirectTextChild text="bad" />, {
onRecoverableError(error) {Scheduler.log(error.message);
},});});}).toErrorDev(
[
'Text content did not match. Server: "good" Client: "bad"','An error occurred during hydration. The server HTML was replaced with ' +'client content in <div>.',],
{withoutStack: 1},);assertLog([
'Text content does not match server-rendered HTML.','There was an error while hydrating. Because the error happened outside ' +'of a Suspense boundary, the entire root will switch to client rendering.',]);
});// @gate enableClientRenderFallbackOnTextMismatchit("falls back to client rendering when there's a text mismatch (text child with siblings)", async () => {function Sibling() {return 'Sibling';}function TextChildWithSibling({text}) {return (<div><Sibling />{text}</div>);}const container2 = document.createElement('div');
container2.innerHTML = ReactDOMServer.renderToString(
<TextChildWithSibling text="good" />,);await expect(async () => {await act(() => {ReactDOMClient.hydrateRoot(
container2,<TextChildWithSibling text="bad" />,{onRecoverableError(error) {Scheduler.log(error.message);
},},);});}).toErrorDev(
[
'Text content did not match. Server: "good" Client: "bad"','An error occurred during hydration. The server HTML was replaced with ' +'client content in <div>.',],
{withoutStack: 1},);assertLog([
'Text content does not match server-rendered HTML.','There was an error while hydrating. Because the error happened outside ' +'of a Suspense boundary, the entire root will switch to client rendering.',]);
});});