diff --git a/packages/react-reconciler/src/__tests__/ReactLazy-test.internal.js b/packages/react-reconciler/src/__tests__/ReactLazy-test.internal.js
index c592e91e94096..769679c9a7013 100644
--- a/packages/react-reconciler/src/__tests__/ReactLazy-test.internal.js
+++ b/packages/react-reconciler/src/__tests__/ReactLazy-test.internal.js
@@ -1631,369 +1631,4 @@ describe('ReactLazy', () => {
]);
expect(root).toMatchRenderedOutput('ba');
});
-
- it('does not destroy layout effects twice when hidden child is removed', async () => {
- function ChildA({label}) {
- React.useLayoutEffect(() => {
- Scheduler.unstable_yieldValue('Did mount: ' + label);
- return () => {
- Scheduler.unstable_yieldValue('Will unmount: ' + label);
- };
- }, []);
- return ;
- }
-
- function ChildB({label}) {
- React.useLayoutEffect(() => {
- Scheduler.unstable_yieldValue('Did mount: ' + label);
- return () => {
- Scheduler.unstable_yieldValue('Will unmount: ' + label);
- };
- }, []);
- return ;
- }
-
- const LazyChildA = lazy(() => fakeImport(ChildA));
- const LazyChildB = lazy(() => fakeImport(ChildB));
-
- function Parent({swap}) {
- return (
- }>
- {swap ? : }
-
- );
- }
-
- const root = ReactTestRenderer.create(, {
- unstable_isConcurrent: true,
- });
-
- expect(Scheduler).toFlushAndYield(['Loading...']);
-
- await LazyChildA;
- expect(Scheduler).toFlushAndYield(['A', 'Did mount: A']);
- expect(root).toMatchRenderedOutput('A');
-
- // Swap the position of A and B
- root.unstable_flushSync(() => {
- root.update();
- });
- expect(Scheduler).toHaveYielded(['Loading...', 'Will unmount: A']);
- expect(root).toMatchRenderedOutput('Loading...');
-
- await LazyChildB;
- expect(Scheduler).toFlushAndYield(['B', 'Did mount: B']);
- expect(root).toMatchRenderedOutput('B');
- });
-
- it('does not destroy ref cleanup twice when hidden child is removed', async () => {
- function ChildA({label}) {
- return (
- {
- if (node) {
- Scheduler.unstable_yieldValue('Ref mount: ' + label);
- } else {
- Scheduler.unstable_yieldValue('Ref unmount: ' + label);
- }
- }}>
-
-
- );
- }
-
- function ChildB({label}) {
- return (
- {
- if (node) {
- Scheduler.unstable_yieldValue('Ref mount: ' + label);
- } else {
- Scheduler.unstable_yieldValue('Ref unmount: ' + label);
- }
- }}>
-
-
- );
- }
-
- const LazyChildA = lazy(() => fakeImport(ChildA));
- const LazyChildB = lazy(() => fakeImport(ChildB));
-
- function Parent({swap}) {
- return (
- }>
- {swap ? : }
-
- );
- }
-
- const root = ReactTestRenderer.create(, {
- unstable_isConcurrent: true,
- createNodeMock() {
- return {};
- },
- });
-
- expect(Scheduler).toFlushAndYield(['Loading...']);
-
- await LazyChildA;
- expect(Scheduler).toFlushAndYield(['A', 'Ref mount: A']);
- expect(root).toMatchRenderedOutput(A);
-
- // Swap the position of A and B
- root.unstable_flushSync(() => {
- root.update();
- });
- expect(Scheduler).toHaveYielded(['Loading...', 'Ref unmount: A']);
- expect(root).toMatchRenderedOutput('Loading...');
-
- await LazyChildB;
- expect(Scheduler).toFlushAndYield(['B', 'Ref mount: B']);
- expect(root).toMatchRenderedOutput(B);
- });
-
- it('does not call componentWillUnmount twice when hidden child is removed', async () => {
- class ChildA extends React.Component {
- componentDidMount() {
- Scheduler.unstable_yieldValue('Did mount: ' + this.props.label);
- }
- componentWillUnmount() {
- Scheduler.unstable_yieldValue('Will unmount: ' + this.props.label);
- }
- render() {
- return ;
- }
- }
-
- class ChildB extends React.Component {
- componentDidMount() {
- Scheduler.unstable_yieldValue('Did mount: ' + this.props.label);
- }
- componentWillUnmount() {
- Scheduler.unstable_yieldValue('Will unmount: ' + this.props.label);
- }
- render() {
- return ;
- }
- }
-
- const LazyChildA = lazy(() => fakeImport(ChildA));
- const LazyChildB = lazy(() => fakeImport(ChildB));
-
- function Parent({swap}) {
- return (
- }>
- {swap ? : }
-
- );
- }
-
- const root = ReactTestRenderer.create(, {
- unstable_isConcurrent: true,
- });
-
- expect(Scheduler).toFlushAndYield(['Loading...']);
-
- await LazyChildA;
- expect(Scheduler).toFlushAndYield(['A', 'Did mount: A']);
- expect(root).toMatchRenderedOutput('A');
-
- // Swap the position of A and B
- root.unstable_flushSync(() => {
- root.update();
- });
- expect(Scheduler).toHaveYielded(['Loading...', 'Will unmount: A']);
- expect(root).toMatchRenderedOutput('Loading...');
-
- await LazyChildB;
- expect(Scheduler).toFlushAndYield(['B', 'Did mount: B']);
- expect(root).toMatchRenderedOutput('B');
- });
-
- it('does not destroy layout effects twice when parent suspense is removed', async () => {
- function ChildA({label}) {
- React.useLayoutEffect(() => {
- Scheduler.unstable_yieldValue('Did mount: ' + label);
- return () => {
- Scheduler.unstable_yieldValue('Will unmount: ' + label);
- };
- }, []);
- return ;
- }
- function ChildB({label}) {
- React.useLayoutEffect(() => {
- Scheduler.unstable_yieldValue('Did mount: ' + label);
- return () => {
- Scheduler.unstable_yieldValue('Will unmount: ' + label);
- };
- }, []);
- return ;
- }
- const LazyChildA = lazy(() => fakeImport(ChildA));
- const LazyChildB = lazy(() => fakeImport(ChildB));
-
- function Parent({swap}) {
- return (
- }>
- {swap ? : }
-
- );
- }
-
- const root = ReactTestRenderer.create(, {
- unstable_isConcurrent: true,
- });
-
- expect(Scheduler).toFlushAndYield(['Loading...']);
-
- await LazyChildA;
- expect(Scheduler).toFlushAndYield(['A', 'Did mount: A']);
- expect(root).toMatchRenderedOutput('A');
-
- // Swap the position of A and B
- root.unstable_flushSync(() => {
- root.update();
- });
- expect(Scheduler).toHaveYielded(['Loading...', 'Will unmount: A']);
- expect(root).toMatchRenderedOutput('Loading...');
-
- // Destroy the whole tree, including the hidden A
- root.unstable_flushSync(() => {
- root.update(
Hello
);
- });
- expect(Scheduler).toFlushAndYield([]);
- expect(root).toMatchRenderedOutput(Hello
);
- });
-
- it('does not destroy ref cleanup twice when parent suspense is removed', async () => {
- function ChildA({label}) {
- return (
- {
- if (node) {
- Scheduler.unstable_yieldValue('Ref mount: ' + label);
- } else {
- Scheduler.unstable_yieldValue('Ref unmount: ' + label);
- }
- }}>
-
-
- );
- }
-
- function ChildB({label}) {
- return (
- {
- if (node) {
- Scheduler.unstable_yieldValue('Ref mount: ' + label);
- } else {
- Scheduler.unstable_yieldValue('Ref unmount: ' + label);
- }
- }}>
-
-
- );
- }
-
- const LazyChildA = lazy(() => fakeImport(ChildA));
- const LazyChildB = lazy(() => fakeImport(ChildB));
-
- function Parent({swap}) {
- return (
- }>
- {swap ? : }
-
- );
- }
-
- const root = ReactTestRenderer.create(, {
- unstable_isConcurrent: true,
- createNodeMock() {
- return {};
- },
- });
-
- expect(Scheduler).toFlushAndYield(['Loading...']);
-
- await LazyChildA;
- expect(Scheduler).toFlushAndYield(['A', 'Ref mount: A']);
- expect(root).toMatchRenderedOutput(A);
-
- // Swap the position of A and B
- root.unstable_flushSync(() => {
- root.update();
- });
- expect(Scheduler).toHaveYielded(['Loading...', 'Ref unmount: A']);
- expect(root).toMatchRenderedOutput('Loading...');
-
- // Destroy the whole tree, including the hidden A
- root.unstable_flushSync(() => {
- root.update(Hello
);
- });
- expect(Scheduler).toFlushAndYield([]);
- expect(root).toMatchRenderedOutput(Hello
);
- });
-
- it('does not call componentWillUnmount twice when parent suspense is removed', async () => {
- class ChildA extends React.Component {
- componentDidMount() {
- Scheduler.unstable_yieldValue('Did mount: ' + this.props.label);
- }
- componentWillUnmount() {
- Scheduler.unstable_yieldValue('Will unmount: ' + this.props.label);
- }
- render() {
- return ;
- }
- }
-
- class ChildB extends React.Component {
- componentDidMount() {
- Scheduler.unstable_yieldValue('Did mount: ' + this.props.label);
- }
- componentWillUnmount() {
- Scheduler.unstable_yieldValue('Will unmount: ' + this.props.label);
- }
- render() {
- return ;
- }
- }
-
- const LazyChildA = lazy(() => fakeImport(ChildA));
- const LazyChildB = lazy(() => fakeImport(ChildB));
-
- function Parent({swap}) {
- return (
- }>
- {swap ? : }
-
- );
- }
-
- const root = ReactTestRenderer.create(, {
- unstable_isConcurrent: true,
- });
-
- expect(Scheduler).toFlushAndYield(['Loading...']);
-
- await LazyChildA;
- expect(Scheduler).toFlushAndYield(['A', 'Did mount: A']);
- expect(root).toMatchRenderedOutput('A');
-
- // Swap the position of A and B
- root.unstable_flushSync(() => {
- root.update();
- });
- expect(Scheduler).toHaveYielded(['Loading...', 'Will unmount: A']);
- expect(root).toMatchRenderedOutput('Loading...');
-
- // Destroy the whole tree, including the hidden A
- root.unstable_flushSync(() => {
- root.update(Hello
);
- });
- expect(Scheduler).toFlushAndYield([]);
- expect(root).toMatchRenderedOutput(Hello
);
- });
});
diff --git a/packages/react-reconciler/src/__tests__/ReactSuspenseEffectsSemanticsDOM-test.js b/packages/react-reconciler/src/__tests__/ReactSuspenseEffectsSemanticsDOM-test.js
index cb1196baffe6a..00c126bb36450 100644
--- a/packages/react-reconciler/src/__tests__/ReactSuspenseEffectsSemanticsDOM-test.js
+++ b/packages/react-reconciler/src/__tests__/ReactSuspenseEffectsSemanticsDOM-test.js
@@ -10,18 +10,39 @@
'use strict';
let React;
+let ReactDOM;
let ReactDOMClient;
+let Scheduler;
let act;
+let container;
describe('ReactSuspenseEffectsSemanticsDOM', () => {
beforeEach(() => {
jest.resetModules();
React = require('react');
+ ReactDOM = require('react-dom');
ReactDOMClient = require('react-dom/client');
+ Scheduler = require('scheduler');
act = require('jest-react').act;
+
+ container = document.createElement('div');
+ document.body.appendChild(container);
+ });
+
+ afterEach(() => {
+ document.body.removeChild(container);
});
+ async function fakeImport(result) {
+ return {default: result};
+ }
+
+ function Text(props) {
+ Scheduler.unstable_yieldValue(props.text);
+ return props.text;
+ }
+
it('should not cause a cycle when combined with a render phase update', () => {
let scheduleSuspendingUpdate;
@@ -63,7 +84,7 @@ describe('ReactSuspenseEffectsSemanticsDOM', () => {
}
act(() => {
- const root = ReactDOMClient.createRoot(document.createElement('div'));
+ const root = ReactDOMClient.createRoot(container);
root.render();
});
@@ -71,4 +92,367 @@ describe('ReactSuspenseEffectsSemanticsDOM', () => {
scheduleSuspendingUpdate();
});
});
+
+ it('does not destroy layout effects twice when hidden child is removed', async () => {
+ function ChildA({label}) {
+ React.useLayoutEffect(() => {
+ Scheduler.unstable_yieldValue('Did mount: ' + label);
+ return () => {
+ Scheduler.unstable_yieldValue('Will unmount: ' + label);
+ };
+ }, []);
+ return ;
+ }
+
+ function ChildB({label}) {
+ React.useLayoutEffect(() => {
+ Scheduler.unstable_yieldValue('Did mount: ' + label);
+ return () => {
+ Scheduler.unstable_yieldValue('Will unmount: ' + label);
+ };
+ }, []);
+ return ;
+ }
+
+ const LazyChildA = React.lazy(() => fakeImport(ChildA));
+ const LazyChildB = React.lazy(() => fakeImport(ChildB));
+
+ function Parent({swap}) {
+ return (
+ }>
+ {swap ? : }
+
+ );
+ }
+
+ const root = ReactDOMClient.createRoot(container);
+ act(() => {
+ root.render();
+ });
+ expect(Scheduler).toHaveYielded(['Loading...']);
+
+ await LazyChildA;
+ expect(Scheduler).toFlushAndYield(['A', 'Did mount: A']);
+ expect(container.innerHTML).toBe('A');
+
+ // Swap the position of A and B
+ ReactDOM.flushSync(() => {
+ root.render();
+ });
+ expect(Scheduler).toHaveYielded(['Loading...', 'Will unmount: A']);
+ expect(container.innerHTML).toBe('Loading...');
+
+ await LazyChildB;
+ expect(Scheduler).toFlushAndYield(['B', 'Did mount: B']);
+ expect(container.innerHTML).toBe('B');
+ });
+
+ it('does not destroy ref cleanup twice when hidden child is removed', async () => {
+ function ChildA({label}) {
+ return (
+ {
+ if (node) {
+ Scheduler.unstable_yieldValue('Ref mount: ' + label);
+ } else {
+ Scheduler.unstable_yieldValue('Ref unmount: ' + label);
+ }
+ }}>
+
+
+ );
+ }
+
+ function ChildB({label}) {
+ return (
+ {
+ if (node) {
+ Scheduler.unstable_yieldValue('Ref mount: ' + label);
+ } else {
+ Scheduler.unstable_yieldValue('Ref unmount: ' + label);
+ }
+ }}>
+
+
+ );
+ }
+
+ const LazyChildA = React.lazy(() => fakeImport(ChildA));
+ const LazyChildB = React.lazy(() => fakeImport(ChildB));
+
+ function Parent({swap}) {
+ return (
+ }>
+ {swap ? : }
+
+ );
+ }
+
+ const root = ReactDOMClient.createRoot(container);
+ act(() => {
+ root.render();
+ });
+ expect(Scheduler).toHaveYielded(['Loading...']);
+
+ await LazyChildA;
+ expect(Scheduler).toFlushAndYield(['A', 'Ref mount: A']);
+ expect(container.innerHTML).toBe('A');
+
+ // Swap the position of A and B
+ ReactDOM.flushSync(() => {
+ root.render();
+ });
+ expect(Scheduler).toHaveYielded(['Loading...', 'Ref unmount: A']);
+ expect(container.innerHTML).toBe(
+ 'ALoading...',
+ );
+
+ await LazyChildB;
+ expect(Scheduler).toFlushAndYield(['B', 'Ref mount: B']);
+ expect(container.innerHTML).toBe('B');
+ });
+
+ it('does not call componentWillUnmount twice when hidden child is removed', async () => {
+ class ChildA extends React.Component {
+ componentDidMount() {
+ Scheduler.unstable_yieldValue('Did mount: ' + this.props.label);
+ }
+ componentWillUnmount() {
+ Scheduler.unstable_yieldValue('Will unmount: ' + this.props.label);
+ }
+ render() {
+ return ;
+ }
+ }
+
+ class ChildB extends React.Component {
+ componentDidMount() {
+ Scheduler.unstable_yieldValue('Did mount: ' + this.props.label);
+ }
+ componentWillUnmount() {
+ Scheduler.unstable_yieldValue('Will unmount: ' + this.props.label);
+ }
+ render() {
+ return ;
+ }
+ }
+
+ const LazyChildA = React.lazy(() => fakeImport(ChildA));
+ const LazyChildB = React.lazy(() => fakeImport(ChildB));
+
+ function Parent({swap}) {
+ return (
+ }>
+ {swap ? : }
+
+ );
+ }
+
+ const root = ReactDOMClient.createRoot(container);
+ act(() => {
+ root.render();
+ });
+ expect(Scheduler).toHaveYielded(['Loading...']);
+
+ await LazyChildA;
+ expect(Scheduler).toFlushAndYield(['A', 'Did mount: A']);
+ expect(container.innerHTML).toBe('A');
+
+ // Swap the position of A and B
+ ReactDOM.flushSync(() => {
+ root.render();
+ });
+ expect(Scheduler).toHaveYielded(['Loading...', 'Will unmount: A']);
+ expect(container.innerHTML).toBe('Loading...');
+
+ await LazyChildB;
+ expect(Scheduler).toFlushAndYield(['B', 'Did mount: B']);
+ expect(container.innerHTML).toBe('B');
+ });
+
+ it('does not destroy layout effects twice when parent suspense is removed', async () => {
+ function ChildA({label}) {
+ React.useLayoutEffect(() => {
+ Scheduler.unstable_yieldValue('Did mount: ' + label);
+ return () => {
+ Scheduler.unstable_yieldValue('Will unmount: ' + label);
+ };
+ }, []);
+ return ;
+ }
+ function ChildB({label}) {
+ React.useLayoutEffect(() => {
+ Scheduler.unstable_yieldValue('Did mount: ' + label);
+ return () => {
+ Scheduler.unstable_yieldValue('Will unmount: ' + label);
+ };
+ }, []);
+ return ;
+ }
+ const LazyChildA = React.lazy(() => fakeImport(ChildA));
+ const LazyChildB = React.lazy(() => fakeImport(ChildB));
+
+ function Parent({swap}) {
+ return (
+ }>
+ {swap ? : }
+
+ );
+ }
+
+ const root = ReactDOMClient.createRoot(container);
+ act(() => {
+ root.render();
+ });
+ expect(Scheduler).toHaveYielded(['Loading...']);
+
+ await LazyChildA;
+ expect(Scheduler).toFlushAndYield(['A', 'Did mount: A']);
+ expect(container.innerHTML).toBe('A');
+
+ // Swap the position of A and B
+ ReactDOM.flushSync(() => {
+ root.render();
+ });
+ expect(Scheduler).toHaveYielded(['Loading...', 'Will unmount: A']);
+ expect(container.innerHTML).toBe('Loading...');
+
+ // Destroy the whole tree, including the hidden A
+ ReactDOM.flushSync(() => {
+ root.render(Hello
);
+ });
+ expect(Scheduler).toFlushAndYield([]);
+ expect(container.innerHTML).toBe('Hello
');
+ });
+
+ it('does not destroy ref cleanup twice when parent suspense is removed', async () => {
+ function ChildA({label}) {
+ return (
+ {
+ if (node) {
+ Scheduler.unstable_yieldValue('Ref mount: ' + label);
+ } else {
+ Scheduler.unstable_yieldValue('Ref unmount: ' + label);
+ }
+ }}>
+
+
+ );
+ }
+
+ function ChildB({label}) {
+ return (
+ {
+ if (node) {
+ Scheduler.unstable_yieldValue('Ref mount: ' + label);
+ } else {
+ Scheduler.unstable_yieldValue('Ref unmount: ' + label);
+ }
+ }}>
+
+
+ );
+ }
+
+ const LazyChildA = React.lazy(() => fakeImport(ChildA));
+ const LazyChildB = React.lazy(() => fakeImport(ChildB));
+
+ function Parent({swap}) {
+ return (
+ }>
+ {swap ? : }
+
+ );
+ }
+
+ const root = ReactDOMClient.createRoot(container);
+ act(() => {
+ root.render();
+ });
+ expect(Scheduler).toHaveYielded(['Loading...']);
+
+ await LazyChildA;
+ expect(Scheduler).toFlushAndYield(['A', 'Ref mount: A']);
+ expect(container.innerHTML).toBe('A');
+
+ // Swap the position of A and B
+ ReactDOM.flushSync(() => {
+ root.render();
+ });
+ expect(Scheduler).toHaveYielded(['Loading...', 'Ref unmount: A']);
+ expect(container.innerHTML).toBe(
+ 'ALoading...',
+ );
+
+ // Destroy the whole tree, including the hidden A
+ ReactDOM.flushSync(() => {
+ root.render(Hello
);
+ });
+ expect(Scheduler).toFlushAndYield([]);
+ expect(container.innerHTML).toBe('Hello
');
+ });
+
+ it('does not call componentWillUnmount twice when parent suspense is removed', async () => {
+ class ChildA extends React.Component {
+ componentDidMount() {
+ Scheduler.unstable_yieldValue('Did mount: ' + this.props.label);
+ }
+ componentWillUnmount() {
+ Scheduler.unstable_yieldValue('Will unmount: ' + this.props.label);
+ }
+ render() {
+ return ;
+ }
+ }
+
+ class ChildB extends React.Component {
+ componentDidMount() {
+ Scheduler.unstable_yieldValue('Did mount: ' + this.props.label);
+ }
+ componentWillUnmount() {
+ Scheduler.unstable_yieldValue('Will unmount: ' + this.props.label);
+ }
+ render() {
+ return ;
+ }
+ }
+
+ const LazyChildA = React.lazy(() => fakeImport(ChildA));
+ const LazyChildB = React.lazy(() => fakeImport(ChildB));
+
+ function Parent({swap}) {
+ return (
+ }>
+ {swap ? : }
+
+ );
+ }
+
+ const root = ReactDOMClient.createRoot(container);
+ act(() => {
+ root.render();
+ });
+ expect(Scheduler).toHaveYielded(['Loading...']);
+
+ await LazyChildA;
+ expect(Scheduler).toFlushAndYield(['A', 'Did mount: A']);
+ expect(container.innerHTML).toBe('A');
+
+ // Swap the position of A and B
+ ReactDOM.flushSync(() => {
+ root.render();
+ });
+ expect(Scheduler).toHaveYielded(['Loading...', 'Will unmount: A']);
+ expect(container.innerHTML).toBe('Loading...');
+
+ // Destroy the whole tree, including the hidden A
+ ReactDOM.flushSync(() => {
+ root.render(Hello
);
+ });
+ expect(Scheduler).toFlushAndYield([]);
+ expect(container.innerHTML).toBe('Hello
');
+ });
});