mirror of
https://github.com/zebrajr/react.git
synced 2025-12-06 12:20:20 +01:00
3191 lines
102 KiB
JavaScript
3191 lines
102 KiB
JavaScript
/**
|
|
* 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.
|
|
*
|
|
* @flow
|
|
*/
|
|
|
|
let React;
|
|
let ReactNoop;
|
|
let Scheduler;
|
|
let act;
|
|
let Suspense;
|
|
let getCacheForType;
|
|
let caches;
|
|
let seededCache;
|
|
let ErrorBoundary;
|
|
|
|
// TODO: These tests don't pass in persistent mode yet. Need to implement.
|
|
|
|
describe('ReactSuspenseEffectsSemantics', () => {
|
|
beforeEach(() => {
|
|
jest.resetModules();
|
|
|
|
React = require('react');
|
|
ReactNoop = require('react-noop-renderer');
|
|
Scheduler = require('scheduler');
|
|
act = require('jest-react').act;
|
|
Suspense = React.Suspense;
|
|
|
|
getCacheForType = React.unstable_getCacheForType;
|
|
|
|
caches = [];
|
|
seededCache = null;
|
|
|
|
ErrorBoundary = class extends React.Component {
|
|
state = {error: null};
|
|
componentDidCatch(error) {
|
|
this.setState({error});
|
|
}
|
|
render() {
|
|
if (this.state.error) {
|
|
Scheduler.unstable_yieldValue('ErrorBoundary render: catch');
|
|
return this.props.fallback;
|
|
}
|
|
Scheduler.unstable_yieldValue('ErrorBoundary render: try');
|
|
return this.props.children;
|
|
}
|
|
};
|
|
});
|
|
|
|
function createTextCache() {
|
|
if (seededCache !== null) {
|
|
// Trick to seed a cache before it exists.
|
|
// TODO: Need a built-in API to seed data before the initial render (i.e.
|
|
// not a refresh because nothing has mounted yet).
|
|
const cache = seededCache;
|
|
seededCache = null;
|
|
return cache;
|
|
}
|
|
|
|
const data = new Map();
|
|
const version = caches.length + 1;
|
|
const cache = {
|
|
version,
|
|
data,
|
|
resolve(text) {
|
|
const record = data.get(text);
|
|
if (record === undefined) {
|
|
const newRecord = {
|
|
status: 'resolved',
|
|
value: text,
|
|
};
|
|
data.set(text, newRecord);
|
|
} else if (record.status === 'pending') {
|
|
const thenable = record.value;
|
|
record.status = 'resolved';
|
|
record.value = text;
|
|
thenable.pings.forEach(t => t());
|
|
}
|
|
},
|
|
reject(text, error) {
|
|
const record = data.get(text);
|
|
if (record === undefined) {
|
|
const newRecord = {
|
|
status: 'rejected',
|
|
value: error,
|
|
};
|
|
data.set(text, newRecord);
|
|
} else if (record.status === 'pending') {
|
|
const thenable = record.value;
|
|
record.status = 'rejected';
|
|
record.value = error;
|
|
thenable.pings.forEach(t => t());
|
|
}
|
|
},
|
|
};
|
|
caches.push(cache);
|
|
return cache;
|
|
}
|
|
|
|
function readText(text) {
|
|
const textCache = getCacheForType(createTextCache);
|
|
const record = textCache.data.get(text);
|
|
if (record !== undefined) {
|
|
switch (record.status) {
|
|
case 'pending':
|
|
Scheduler.unstable_yieldValue(`Suspend:${text}`);
|
|
throw record.value;
|
|
case 'rejected':
|
|
Scheduler.unstable_yieldValue(`Error:${text}`);
|
|
throw record.value;
|
|
case 'resolved':
|
|
return textCache.version;
|
|
}
|
|
} else {
|
|
Scheduler.unstable_yieldValue(`Suspend:${text}`);
|
|
|
|
const thenable = {
|
|
pings: [],
|
|
then(resolve) {
|
|
if (newRecord.status === 'pending') {
|
|
thenable.pings.push(resolve);
|
|
} else {
|
|
Promise.resolve().then(() => resolve(newRecord.value));
|
|
}
|
|
},
|
|
};
|
|
|
|
const newRecord = {
|
|
status: 'pending',
|
|
value: thenable,
|
|
};
|
|
textCache.data.set(text, newRecord);
|
|
|
|
throw thenable;
|
|
}
|
|
}
|
|
|
|
function Text({children = null, text}) {
|
|
Scheduler.unstable_yieldValue(`Text:${text} render`);
|
|
React.useLayoutEffect(() => {
|
|
Scheduler.unstable_yieldValue(`Text:${text} create layout`);
|
|
return () => {
|
|
Scheduler.unstable_yieldValue(`Text:${text} destroy layout`);
|
|
};
|
|
}, []);
|
|
React.useEffect(() => {
|
|
Scheduler.unstable_yieldValue(`Text:${text} create passive`);
|
|
return () => {
|
|
Scheduler.unstable_yieldValue(`Text:${text} destroy passive`);
|
|
};
|
|
}, []);
|
|
return <span prop={text}>{children}</span>;
|
|
}
|
|
|
|
function AsyncText({children = null, text}) {
|
|
readText(text);
|
|
Scheduler.unstable_yieldValue(`AsyncText:${text} render`);
|
|
React.useLayoutEffect(() => {
|
|
Scheduler.unstable_yieldValue(`AsyncText:${text} create layout`);
|
|
return () => {
|
|
Scheduler.unstable_yieldValue(`AsyncText:${text} destroy layout`);
|
|
};
|
|
}, []);
|
|
React.useEffect(() => {
|
|
Scheduler.unstable_yieldValue(`AsyncText:${text} create passive`);
|
|
return () => {
|
|
Scheduler.unstable_yieldValue(`AsyncText:${text} destroy passive`);
|
|
};
|
|
}, []);
|
|
return <span prop={text}>{children}</span>;
|
|
}
|
|
|
|
function resolveMostRecentTextCache(text) {
|
|
if (caches.length === 0) {
|
|
throw Error('Cache does not exist.');
|
|
} else {
|
|
// Resolve the most recently created cache. An older cache can by
|
|
// resolved with `caches[index].resolve(text)`.
|
|
caches[caches.length - 1].resolve(text);
|
|
}
|
|
}
|
|
|
|
const resolveText = resolveMostRecentTextCache;
|
|
|
|
function span(prop, children = []) {
|
|
return {type: 'span', children, prop, hidden: false};
|
|
}
|
|
|
|
function spanHidden(prop, children = []) {
|
|
return {type: 'span', children, prop, hidden: true};
|
|
}
|
|
|
|
function advanceTimers(ms) {
|
|
// Note: This advances Jest's virtual time but not React's. Use
|
|
// ReactNoop.expire for that.
|
|
if (typeof ms !== 'number') {
|
|
throw new Error('Must specify ms');
|
|
}
|
|
jest.advanceTimersByTime(ms);
|
|
// Wait until the end of the current tick
|
|
// We cannot use a timer since we're faking them
|
|
return Promise.resolve().then(() => {});
|
|
}
|
|
|
|
describe('when a component suspends during initial mount', () => {
|
|
// @gate enableLegacyCache
|
|
it('should not change behavior in concurrent mode', async () => {
|
|
class ClassText extends React.Component {
|
|
componentDidMount() {
|
|
const {text} = this.props;
|
|
Scheduler.unstable_yieldValue(`ClassText:${text} componentDidMount`);
|
|
}
|
|
componentDidUpdate() {
|
|
const {text} = this.props;
|
|
Scheduler.unstable_yieldValue(`ClassText:${text} componentDidUpdate`);
|
|
}
|
|
componentWillUnmount() {
|
|
const {text} = this.props;
|
|
Scheduler.unstable_yieldValue(
|
|
`ClassText:${text} componentWillUnmount`,
|
|
);
|
|
}
|
|
render() {
|
|
const {children, text} = this.props;
|
|
Scheduler.unstable_yieldValue(`ClassText:${text} render`);
|
|
return <span prop={text}>{children}</span>;
|
|
}
|
|
}
|
|
|
|
function App({children = null}) {
|
|
Scheduler.unstable_yieldValue('App render');
|
|
React.useLayoutEffect(() => {
|
|
Scheduler.unstable_yieldValue('App create layout');
|
|
return () => {
|
|
Scheduler.unstable_yieldValue('App destroy layout');
|
|
};
|
|
}, []);
|
|
React.useEffect(() => {
|
|
Scheduler.unstable_yieldValue('App create passive');
|
|
return () => {
|
|
Scheduler.unstable_yieldValue('App destroy passive');
|
|
};
|
|
}, []);
|
|
return (
|
|
<>
|
|
<Suspense fallback={<Text text="Fallback" />}>
|
|
<Text text="Inside:Before" />
|
|
{children}
|
|
<ClassText text="Inside:After" />
|
|
</Suspense>
|
|
<Text text="Outside" />
|
|
</>
|
|
);
|
|
}
|
|
|
|
// Mount and suspend.
|
|
await act(async () => {
|
|
ReactNoop.render(
|
|
<App>
|
|
<AsyncText text="Async" ms={1000} />
|
|
</App>,
|
|
);
|
|
});
|
|
expect(Scheduler).toHaveYielded([
|
|
'App render',
|
|
'Text:Inside:Before render',
|
|
'Suspend:Async',
|
|
'ClassText:Inside:After render',
|
|
'Text:Fallback render',
|
|
'Text:Outside render',
|
|
'Text:Fallback create layout',
|
|
'Text:Outside create layout',
|
|
'App create layout',
|
|
'Text:Fallback create passive',
|
|
'Text:Outside create passive',
|
|
'App create passive',
|
|
]);
|
|
expect(ReactNoop.getChildren()).toEqual([
|
|
span('Fallback'),
|
|
span('Outside'),
|
|
]);
|
|
|
|
// Resolving the suspended resource should
|
|
await act(async () => {
|
|
await resolveText('Async');
|
|
});
|
|
expect(Scheduler).toHaveYielded([
|
|
'Text:Inside:Before render',
|
|
'AsyncText:Async render',
|
|
'ClassText:Inside:After render',
|
|
'Text:Fallback destroy layout',
|
|
'Text:Inside:Before create layout',
|
|
'AsyncText:Async create layout',
|
|
'ClassText:Inside:After componentDidMount',
|
|
'Text:Fallback destroy passive',
|
|
'Text:Inside:Before create passive',
|
|
'AsyncText:Async create passive',
|
|
]);
|
|
expect(ReactNoop.getChildren()).toEqual([
|
|
span('Inside:Before'),
|
|
span('Async'),
|
|
span('Inside:After'),
|
|
span('Outside'),
|
|
]);
|
|
|
|
await act(async () => {
|
|
ReactNoop.render(null);
|
|
});
|
|
expect(Scheduler).toHaveYielded([
|
|
'App destroy layout',
|
|
'Text:Inside:Before destroy layout',
|
|
'AsyncText:Async destroy layout',
|
|
'ClassText:Inside:After componentWillUnmount',
|
|
'Text:Outside destroy layout',
|
|
'App destroy passive',
|
|
'Text:Inside:Before destroy passive',
|
|
'AsyncText:Async destroy passive',
|
|
'Text:Outside destroy passive',
|
|
]);
|
|
expect(ReactNoop.getChildren()).toEqual([]);
|
|
});
|
|
|
|
// @gate enableLegacyCache
|
|
it('should not change behavior in sync', async () => {
|
|
class ClassText extends React.Component {
|
|
componentDidMount() {
|
|
const {text} = this.props;
|
|
Scheduler.unstable_yieldValue(`ClassText:${text} componentDidMount`);
|
|
}
|
|
componentDidUpdate() {
|
|
const {text} = this.props;
|
|
Scheduler.unstable_yieldValue(`ClassText:${text} componentDidUpdate`);
|
|
}
|
|
componentWillUnmount() {
|
|
const {text} = this.props;
|
|
Scheduler.unstable_yieldValue(
|
|
`ClassText:${text} componentWillUnmount`,
|
|
);
|
|
}
|
|
render() {
|
|
const {children, text} = this.props;
|
|
Scheduler.unstable_yieldValue(`ClassText:${text} render`);
|
|
return <span prop={text}>{children}</span>;
|
|
}
|
|
}
|
|
|
|
function App({children = null}) {
|
|
Scheduler.unstable_yieldValue('App render');
|
|
React.useLayoutEffect(() => {
|
|
Scheduler.unstable_yieldValue('App create layout');
|
|
return () => {
|
|
Scheduler.unstable_yieldValue('App destroy layout');
|
|
};
|
|
}, []);
|
|
React.useEffect(() => {
|
|
Scheduler.unstable_yieldValue('App create passive');
|
|
return () => {
|
|
Scheduler.unstable_yieldValue('App destroy passive');
|
|
};
|
|
}, []);
|
|
return (
|
|
<>
|
|
<Suspense fallback={<Text text="Fallback" />}>
|
|
<Text text="Inside:Before" />
|
|
{children}
|
|
<ClassText text="Inside:After" />
|
|
</Suspense>
|
|
<Text text="Outside" />
|
|
</>
|
|
);
|
|
}
|
|
|
|
// Mount and suspend.
|
|
act(() => {
|
|
ReactNoop.renderLegacySyncRoot(
|
|
<App>
|
|
<AsyncText text="Async" ms={1000} />
|
|
</App>,
|
|
);
|
|
});
|
|
expect(Scheduler).toHaveYielded([
|
|
'App render',
|
|
'Text:Inside:Before render',
|
|
'Suspend:Async',
|
|
'ClassText:Inside:After render',
|
|
'Text:Fallback render',
|
|
'Text:Outside render',
|
|
'Text:Inside:Before create layout',
|
|
'ClassText:Inside:After componentDidMount',
|
|
'Text:Fallback create layout',
|
|
'Text:Outside create layout',
|
|
'App create layout',
|
|
'Text:Inside:Before create passive',
|
|
'Text:Fallback create passive',
|
|
'Text:Outside create passive',
|
|
'App create passive',
|
|
]);
|
|
expect(ReactNoop.getChildren()).toEqual([
|
|
spanHidden('Inside:Before'),
|
|
spanHidden('Inside:After'),
|
|
span('Fallback'),
|
|
span('Outside'),
|
|
]);
|
|
|
|
// Resolving the suspended resource should
|
|
await act(async () => {
|
|
await resolveText('Async');
|
|
});
|
|
expect(Scheduler).toHaveYielded([
|
|
'AsyncText:Async render',
|
|
'Text:Fallback destroy layout',
|
|
'AsyncText:Async create layout',
|
|
'Text:Fallback destroy passive',
|
|
'AsyncText:Async create passive',
|
|
]);
|
|
expect(ReactNoop.getChildren()).toEqual([
|
|
span('Inside:Before'),
|
|
span('Async'),
|
|
span('Inside:After'),
|
|
span('Outside'),
|
|
]);
|
|
|
|
await act(async () => {
|
|
ReactNoop.renderLegacySyncRoot(null);
|
|
});
|
|
expect(Scheduler).toHaveYielded([
|
|
'App destroy layout',
|
|
'Text:Inside:Before destroy layout',
|
|
'AsyncText:Async destroy layout',
|
|
'ClassText:Inside:After componentWillUnmount',
|
|
'Text:Outside destroy layout',
|
|
'App destroy passive',
|
|
'Text:Inside:Before destroy passive',
|
|
'AsyncText:Async destroy passive',
|
|
'Text:Outside destroy passive',
|
|
]);
|
|
expect(ReactNoop.getChildren()).toEqual([]);
|
|
});
|
|
});
|
|
|
|
describe('layout effects within a tree that re-suspends in an update', () => {
|
|
// @gate enableLegacyCache
|
|
it('should not be destroyed or recreated in legacy roots', async () => {
|
|
function App({children = null}) {
|
|
Scheduler.unstable_yieldValue('App render');
|
|
React.useLayoutEffect(() => {
|
|
Scheduler.unstable_yieldValue('App create layout');
|
|
return () => {
|
|
Scheduler.unstable_yieldValue('App destroy layout');
|
|
};
|
|
}, []);
|
|
React.useEffect(() => {
|
|
Scheduler.unstable_yieldValue('App create passive');
|
|
return () => {
|
|
Scheduler.unstable_yieldValue('App destroy passive');
|
|
};
|
|
}, []);
|
|
return (
|
|
<>
|
|
<Suspense fallback={<Text text="Fallback" />}>
|
|
<Text text="Inside:Before" />
|
|
{children}
|
|
<Text text="Inside:After" />
|
|
</Suspense>
|
|
<Text text="Outside" />
|
|
</>
|
|
);
|
|
}
|
|
|
|
// Mount
|
|
act(() => {
|
|
ReactNoop.renderLegacySyncRoot(<App />);
|
|
});
|
|
expect(Scheduler).toHaveYielded([
|
|
'App render',
|
|
'Text:Inside:Before render',
|
|
'Text:Inside:After render',
|
|
'Text:Outside render',
|
|
'Text:Inside:Before create layout',
|
|
'Text:Inside:After create layout',
|
|
'Text:Outside create layout',
|
|
'App create layout',
|
|
'Text:Inside:Before create passive',
|
|
'Text:Inside:After create passive',
|
|
'Text:Outside create passive',
|
|
'App create passive',
|
|
]);
|
|
expect(ReactNoop.getChildren()).toEqual([
|
|
span('Inside:Before'),
|
|
span('Inside:After'),
|
|
span('Outside'),
|
|
]);
|
|
|
|
// Schedule an update that causes React to suspend.
|
|
act(() => {
|
|
ReactNoop.renderLegacySyncRoot(
|
|
<App>
|
|
<AsyncText text="Async" ms={1000} />
|
|
</App>,
|
|
);
|
|
});
|
|
expect(Scheduler).toHaveYielded([
|
|
'App render',
|
|
'Text:Inside:Before render',
|
|
'Suspend:Async',
|
|
'Text:Inside:After render',
|
|
'Text:Fallback render',
|
|
'Text:Outside render',
|
|
'Text:Fallback create layout',
|
|
'Text:Fallback create passive',
|
|
]);
|
|
expect(ReactNoop.getChildren()).toEqual([
|
|
spanHidden('Inside:Before'),
|
|
spanHidden('Inside:After'),
|
|
span('Fallback'),
|
|
span('Outside'),
|
|
]);
|
|
|
|
await advanceTimers(1000);
|
|
|
|
// Noop since sync root has already committed
|
|
expect(Scheduler).toHaveYielded([]);
|
|
expect(ReactNoop.getChildren()).toEqual([
|
|
spanHidden('Inside:Before'),
|
|
spanHidden('Inside:After'),
|
|
span('Fallback'),
|
|
span('Outside'),
|
|
]);
|
|
|
|
// Resolving the suspended resource should re-create inner layout effects.
|
|
await act(async () => {
|
|
await resolveText('Async');
|
|
});
|
|
expect(Scheduler).toHaveYielded([
|
|
'AsyncText:Async render',
|
|
'Text:Fallback destroy layout',
|
|
'AsyncText:Async create layout',
|
|
'Text:Fallback destroy passive',
|
|
'AsyncText:Async create passive',
|
|
]);
|
|
expect(ReactNoop.getChildren()).toEqual([
|
|
span('Inside:Before'),
|
|
span('Async'),
|
|
span('Inside:After'),
|
|
span('Outside'),
|
|
]);
|
|
|
|
await act(async () => {
|
|
ReactNoop.renderLegacySyncRoot(null);
|
|
});
|
|
expect(Scheduler).toHaveYielded([
|
|
'App destroy layout',
|
|
'Text:Inside:Before destroy layout',
|
|
'AsyncText:Async destroy layout',
|
|
'Text:Inside:After destroy layout',
|
|
'Text:Outside destroy layout',
|
|
'App destroy passive',
|
|
'Text:Inside:Before destroy passive',
|
|
'AsyncText:Async destroy passive',
|
|
'Text:Inside:After destroy passive',
|
|
'Text:Outside destroy passive',
|
|
]);
|
|
});
|
|
|
|
// @gate enableLegacyCache
|
|
it('should be destroyed and recreated for function components', async () => {
|
|
function App({children = null}) {
|
|
Scheduler.unstable_yieldValue('App render');
|
|
React.useLayoutEffect(() => {
|
|
Scheduler.unstable_yieldValue('App create layout');
|
|
return () => {
|
|
Scheduler.unstable_yieldValue('App destroy layout');
|
|
};
|
|
}, []);
|
|
React.useEffect(() => {
|
|
Scheduler.unstable_yieldValue('App create passive');
|
|
return () => {
|
|
Scheduler.unstable_yieldValue('App destroy passive');
|
|
};
|
|
}, []);
|
|
return (
|
|
<>
|
|
<Suspense fallback={<Text text="Fallback" />}>
|
|
<Text text="Inside:Before" />
|
|
{children}
|
|
<Text text="Inside:After" />
|
|
</Suspense>
|
|
<Text text="Outside" />
|
|
</>
|
|
);
|
|
}
|
|
|
|
await act(async () => {
|
|
ReactNoop.render(<App />);
|
|
});
|
|
expect(Scheduler).toHaveYielded([
|
|
'App render',
|
|
'Text:Inside:Before render',
|
|
'Text:Inside:After render',
|
|
'Text:Outside render',
|
|
'Text:Inside:Before create layout',
|
|
'Text:Inside:After create layout',
|
|
'Text:Outside create layout',
|
|
'App create layout',
|
|
'Text:Inside:Before create passive',
|
|
'Text:Inside:After create passive',
|
|
'Text:Outside create passive',
|
|
'App create passive',
|
|
]);
|
|
expect(ReactNoop.getChildren()).toEqual([
|
|
span('Inside:Before'),
|
|
span('Inside:After'),
|
|
span('Outside'),
|
|
]);
|
|
|
|
// Schedule an update that causes React to suspend.
|
|
act(() => {
|
|
ReactNoop.render(
|
|
<App>
|
|
<AsyncText text="Async" ms={1000} />
|
|
</App>,
|
|
);
|
|
});
|
|
expect(Scheduler).toHaveYielded([
|
|
'App render',
|
|
'Text:Inside:Before render',
|
|
'Suspend:Async',
|
|
'Text:Inside:After render',
|
|
'Text:Fallback render',
|
|
'Text:Outside render',
|
|
]);
|
|
expect(ReactNoop.getChildren()).toEqual([
|
|
span('Inside:Before'),
|
|
span('Inside:After'),
|
|
span('Outside'),
|
|
]);
|
|
|
|
await advanceTimers(1000);
|
|
|
|
// Timing out should commit the fallback and destroy inner layout effects.
|
|
expect(Scheduler).toHaveYielded([
|
|
'Text:Inside:Before destroy layout',
|
|
'Text:Inside:After destroy layout',
|
|
'Text:Fallback create layout',
|
|
]);
|
|
expect(Scheduler).toFlushAndYield(['Text:Fallback create passive']);
|
|
expect(ReactNoop.getChildren()).toEqual([
|
|
spanHidden('Inside:Before'),
|
|
spanHidden('Inside:After'),
|
|
span('Fallback'),
|
|
span('Outside'),
|
|
]);
|
|
|
|
// Resolving the suspended resource should re-create inner layout effects.
|
|
await act(async () => {
|
|
await resolveText('Async');
|
|
});
|
|
expect(Scheduler).toHaveYielded([
|
|
'Text:Inside:Before render',
|
|
'AsyncText:Async render',
|
|
'Text:Inside:After render',
|
|
'Text:Fallback destroy layout',
|
|
'Text:Inside:Before create layout',
|
|
'AsyncText:Async create layout',
|
|
'Text:Inside:After create layout',
|
|
'Text:Fallback destroy passive',
|
|
'AsyncText:Async create passive',
|
|
]);
|
|
expect(ReactNoop.getChildren()).toEqual([
|
|
span('Inside:Before'),
|
|
span('Async'),
|
|
span('Inside:After'),
|
|
span('Outside'),
|
|
]);
|
|
|
|
await act(async () => {
|
|
ReactNoop.render(null);
|
|
});
|
|
expect(Scheduler).toHaveYielded([
|
|
'App destroy layout',
|
|
'Text:Inside:Before destroy layout',
|
|
'AsyncText:Async destroy layout',
|
|
'Text:Inside:After destroy layout',
|
|
'Text:Outside destroy layout',
|
|
'App destroy passive',
|
|
'Text:Inside:Before destroy passive',
|
|
'AsyncText:Async destroy passive',
|
|
'Text:Inside:After destroy passive',
|
|
'Text:Outside destroy passive',
|
|
]);
|
|
});
|
|
|
|
// @gate enableLegacyCache
|
|
it('should be destroyed and recreated for class components', async () => {
|
|
class ClassText extends React.Component {
|
|
componentDidMount() {
|
|
const {text} = this.props;
|
|
Scheduler.unstable_yieldValue(`ClassText:${text} componentDidMount`);
|
|
}
|
|
componentDidUpdate() {
|
|
const {text} = this.props;
|
|
Scheduler.unstable_yieldValue(`ClassText:${text} componentDidUpdate`);
|
|
}
|
|
componentWillUnmount() {
|
|
const {text} = this.props;
|
|
Scheduler.unstable_yieldValue(
|
|
`ClassText:${text} componentWillUnmount`,
|
|
);
|
|
}
|
|
render() {
|
|
const {children, text} = this.props;
|
|
Scheduler.unstable_yieldValue(`ClassText:${text} render`);
|
|
return <span prop={text}>{children}</span>;
|
|
}
|
|
}
|
|
|
|
function App({children = null}) {
|
|
Scheduler.unstable_yieldValue('App render');
|
|
React.useLayoutEffect(() => {
|
|
Scheduler.unstable_yieldValue('App create layout');
|
|
return () => {
|
|
Scheduler.unstable_yieldValue('App destroy layout');
|
|
};
|
|
}, []);
|
|
React.useEffect(() => {
|
|
Scheduler.unstable_yieldValue('App create passive');
|
|
return () => {
|
|
Scheduler.unstable_yieldValue('App destroy passive');
|
|
};
|
|
}, []);
|
|
return (
|
|
<>
|
|
<Suspense fallback={<ClassText text="Fallback" />}>
|
|
<ClassText text="Inside:Before" />
|
|
{children}
|
|
<ClassText text="Inside:After" />
|
|
</Suspense>
|
|
<ClassText text="Outside" />
|
|
</>
|
|
);
|
|
}
|
|
|
|
// Mount
|
|
await act(async () => {
|
|
ReactNoop.render(<App />);
|
|
});
|
|
expect(Scheduler).toHaveYielded([
|
|
'App render',
|
|
'ClassText:Inside:Before render',
|
|
'ClassText:Inside:After render',
|
|
'ClassText:Outside render',
|
|
'ClassText:Inside:Before componentDidMount',
|
|
'ClassText:Inside:After componentDidMount',
|
|
'ClassText:Outside componentDidMount',
|
|
'App create layout',
|
|
'App create passive',
|
|
]);
|
|
expect(ReactNoop.getChildren()).toEqual([
|
|
span('Inside:Before'),
|
|
span('Inside:After'),
|
|
span('Outside'),
|
|
]);
|
|
|
|
// Schedule an update that causes React to suspend.
|
|
act(() => {
|
|
ReactNoop.render(
|
|
<App>
|
|
<AsyncText text="Async" ms={1000} />
|
|
</App>,
|
|
);
|
|
});
|
|
expect(Scheduler).toHaveYielded([
|
|
'App render',
|
|
'ClassText:Inside:Before render',
|
|
'Suspend:Async',
|
|
'ClassText:Inside:After render',
|
|
'ClassText:Fallback render',
|
|
'ClassText:Outside render',
|
|
]);
|
|
expect(ReactNoop.getChildren()).toEqual([
|
|
span('Inside:Before'),
|
|
span('Inside:After'),
|
|
span('Outside'),
|
|
]);
|
|
|
|
await advanceTimers(1000);
|
|
|
|
// Timing out should commit the fallback and destroy inner layout effects.
|
|
expect(Scheduler).toHaveYielded([
|
|
'ClassText:Inside:Before componentWillUnmount',
|
|
'ClassText:Inside:After componentWillUnmount',
|
|
'ClassText:Fallback componentDidMount',
|
|
'ClassText:Outside componentDidUpdate',
|
|
]);
|
|
expect(ReactNoop.getChildren()).toEqual([
|
|
spanHidden('Inside:Before'),
|
|
spanHidden('Inside:After'),
|
|
span('Fallback'),
|
|
span('Outside'),
|
|
]);
|
|
|
|
// Resolving the suspended resource should re-create inner layout effects.
|
|
await act(async () => {
|
|
await resolveText('Async');
|
|
});
|
|
expect(Scheduler).toHaveYielded([
|
|
'ClassText:Inside:Before render',
|
|
'AsyncText:Async render',
|
|
'ClassText:Inside:After render',
|
|
'ClassText:Fallback componentWillUnmount',
|
|
'ClassText:Inside:Before componentDidMount',
|
|
'AsyncText:Async create layout',
|
|
'ClassText:Inside:After componentDidMount',
|
|
'AsyncText:Async create passive',
|
|
]);
|
|
expect(ReactNoop.getChildren()).toEqual([
|
|
span('Inside:Before'),
|
|
span('Async'),
|
|
span('Inside:After'),
|
|
span('Outside'),
|
|
]);
|
|
|
|
await act(async () => {
|
|
ReactNoop.render(null);
|
|
});
|
|
expect(Scheduler).toHaveYielded([
|
|
'App destroy layout',
|
|
'ClassText:Inside:Before componentWillUnmount',
|
|
'AsyncText:Async destroy layout',
|
|
'ClassText:Inside:After componentWillUnmount',
|
|
'ClassText:Outside componentWillUnmount',
|
|
'App destroy passive',
|
|
'AsyncText:Async destroy passive',
|
|
]);
|
|
});
|
|
|
|
// @gate enableLegacyCache
|
|
it('should be destroyed and recreated when nested below host components', async () => {
|
|
function App({children = null}) {
|
|
Scheduler.unstable_yieldValue('App render');
|
|
React.useLayoutEffect(() => {
|
|
Scheduler.unstable_yieldValue('App create layout');
|
|
return () => {
|
|
Scheduler.unstable_yieldValue('App destroy layout');
|
|
};
|
|
}, []);
|
|
React.useEffect(() => {
|
|
Scheduler.unstable_yieldValue('App create passive');
|
|
return () => {
|
|
Scheduler.unstable_yieldValue('App destroy passive');
|
|
};
|
|
}, []);
|
|
return (
|
|
<Suspense fallback={<Text text="Fallback" />}>
|
|
{children}
|
|
<Text text="Outer">
|
|
<Text text="Inner" />
|
|
</Text>
|
|
</Suspense>
|
|
);
|
|
}
|
|
|
|
// Mount
|
|
await act(async () => {
|
|
ReactNoop.render(<App />);
|
|
});
|
|
expect(Scheduler).toHaveYielded([
|
|
'App render',
|
|
'Text:Outer render',
|
|
'Text:Inner render',
|
|
'Text:Inner create layout',
|
|
'Text:Outer create layout',
|
|
'App create layout',
|
|
'Text:Inner create passive',
|
|
'Text:Outer create passive',
|
|
'App create passive',
|
|
]);
|
|
expect(ReactNoop.getChildren()).toEqual([span('Outer', [span('Inner')])]);
|
|
|
|
// Schedule an update that causes React to suspend.
|
|
act(() => {
|
|
ReactNoop.render(
|
|
<App>
|
|
<AsyncText text="Async" ms={1000} />
|
|
</App>,
|
|
);
|
|
});
|
|
expect(Scheduler).toHaveYielded([
|
|
'App render',
|
|
'Suspend:Async',
|
|
'Text:Outer render',
|
|
'Text:Inner render',
|
|
'Text:Fallback render',
|
|
]);
|
|
expect(ReactNoop.getChildren()).toEqual([span('Outer', [span('Inner')])]);
|
|
|
|
await advanceTimers(1000);
|
|
|
|
// Timing out should commit the fallback and destroy inner layout effects.
|
|
expect(Scheduler).toHaveYielded([
|
|
'Text:Outer destroy layout',
|
|
'Text:Inner destroy layout',
|
|
'Text:Fallback create layout',
|
|
]);
|
|
expect(Scheduler).toFlushAndYield(['Text:Fallback create passive']);
|
|
expect(ReactNoop.getChildren()).toEqual([
|
|
spanHidden('Outer', [span('Inner')]),
|
|
span('Fallback'),
|
|
]);
|
|
|
|
// Resolving the suspended resource should re-create inner layout effects.
|
|
await act(async () => {
|
|
await resolveText('Async');
|
|
});
|
|
expect(Scheduler).toHaveYielded([
|
|
'AsyncText:Async render',
|
|
'Text:Outer render',
|
|
'Text:Inner render',
|
|
'Text:Fallback destroy layout',
|
|
'AsyncText:Async create layout',
|
|
'Text:Inner create layout',
|
|
'Text:Outer create layout',
|
|
'Text:Fallback destroy passive',
|
|
'AsyncText:Async create passive',
|
|
]);
|
|
expect(ReactNoop.getChildren()).toEqual([
|
|
span('Async'),
|
|
span('Outer', [span('Inner')]),
|
|
]);
|
|
|
|
await act(async () => {
|
|
ReactNoop.render(null);
|
|
});
|
|
expect(Scheduler).toHaveYielded([
|
|
'App destroy layout',
|
|
'AsyncText:Async destroy layout',
|
|
'Text:Outer destroy layout',
|
|
'Text:Inner destroy layout',
|
|
'App destroy passive',
|
|
'AsyncText:Async destroy passive',
|
|
'Text:Outer destroy passive',
|
|
'Text:Inner destroy passive',
|
|
]);
|
|
});
|
|
|
|
// @gate enableLegacyCache
|
|
it('should be destroyed and recreated even if there is a bailout because of memoization', async () => {
|
|
const MemoizedText = React.memo(Text, () => true);
|
|
|
|
function App({children = null}) {
|
|
Scheduler.unstable_yieldValue('App render');
|
|
React.useLayoutEffect(() => {
|
|
Scheduler.unstable_yieldValue('App create layout');
|
|
return () => {
|
|
Scheduler.unstable_yieldValue('App destroy layout');
|
|
};
|
|
}, []);
|
|
React.useEffect(() => {
|
|
Scheduler.unstable_yieldValue('App create passive');
|
|
return () => {
|
|
Scheduler.unstable_yieldValue('App destroy passive');
|
|
};
|
|
}, []);
|
|
return (
|
|
<Suspense fallback={<Text text="Fallback" />}>
|
|
{children}
|
|
<Text text="Outer">
|
|
<MemoizedText text="MemoizedInner" />
|
|
</Text>
|
|
</Suspense>
|
|
);
|
|
}
|
|
|
|
// Mount
|
|
await act(async () => {
|
|
ReactNoop.render(<App />);
|
|
});
|
|
expect(Scheduler).toHaveYielded([
|
|
'App render',
|
|
'Text:Outer render',
|
|
'Text:MemoizedInner render',
|
|
'Text:MemoizedInner create layout',
|
|
'Text:Outer create layout',
|
|
'App create layout',
|
|
'Text:MemoizedInner create passive',
|
|
'Text:Outer create passive',
|
|
'App create passive',
|
|
]);
|
|
expect(ReactNoop.getChildren()).toEqual([
|
|
span('Outer', [span('MemoizedInner')]),
|
|
]);
|
|
|
|
// Schedule an update that causes React to suspend.
|
|
act(() => {
|
|
ReactNoop.render(
|
|
<App>
|
|
<AsyncText text="Async" ms={1000} />
|
|
</App>,
|
|
);
|
|
});
|
|
expect(Scheduler).toHaveYielded([
|
|
'App render',
|
|
'Suspend:Async',
|
|
'Text:Outer render',
|
|
// Text:MemoizedInner is memoized
|
|
'Text:Fallback render',
|
|
]);
|
|
expect(ReactNoop.getChildren()).toEqual([
|
|
span('Outer', [span('MemoizedInner')]),
|
|
]);
|
|
|
|
await advanceTimers(1000);
|
|
|
|
// Timing out should commit the fallback and destroy inner layout effects.
|
|
// Even though the innermost layout effects are beneath a hidden HostComponent.
|
|
expect(Scheduler).toHaveYielded([
|
|
'Text:Outer destroy layout',
|
|
'Text:MemoizedInner destroy layout',
|
|
'Text:Fallback create layout',
|
|
]);
|
|
expect(Scheduler).toFlushAndYield(['Text:Fallback create passive']);
|
|
expect(ReactNoop.getChildren()).toEqual([
|
|
spanHidden('Outer', [span('MemoizedInner')]),
|
|
span('Fallback'),
|
|
]);
|
|
|
|
// Resolving the suspended resource should re-create inner layout effects.
|
|
await act(async () => {
|
|
await resolveText('Async');
|
|
});
|
|
expect(Scheduler).toHaveYielded([
|
|
'AsyncText:Async render',
|
|
'Text:Outer render',
|
|
'Text:Fallback destroy layout',
|
|
'AsyncText:Async create layout',
|
|
'Text:MemoizedInner create layout',
|
|
'Text:Outer create layout',
|
|
'Text:Fallback destroy passive',
|
|
'AsyncText:Async create passive',
|
|
]);
|
|
expect(ReactNoop.getChildren()).toEqual([
|
|
span('Async'),
|
|
span('Outer', [span('MemoizedInner')]),
|
|
]);
|
|
|
|
await act(async () => {
|
|
ReactNoop.render(null);
|
|
});
|
|
expect(Scheduler).toHaveYielded([
|
|
'App destroy layout',
|
|
'AsyncText:Async destroy layout',
|
|
'Text:Outer destroy layout',
|
|
'Text:MemoizedInner destroy layout',
|
|
'App destroy passive',
|
|
'AsyncText:Async destroy passive',
|
|
'Text:Outer destroy passive',
|
|
'Text:MemoizedInner destroy passive',
|
|
]);
|
|
});
|
|
|
|
// @gate enableLegacyCache
|
|
it('should respect nested suspense boundaries', async () => {
|
|
function App({innerChildren = null, outerChildren = null}) {
|
|
return (
|
|
<Suspense fallback={<Text text="OuterFallback" />}>
|
|
<Text text="Outer" />
|
|
{outerChildren}
|
|
<Suspense fallback={<Text text="InnerFallback" />}>
|
|
<Text text="Inner" />
|
|
{innerChildren}
|
|
</Suspense>
|
|
</Suspense>
|
|
);
|
|
}
|
|
|
|
// Mount
|
|
await act(async () => {
|
|
ReactNoop.render(<App />);
|
|
});
|
|
expect(Scheduler).toHaveYielded([
|
|
'Text:Outer render',
|
|
'Text:Inner render',
|
|
'Text:Outer create layout',
|
|
'Text:Inner create layout',
|
|
'Text:Outer create passive',
|
|
'Text:Inner create passive',
|
|
]);
|
|
expect(ReactNoop.getChildren()).toEqual([span('Outer'), span('Inner')]);
|
|
|
|
// Suspend the inner Suspense subtree (only inner effects should be destroyed)
|
|
act(() => {
|
|
ReactNoop.render(
|
|
<App innerChildren={<AsyncText text="InnerAsync_1" ms={1000} />} />,
|
|
);
|
|
});
|
|
await advanceTimers(1000);
|
|
expect(Scheduler).toHaveYielded([
|
|
'Text:Outer render',
|
|
'Text:Inner render',
|
|
'Suspend:InnerAsync_1',
|
|
'Text:InnerFallback render',
|
|
'Text:Inner destroy layout',
|
|
'Text:InnerFallback create layout',
|
|
]);
|
|
expect(Scheduler).toFlushAndYield(['Text:InnerFallback create passive']);
|
|
expect(ReactNoop.getChildren()).toEqual([
|
|
span('Outer'),
|
|
spanHidden('Inner'),
|
|
span('InnerFallback'),
|
|
]);
|
|
|
|
// Suspend the outer Suspense subtree (outer effects and inner fallback effects should be destroyed)
|
|
// (This check also ensures we don't destroy effects for mounted inner fallback.)
|
|
act(() => {
|
|
ReactNoop.render(
|
|
<App
|
|
outerChildren={<AsyncText text="OuterAsync_1" ms={1000} />}
|
|
innerChildren={<AsyncText text="InnerAsync_1" ms={1000} />}
|
|
/>,
|
|
);
|
|
});
|
|
await advanceTimers(1000);
|
|
expect(Scheduler).toHaveYielded([
|
|
'Text:Outer render',
|
|
'Suspend:OuterAsync_1',
|
|
'Text:Inner render',
|
|
'Suspend:InnerAsync_1',
|
|
'Text:InnerFallback render',
|
|
'Text:OuterFallback render',
|
|
'Text:Outer destroy layout',
|
|
'Text:InnerFallback destroy layout',
|
|
'Text:OuterFallback create layout',
|
|
]);
|
|
expect(Scheduler).toFlushAndYield(['Text:OuterFallback create passive']);
|
|
expect(ReactNoop.getChildren()).toEqual([
|
|
spanHidden('Outer'),
|
|
spanHidden('Inner'),
|
|
spanHidden('InnerFallback'),
|
|
span('OuterFallback'),
|
|
]);
|
|
|
|
// Show the inner Suspense subtree (no effects should be recreated)
|
|
await act(async () => {
|
|
await resolveText('InnerAsync_1');
|
|
});
|
|
expect(Scheduler).toHaveYielded([
|
|
'Text:Outer render',
|
|
'Suspend:OuterAsync_1',
|
|
'Text:Inner render',
|
|
'AsyncText:InnerAsync_1 render',
|
|
]);
|
|
expect(ReactNoop.getChildren()).toEqual([
|
|
spanHidden('Outer'),
|
|
spanHidden('Inner'),
|
|
spanHidden('InnerFallback'),
|
|
span('OuterFallback'),
|
|
]);
|
|
|
|
// Suspend the inner Suspense subtree (no effects should be destroyed)
|
|
act(() => {
|
|
ReactNoop.render(
|
|
<App
|
|
outerChildren={<AsyncText text="OuterAsync_1" ms={1000} />}
|
|
innerChildren={<AsyncText text="InnerAsync_2" ms={1000} />}
|
|
/>,
|
|
);
|
|
});
|
|
await advanceTimers(1000);
|
|
expect(Scheduler).toHaveYielded([
|
|
'Text:Outer render',
|
|
'Suspend:OuterAsync_1',
|
|
'Text:Inner render',
|
|
'Suspend:InnerAsync_2',
|
|
'Text:InnerFallback render',
|
|
'Text:OuterFallback render',
|
|
]);
|
|
expect(ReactNoop.getChildren()).toEqual([
|
|
spanHidden('Outer'),
|
|
spanHidden('Inner'),
|
|
spanHidden('InnerFallback'),
|
|
span('OuterFallback'),
|
|
]);
|
|
|
|
// Show the outer Suspense subtree (only outer effects should be recreated)
|
|
await act(async () => {
|
|
await resolveText('OuterAsync_1');
|
|
});
|
|
expect(Scheduler).toHaveYielded([
|
|
'Text:Outer render',
|
|
'AsyncText:OuterAsync_1 render',
|
|
'Text:Inner render',
|
|
'Suspend:InnerAsync_2',
|
|
'Text:InnerFallback render',
|
|
'Text:OuterFallback destroy layout',
|
|
'Text:Outer create layout',
|
|
'AsyncText:OuterAsync_1 create layout',
|
|
'Text:InnerFallback create layout',
|
|
'Text:OuterFallback destroy passive',
|
|
'AsyncText:OuterAsync_1 create passive',
|
|
]);
|
|
expect(ReactNoop.getChildren()).toEqual([
|
|
span('Outer'),
|
|
span('OuterAsync_1'),
|
|
spanHidden('Inner'),
|
|
span('InnerFallback'),
|
|
]);
|
|
|
|
// Show the inner Suspense subtree (only inner effects should be recreated)
|
|
await act(async () => {
|
|
await resolveText('InnerAsync_2');
|
|
});
|
|
expect(Scheduler).toHaveYielded([
|
|
'Text:Inner render',
|
|
'AsyncText:InnerAsync_2 render',
|
|
'Text:InnerFallback destroy layout',
|
|
'Text:Inner create layout',
|
|
'AsyncText:InnerAsync_2 create layout',
|
|
'Text:InnerFallback destroy passive',
|
|
'AsyncText:InnerAsync_2 create passive',
|
|
]);
|
|
expect(ReactNoop.getChildren()).toEqual([
|
|
span('Outer'),
|
|
span('OuterAsync_1'),
|
|
span('Inner'),
|
|
span('InnerAsync_2'),
|
|
]);
|
|
|
|
// Suspend the outer Suspense subtree (all effects should be destroyed)
|
|
act(() => {
|
|
ReactNoop.render(
|
|
<App
|
|
outerChildren={<AsyncText text="OuterAsync_2" ms={1000} />}
|
|
innerChildren={<AsyncText text="InnerAsync_2" ms={1000} />}
|
|
/>,
|
|
);
|
|
});
|
|
await advanceTimers(1000);
|
|
expect(Scheduler).toHaveYielded([
|
|
'Text:Outer render',
|
|
'Suspend:OuterAsync_2',
|
|
'Text:Inner render',
|
|
'AsyncText:InnerAsync_2 render',
|
|
'Text:OuterFallback render',
|
|
'Text:Outer destroy layout',
|
|
'AsyncText:OuterAsync_1 destroy layout',
|
|
'Text:Inner destroy layout',
|
|
'AsyncText:InnerAsync_2 destroy layout',
|
|
'Text:OuterFallback create layout',
|
|
]);
|
|
expect(ReactNoop.getChildren()).toEqual([
|
|
spanHidden('Outer'),
|
|
spanHidden('OuterAsync_1'),
|
|
spanHidden('Inner'),
|
|
spanHidden('InnerAsync_2'),
|
|
span('OuterFallback'),
|
|
]);
|
|
|
|
// Show the outer Suspense subtree (all effects should be recreated)
|
|
await act(async () => {
|
|
await resolveText('OuterAsync_2');
|
|
});
|
|
expect(Scheduler).toHaveYielded([
|
|
'Text:OuterFallback create passive',
|
|
'Text:Outer render',
|
|
'AsyncText:OuterAsync_2 render',
|
|
'Text:Inner render',
|
|
'AsyncText:InnerAsync_2 render',
|
|
'Text:OuterFallback destroy layout',
|
|
'Text:Outer create layout',
|
|
'AsyncText:OuterAsync_2 create layout',
|
|
'Text:Inner create layout',
|
|
'AsyncText:InnerAsync_2 create layout',
|
|
'Text:OuterFallback destroy passive',
|
|
]);
|
|
expect(ReactNoop.getChildren()).toEqual([
|
|
span('Outer'),
|
|
span('OuterAsync_2'),
|
|
span('Inner'),
|
|
span('InnerAsync_2'),
|
|
]);
|
|
});
|
|
|
|
// @gate enableLegacyCache
|
|
it('should show nested host nodes if multiple boundaries resolve at the same time', async () => {
|
|
function App({innerChildren = null, outerChildren = null}) {
|
|
return (
|
|
<Suspense fallback={<Text text="OuterFallback" />}>
|
|
<Text text="Outer" />
|
|
{outerChildren}
|
|
<Suspense fallback={<Text text="InnerFallback" />}>
|
|
<Text text="Inner" />
|
|
{innerChildren}
|
|
</Suspense>
|
|
</Suspense>
|
|
);
|
|
}
|
|
|
|
// Mount
|
|
await act(async () => {
|
|
ReactNoop.render(<App />);
|
|
});
|
|
expect(Scheduler).toHaveYielded([
|
|
'Text:Outer render',
|
|
'Text:Inner render',
|
|
'Text:Outer create layout',
|
|
'Text:Inner create layout',
|
|
'Text:Outer create passive',
|
|
'Text:Inner create passive',
|
|
]);
|
|
expect(ReactNoop.getChildren()).toEqual([span('Outer'), span('Inner')]);
|
|
|
|
// Suspend the inner Suspense subtree (only inner effects should be destroyed)
|
|
act(() => {
|
|
ReactNoop.render(
|
|
<App innerChildren={<AsyncText text="InnerAsync_1" ms={1000} />} />,
|
|
);
|
|
});
|
|
await advanceTimers(1000);
|
|
expect(Scheduler).toHaveYielded([
|
|
'Text:Outer render',
|
|
'Text:Inner render',
|
|
'Suspend:InnerAsync_1',
|
|
'Text:InnerFallback render',
|
|
'Text:Inner destroy layout',
|
|
'Text:InnerFallback create layout',
|
|
]);
|
|
expect(Scheduler).toFlushAndYield(['Text:InnerFallback create passive']);
|
|
expect(ReactNoop.getChildren()).toEqual([
|
|
span('Outer'),
|
|
spanHidden('Inner'),
|
|
span('InnerFallback'),
|
|
]);
|
|
|
|
// Suspend the outer Suspense subtree (outer effects and inner fallback effects should be destroyed)
|
|
// (This check also ensures we don't destroy effects for mounted inner fallback.)
|
|
act(() => {
|
|
ReactNoop.render(
|
|
<App
|
|
outerChildren={<AsyncText text="OuterAsync_1" ms={1000} />}
|
|
innerChildren={<AsyncText text="InnerAsync_1" ms={1000} />}
|
|
/>,
|
|
);
|
|
});
|
|
await advanceTimers(1000);
|
|
expect(Scheduler).toHaveYielded([
|
|
'Text:Outer render',
|
|
'Suspend:OuterAsync_1',
|
|
'Text:Inner render',
|
|
'Suspend:InnerAsync_1',
|
|
'Text:InnerFallback render',
|
|
'Text:OuterFallback render',
|
|
'Text:Outer destroy layout',
|
|
'Text:InnerFallback destroy layout',
|
|
'Text:OuterFallback create layout',
|
|
]);
|
|
expect(Scheduler).toFlushAndYield(['Text:OuterFallback create passive']);
|
|
expect(ReactNoop.getChildren()).toEqual([
|
|
spanHidden('Outer'),
|
|
spanHidden('Inner'),
|
|
spanHidden('InnerFallback'),
|
|
span('OuterFallback'),
|
|
]);
|
|
|
|
// Resolve both suspended trees.
|
|
await act(async () => {
|
|
await resolveText('OuterAsync_1');
|
|
await resolveText('InnerAsync_1');
|
|
});
|
|
expect(Scheduler).toHaveYielded([
|
|
'Text:Outer render',
|
|
'AsyncText:OuterAsync_1 render',
|
|
'Text:Inner render',
|
|
'AsyncText:InnerAsync_1 render',
|
|
'Text:OuterFallback destroy layout',
|
|
'Text:Outer create layout',
|
|
'AsyncText:OuterAsync_1 create layout',
|
|
'Text:Inner create layout',
|
|
'AsyncText:InnerAsync_1 create layout',
|
|
'Text:OuterFallback destroy passive',
|
|
'Text:InnerFallback destroy passive',
|
|
'AsyncText:OuterAsync_1 create passive',
|
|
'AsyncText:InnerAsync_1 create passive',
|
|
]);
|
|
expect(ReactNoop.getChildren()).toEqual([
|
|
span('Outer'),
|
|
span('OuterAsync_1'),
|
|
span('Inner'),
|
|
span('InnerAsync_1'),
|
|
]);
|
|
});
|
|
|
|
// @gate enableLegacyCache
|
|
it('should be cleaned up inside of a fallback that suspends', async () => {
|
|
function App({fallbackChildren = null, outerChildren = null}) {
|
|
return (
|
|
<>
|
|
<Suspense
|
|
fallback={
|
|
<>
|
|
<Suspense fallback={<Text text="Fallback:Fallback" />}>
|
|
<Text text="Fallback:Inside" />
|
|
{fallbackChildren}
|
|
</Suspense>
|
|
<Text text="Fallback:Outside" />
|
|
</>
|
|
}>
|
|
<Text text="Inside" />
|
|
{outerChildren}
|
|
</Suspense>
|
|
<Text text="Outside" />
|
|
</>
|
|
);
|
|
}
|
|
|
|
// Mount
|
|
await act(async () => {
|
|
ReactNoop.render(<App />);
|
|
});
|
|
expect(Scheduler).toHaveYielded([
|
|
'Text:Inside render',
|
|
'Text:Outside render',
|
|
'Text:Inside create layout',
|
|
'Text:Outside create layout',
|
|
'Text:Inside create passive',
|
|
'Text:Outside create passive',
|
|
]);
|
|
expect(ReactNoop.getChildren()).toEqual([
|
|
span('Inside'),
|
|
span('Outside'),
|
|
]);
|
|
|
|
// Suspend the outer shell
|
|
act(() => {
|
|
ReactNoop.render(
|
|
<App outerChildren={<AsyncText text="OutsideAsync" ms={1000} />} />,
|
|
);
|
|
});
|
|
expect(Scheduler).toHaveYielded([
|
|
'Text:Inside render',
|
|
'Suspend:OutsideAsync',
|
|
'Text:Fallback:Inside render',
|
|
'Text:Fallback:Outside render',
|
|
'Text:Outside render',
|
|
]);
|
|
expect(ReactNoop.getChildren()).toEqual([
|
|
span('Inside'),
|
|
span('Outside'),
|
|
]);
|
|
|
|
// Timing out should commit the fallback and destroy inner layout effects.
|
|
await advanceTimers(1000);
|
|
expect(Scheduler).toHaveYielded([
|
|
'Text:Inside destroy layout',
|
|
'Text:Fallback:Inside create layout',
|
|
'Text:Fallback:Outside create layout',
|
|
]);
|
|
expect(Scheduler).toFlushAndYield([
|
|
'Text:Fallback:Inside create passive',
|
|
'Text:Fallback:Outside create passive',
|
|
]);
|
|
expect(ReactNoop.getChildren()).toEqual([
|
|
spanHidden('Inside'),
|
|
span('Fallback:Inside'),
|
|
span('Fallback:Outside'),
|
|
span('Outside'),
|
|
]);
|
|
|
|
// Suspend the fallback and verify that it's effects get cleaned up as well
|
|
act(() => {
|
|
ReactNoop.render(
|
|
<App
|
|
fallbackChildren={<AsyncText text="FallbackAsync" ms={1000} />}
|
|
outerChildren={<AsyncText text="OutsideAsync" ms={1000} />}
|
|
/>,
|
|
);
|
|
});
|
|
expect(Scheduler).toHaveYielded([
|
|
'Text:Inside render',
|
|
'Suspend:OutsideAsync',
|
|
'Text:Fallback:Inside render',
|
|
'Suspend:FallbackAsync',
|
|
'Text:Fallback:Fallback render',
|
|
'Text:Fallback:Outside render',
|
|
'Text:Outside render',
|
|
]);
|
|
expect(ReactNoop.getChildren()).toEqual([
|
|
spanHidden('Inside'),
|
|
span('Fallback:Inside'),
|
|
span('Fallback:Outside'),
|
|
span('Outside'),
|
|
]);
|
|
|
|
// Timing out should commit the inner fallback and destroy outer fallback layout effects.
|
|
await advanceTimers(1000);
|
|
expect(Scheduler).toHaveYielded([
|
|
'Text:Fallback:Inside destroy layout',
|
|
'Text:Fallback:Fallback create layout',
|
|
]);
|
|
expect(Scheduler).toFlushAndYield([
|
|
'Text:Fallback:Fallback create passive',
|
|
]);
|
|
expect(ReactNoop.getChildren()).toEqual([
|
|
spanHidden('Inside'),
|
|
spanHidden('Fallback:Inside'),
|
|
span('Fallback:Fallback'),
|
|
span('Fallback:Outside'),
|
|
span('Outside'),
|
|
]);
|
|
|
|
// Resolving both resources should cleanup fallback effects and recreate main effects
|
|
await act(async () => {
|
|
await resolveText('FallbackAsync');
|
|
await resolveText('OutsideAsync');
|
|
});
|
|
expect(Scheduler).toHaveYielded([
|
|
'Text:Inside render',
|
|
'AsyncText:OutsideAsync render',
|
|
'Text:Fallback:Fallback destroy layout',
|
|
'Text:Fallback:Outside destroy layout',
|
|
'Text:Inside create layout',
|
|
'AsyncText:OutsideAsync create layout',
|
|
'Text:Fallback:Inside destroy passive',
|
|
'Text:Fallback:Fallback destroy passive',
|
|
'Text:Fallback:Outside destroy passive',
|
|
'AsyncText:OutsideAsync create passive',
|
|
]);
|
|
expect(ReactNoop.getChildren()).toEqual([
|
|
span('Inside'),
|
|
span('OutsideAsync'),
|
|
span('Outside'),
|
|
]);
|
|
});
|
|
|
|
// @gate enableLegacyCache
|
|
it('should be cleaned up inside of a fallback that suspends (alternate)', async () => {
|
|
function App({fallbackChildren = null, outerChildren = null}) {
|
|
return (
|
|
<>
|
|
<Suspense
|
|
fallback={
|
|
<>
|
|
<Suspense fallback={<Text text="Fallback:Fallback" />}>
|
|
<Text text="Fallback:Inside" />
|
|
{fallbackChildren}
|
|
</Suspense>
|
|
<Text text="Fallback:Outside" />
|
|
</>
|
|
}>
|
|
<Text text="Inside" />
|
|
{outerChildren}
|
|
</Suspense>
|
|
<Text text="Outside" />
|
|
</>
|
|
);
|
|
}
|
|
|
|
// Mount
|
|
await act(async () => {
|
|
ReactNoop.render(<App />);
|
|
});
|
|
expect(Scheduler).toHaveYielded([
|
|
'Text:Inside render',
|
|
'Text:Outside render',
|
|
'Text:Inside create layout',
|
|
'Text:Outside create layout',
|
|
'Text:Inside create passive',
|
|
'Text:Outside create passive',
|
|
]);
|
|
expect(ReactNoop.getChildren()).toEqual([
|
|
span('Inside'),
|
|
span('Outside'),
|
|
]);
|
|
|
|
// Suspend both the outer boundary and the fallback
|
|
act(() => {
|
|
ReactNoop.render(
|
|
<App
|
|
outerChildren={<AsyncText text="OutsideAsync" ms={1000} />}
|
|
fallbackChildren={<AsyncText text="FallbackAsync" ms={1000} />}
|
|
/>,
|
|
);
|
|
});
|
|
await advanceTimers(1000);
|
|
expect(Scheduler).toHaveYielded([
|
|
'Text:Inside render',
|
|
'Suspend:OutsideAsync',
|
|
'Text:Fallback:Inside render',
|
|
'Suspend:FallbackAsync',
|
|
'Text:Fallback:Fallback render',
|
|
'Text:Fallback:Outside render',
|
|
'Text:Outside render',
|
|
'Text:Inside destroy layout',
|
|
'Text:Fallback:Fallback create layout',
|
|
'Text:Fallback:Outside create layout',
|
|
]);
|
|
expect(Scheduler).toFlushAndYield([
|
|
'Text:Fallback:Fallback create passive',
|
|
'Text:Fallback:Outside create passive',
|
|
]);
|
|
expect(ReactNoop.getChildren()).toEqual([
|
|
spanHidden('Inside'),
|
|
span('Fallback:Fallback'),
|
|
span('Fallback:Outside'),
|
|
span('Outside'),
|
|
]);
|
|
|
|
// Resolving the inside fallback
|
|
await act(async () => {
|
|
await resolveText('FallbackAsync');
|
|
});
|
|
expect(Scheduler).toHaveYielded([
|
|
'Text:Fallback:Inside render',
|
|
'AsyncText:FallbackAsync render',
|
|
'Text:Fallback:Fallback destroy layout',
|
|
'Text:Fallback:Inside create layout',
|
|
'AsyncText:FallbackAsync create layout',
|
|
'Text:Fallback:Fallback destroy passive',
|
|
'Text:Fallback:Inside create passive',
|
|
'AsyncText:FallbackAsync create passive',
|
|
]);
|
|
expect(ReactNoop.getChildren()).toEqual([
|
|
spanHidden('Inside'),
|
|
span('Fallback:Inside'),
|
|
span('FallbackAsync'),
|
|
span('Fallback:Outside'),
|
|
span('Outside'),
|
|
]);
|
|
|
|
// Resolving the outer fallback only
|
|
await act(async () => {
|
|
await resolveText('OutsideAsync');
|
|
});
|
|
expect(Scheduler).toHaveYielded([
|
|
'Text:Inside render',
|
|
'AsyncText:OutsideAsync render',
|
|
'Text:Fallback:Inside destroy layout',
|
|
'AsyncText:FallbackAsync destroy layout',
|
|
'Text:Fallback:Outside destroy layout',
|
|
'Text:Inside create layout',
|
|
'AsyncText:OutsideAsync create layout',
|
|
'Text:Fallback:Inside destroy passive',
|
|
'AsyncText:FallbackAsync destroy passive',
|
|
'Text:Fallback:Outside destroy passive',
|
|
'AsyncText:OutsideAsync create passive',
|
|
]);
|
|
expect(ReactNoop.getChildren()).toEqual([
|
|
span('Inside'),
|
|
span('OutsideAsync'),
|
|
span('Outside'),
|
|
]);
|
|
});
|
|
|
|
// @gate enableLegacyCache
|
|
it('should be cleaned up deeper inside of a subtree that suspends', async () => {
|
|
function ConditionalSuspense({shouldSuspend}) {
|
|
if (shouldSuspend) {
|
|
readText('Suspend');
|
|
}
|
|
return <Text text="Inside" />;
|
|
}
|
|
|
|
function App({children = null, shouldSuspend}) {
|
|
return (
|
|
<>
|
|
<Suspense fallback={<Text text="Fallback" />}>
|
|
<ConditionalSuspense shouldSuspend={shouldSuspend} />
|
|
</Suspense>
|
|
<Text text="Outside" />
|
|
</>
|
|
);
|
|
}
|
|
|
|
// Mount
|
|
await act(async () => {
|
|
ReactNoop.render(<App shouldSuspend={false} />);
|
|
});
|
|
expect(Scheduler).toHaveYielded([
|
|
'Text:Inside render',
|
|
'Text:Outside render',
|
|
'Text:Inside create layout',
|
|
'Text:Outside create layout',
|
|
'Text:Inside create passive',
|
|
'Text:Outside create passive',
|
|
]);
|
|
expect(ReactNoop.getChildren()).toEqual([
|
|
span('Inside'),
|
|
span('Outside'),
|
|
]);
|
|
|
|
// Suspending a component in the middle of the tree
|
|
// should still properly cleanup effects deeper in the tree
|
|
act(() => {
|
|
ReactNoop.render(<App shouldSuspend={true} />);
|
|
});
|
|
expect(Scheduler).toHaveYielded([
|
|
'Suspend:Suspend',
|
|
'Text:Fallback render',
|
|
'Text:Outside render',
|
|
]);
|
|
expect(ReactNoop.getChildren()).toEqual([
|
|
span('Inside'),
|
|
span('Outside'),
|
|
]);
|
|
|
|
// Timing out should commit the inner fallback and destroy outer fallback layout effects.
|
|
await advanceTimers(1000);
|
|
expect(Scheduler).toHaveYielded([
|
|
'Text:Inside destroy layout',
|
|
'Text:Fallback create layout',
|
|
]);
|
|
expect(Scheduler).toFlushAndYield(['Text:Fallback create passive']);
|
|
expect(ReactNoop.getChildren()).toEqual([
|
|
spanHidden('Inside'),
|
|
span('Fallback'),
|
|
span('Outside'),
|
|
]);
|
|
|
|
// Resolving should cleanup.
|
|
await act(async () => {
|
|
await resolveText('Suspend');
|
|
});
|
|
expect(Scheduler).toHaveYielded([
|
|
'Text:Inside render',
|
|
'Text:Fallback destroy layout',
|
|
'Text:Inside create layout',
|
|
'Text:Fallback destroy passive',
|
|
]);
|
|
expect(ReactNoop.getChildren()).toEqual([
|
|
span('Inside'),
|
|
span('Outside'),
|
|
]);
|
|
});
|
|
|
|
describe('that throw errors', () => {
|
|
// @gate enableLegacyCache
|
|
it('are properly handled for componentDidMount', async () => {
|
|
let componentDidMountShouldThrow = false;
|
|
|
|
class ThrowsInDidMount extends React.Component {
|
|
componentWillUnmount() {
|
|
Scheduler.unstable_yieldValue(
|
|
'ThrowsInDidMount componentWillUnmount',
|
|
);
|
|
}
|
|
componentDidMount() {
|
|
Scheduler.unstable_yieldValue('ThrowsInDidMount componentDidMount');
|
|
if (componentDidMountShouldThrow) {
|
|
throw Error('expected');
|
|
}
|
|
}
|
|
render() {
|
|
Scheduler.unstable_yieldValue('ThrowsInDidMount render');
|
|
return <span prop="ThrowsInDidMount" />;
|
|
}
|
|
}
|
|
|
|
function App({children = null}) {
|
|
Scheduler.unstable_yieldValue('App render');
|
|
React.useLayoutEffect(() => {
|
|
Scheduler.unstable_yieldValue('App create layout');
|
|
return () => {
|
|
Scheduler.unstable_yieldValue('App destroy layout');
|
|
};
|
|
}, []);
|
|
return (
|
|
<>
|
|
<Suspense fallback={<Text text="Fallback" />}>
|
|
{children}
|
|
<ThrowsInDidMount />
|
|
<Text text="Inside" />
|
|
</Suspense>
|
|
<Text text="Outside" />
|
|
</>
|
|
);
|
|
}
|
|
|
|
await act(async () => {
|
|
ReactNoop.render(
|
|
<ErrorBoundary fallback={<Text text="Error" />}>
|
|
<App />
|
|
</ErrorBoundary>,
|
|
);
|
|
});
|
|
expect(Scheduler).toHaveYielded([
|
|
'ErrorBoundary render: try',
|
|
'App render',
|
|
'ThrowsInDidMount render',
|
|
'Text:Inside render',
|
|
'Text:Outside render',
|
|
'ThrowsInDidMount componentDidMount',
|
|
'Text:Inside create layout',
|
|
'Text:Outside create layout',
|
|
'App create layout',
|
|
'Text:Inside create passive',
|
|
'Text:Outside create passive',
|
|
]);
|
|
expect(ReactNoop.getChildren()).toEqual([
|
|
span('ThrowsInDidMount'),
|
|
span('Inside'),
|
|
span('Outside'),
|
|
]);
|
|
|
|
// Schedule an update that causes React to suspend.
|
|
await act(async () => {
|
|
ReactNoop.render(
|
|
<ErrorBoundary fallback={<Text text="Error" />}>
|
|
<App>
|
|
<AsyncText text="Async" ms={1000} />
|
|
</App>
|
|
</ErrorBoundary>,
|
|
);
|
|
});
|
|
expect(Scheduler).toHaveYielded([
|
|
'ErrorBoundary render: try',
|
|
'App render',
|
|
'Suspend:Async',
|
|
'ThrowsInDidMount render',
|
|
'Text:Inside render',
|
|
'Text:Fallback render',
|
|
'Text:Outside render',
|
|
'ThrowsInDidMount componentWillUnmount',
|
|
'Text:Inside destroy layout',
|
|
'Text:Fallback create layout',
|
|
'Text:Fallback create passive',
|
|
]);
|
|
expect(ReactNoop.getChildren()).toEqual([
|
|
spanHidden('ThrowsInDidMount'),
|
|
spanHidden('Inside'),
|
|
span('Fallback'),
|
|
span('Outside'),
|
|
]);
|
|
|
|
// Resolve the pending suspense and throw
|
|
componentDidMountShouldThrow = true;
|
|
await act(async () => {
|
|
await resolveText('Async');
|
|
});
|
|
expect(Scheduler).toHaveYielded([
|
|
'AsyncText:Async render',
|
|
'ThrowsInDidMount render',
|
|
'Text:Inside render',
|
|
'Text:Fallback destroy layout',
|
|
'AsyncText:Async create layout',
|
|
|
|
// Even though an error was thrown in componentDidMount,
|
|
// subsequent layout effects should still be destroyed.
|
|
'ThrowsInDidMount componentDidMount',
|
|
'Text:Inside create layout',
|
|
|
|
// Finish the in-progress commit
|
|
'Text:Fallback destroy passive',
|
|
'AsyncText:Async create passive',
|
|
|
|
// Destroy layout and passive effects in the errored tree.
|
|
'App destroy layout',
|
|
'AsyncText:Async destroy layout',
|
|
'ThrowsInDidMount componentWillUnmount',
|
|
'Text:Inside destroy layout',
|
|
'Text:Outside destroy layout',
|
|
'AsyncText:Async destroy passive',
|
|
'Text:Inside destroy passive',
|
|
'Text:Outside destroy passive',
|
|
|
|
// Render fallback
|
|
'ErrorBoundary render: catch',
|
|
'Text:Error render',
|
|
'Text:Error create layout',
|
|
'Text:Error create passive',
|
|
]);
|
|
expect(ReactNoop.getChildren()).toEqual([span('Error')]);
|
|
});
|
|
|
|
// @gate enableLegacyCache
|
|
it('are properly handled for componentWillUnmount', async () => {
|
|
class ThrowsInWillUnmount extends React.Component {
|
|
componentDidMount() {
|
|
Scheduler.unstable_yieldValue(
|
|
'ThrowsInWillUnmount componentDidMount',
|
|
);
|
|
}
|
|
componentWillUnmount() {
|
|
Scheduler.unstable_yieldValue(
|
|
'ThrowsInWillUnmount componentWillUnmount',
|
|
);
|
|
throw Error('expected');
|
|
}
|
|
render() {
|
|
Scheduler.unstable_yieldValue('ThrowsInWillUnmount render');
|
|
return <span prop="ThrowsInWillUnmount" />;
|
|
}
|
|
}
|
|
|
|
function App({children = null}) {
|
|
Scheduler.unstable_yieldValue('App render');
|
|
React.useLayoutEffect(() => {
|
|
Scheduler.unstable_yieldValue('App create layout');
|
|
return () => {
|
|
Scheduler.unstable_yieldValue('App destroy layout');
|
|
};
|
|
}, []);
|
|
return (
|
|
<>
|
|
<Suspense fallback={<Text text="Fallback" />}>
|
|
{children}
|
|
<ThrowsInWillUnmount />
|
|
<Text text="Inside" />
|
|
</Suspense>
|
|
<Text text="Outside" />
|
|
</>
|
|
);
|
|
}
|
|
|
|
await act(async () => {
|
|
ReactNoop.render(
|
|
<ErrorBoundary fallback={<Text text="Error" />}>
|
|
<App />
|
|
</ErrorBoundary>,
|
|
);
|
|
});
|
|
expect(Scheduler).toHaveYielded([
|
|
'ErrorBoundary render: try',
|
|
'App render',
|
|
'ThrowsInWillUnmount render',
|
|
'Text:Inside render',
|
|
'Text:Outside render',
|
|
'ThrowsInWillUnmount componentDidMount',
|
|
'Text:Inside create layout',
|
|
'Text:Outside create layout',
|
|
'App create layout',
|
|
'Text:Inside create passive',
|
|
'Text:Outside create passive',
|
|
]);
|
|
expect(ReactNoop.getChildren()).toEqual([
|
|
span('ThrowsInWillUnmount'),
|
|
span('Inside'),
|
|
span('Outside'),
|
|
]);
|
|
|
|
// Schedule an update that suspends and triggers our error code.
|
|
await act(async () => {
|
|
ReactNoop.render(
|
|
<ErrorBoundary fallback={<Text text="Error" />}>
|
|
<App>
|
|
<AsyncText text="Async" ms={1000} />
|
|
</App>
|
|
</ErrorBoundary>,
|
|
);
|
|
});
|
|
expect(Scheduler).toHaveYielded([
|
|
'ErrorBoundary render: try',
|
|
'App render',
|
|
'Suspend:Async',
|
|
'ThrowsInWillUnmount render',
|
|
'Text:Inside render',
|
|
'Text:Fallback render',
|
|
'Text:Outside render',
|
|
|
|
// Even though an error was thrown in componentWillUnmount,
|
|
// subsequent layout effects should still be destroyed.
|
|
'ThrowsInWillUnmount componentWillUnmount',
|
|
'Text:Inside destroy layout',
|
|
|
|
// Finish the in-progress commit
|
|
'Text:Fallback create layout',
|
|
'Text:Fallback create passive',
|
|
|
|
// Destroy layout and passive effects in the errored tree.
|
|
'App destroy layout',
|
|
'Text:Fallback destroy layout',
|
|
'Text:Outside destroy layout',
|
|
'Text:Inside destroy passive',
|
|
'Text:Fallback destroy passive',
|
|
'Text:Outside destroy passive',
|
|
|
|
// Render fallback
|
|
'ErrorBoundary render: catch',
|
|
'Text:Error render',
|
|
'Text:Error create layout',
|
|
'Text:Error create passive',
|
|
]);
|
|
expect(ReactNoop.getChildren()).toEqual([span('Error')]);
|
|
});
|
|
|
|
// @gate enableLegacyCache
|
|
// @gate replayFailedUnitOfWorkWithInvokeGuardedCallback
|
|
it('are properly handled for layout effect creation', async () => {
|
|
let useLayoutEffectShouldThrow = false;
|
|
|
|
function ThrowsInLayoutEffect() {
|
|
Scheduler.unstable_yieldValue('ThrowsInLayoutEffect render');
|
|
React.useLayoutEffect(() => {
|
|
Scheduler.unstable_yieldValue(
|
|
'ThrowsInLayoutEffect useLayoutEffect create',
|
|
);
|
|
if (useLayoutEffectShouldThrow) {
|
|
throw Error('expected');
|
|
}
|
|
return () => {
|
|
Scheduler.unstable_yieldValue(
|
|
'ThrowsInLayoutEffect useLayoutEffect destroy',
|
|
);
|
|
};
|
|
}, []);
|
|
return <span prop="ThrowsInLayoutEffect" />;
|
|
}
|
|
|
|
function App({children = null}) {
|
|
Scheduler.unstable_yieldValue('App render');
|
|
React.useLayoutEffect(() => {
|
|
Scheduler.unstable_yieldValue('App create layout');
|
|
return () => {
|
|
Scheduler.unstable_yieldValue('App destroy layout');
|
|
};
|
|
}, []);
|
|
return (
|
|
<>
|
|
<Suspense fallback={<Text text="Fallback" />}>
|
|
{children}
|
|
<ThrowsInLayoutEffect />
|
|
<Text text="Inside" />
|
|
</Suspense>
|
|
<Text text="Outside" />
|
|
</>
|
|
);
|
|
}
|
|
|
|
await act(async () => {
|
|
ReactNoop.render(
|
|
<ErrorBoundary fallback={<Text text="Error" />}>
|
|
<App />
|
|
</ErrorBoundary>,
|
|
);
|
|
});
|
|
expect(Scheduler).toHaveYielded([
|
|
'ErrorBoundary render: try',
|
|
'App render',
|
|
'ThrowsInLayoutEffect render',
|
|
'Text:Inside render',
|
|
'Text:Outside render',
|
|
'ThrowsInLayoutEffect useLayoutEffect create',
|
|
'Text:Inside create layout',
|
|
'Text:Outside create layout',
|
|
'App create layout',
|
|
'Text:Inside create passive',
|
|
'Text:Outside create passive',
|
|
]);
|
|
expect(ReactNoop.getChildren()).toEqual([
|
|
span('ThrowsInLayoutEffect'),
|
|
span('Inside'),
|
|
span('Outside'),
|
|
]);
|
|
|
|
// Schedule an update that causes React to suspend.
|
|
await act(async () => {
|
|
ReactNoop.render(
|
|
<ErrorBoundary fallback={<Text text="Error" />}>
|
|
<App>
|
|
<AsyncText text="Async" ms={1000} />
|
|
</App>
|
|
</ErrorBoundary>,
|
|
);
|
|
});
|
|
expect(Scheduler).toHaveYielded([
|
|
'ErrorBoundary render: try',
|
|
'App render',
|
|
'Suspend:Async',
|
|
'ThrowsInLayoutEffect render',
|
|
'Text:Inside render',
|
|
'Text:Fallback render',
|
|
'Text:Outside render',
|
|
'ThrowsInLayoutEffect useLayoutEffect destroy',
|
|
'Text:Inside destroy layout',
|
|
'Text:Fallback create layout',
|
|
'Text:Fallback create passive',
|
|
]);
|
|
expect(ReactNoop.getChildren()).toEqual([
|
|
spanHidden('ThrowsInLayoutEffect'),
|
|
spanHidden('Inside'),
|
|
span('Fallback'),
|
|
span('Outside'),
|
|
]);
|
|
|
|
// Resolve the pending suspense and throw
|
|
useLayoutEffectShouldThrow = true;
|
|
await act(async () => {
|
|
await resolveText('Async');
|
|
});
|
|
expect(Scheduler).toHaveYielded([
|
|
'AsyncText:Async render',
|
|
'ThrowsInLayoutEffect render',
|
|
'Text:Inside render',
|
|
|
|
'Text:Fallback destroy layout',
|
|
|
|
// Even though an error was thrown in useLayoutEffect,
|
|
// subsequent layout effects should still be created.
|
|
'AsyncText:Async create layout',
|
|
'ThrowsInLayoutEffect useLayoutEffect create',
|
|
'Text:Inside create layout',
|
|
|
|
// Finish the in-progress commit
|
|
'Text:Fallback destroy passive',
|
|
'AsyncText:Async create passive',
|
|
|
|
// Destroy layout and passive effects in the errored tree.
|
|
'App destroy layout',
|
|
'AsyncText:Async destroy layout',
|
|
'Text:Inside destroy layout',
|
|
'Text:Outside destroy layout',
|
|
'AsyncText:Async destroy passive',
|
|
'Text:Inside destroy passive',
|
|
'Text:Outside destroy passive',
|
|
|
|
// Render fallback
|
|
'ErrorBoundary render: catch',
|
|
'Text:Error render',
|
|
'Text:Error create layout',
|
|
'Text:Error create passive',
|
|
]);
|
|
expect(ReactNoop.getChildren()).toEqual([span('Error')]);
|
|
});
|
|
|
|
// @gate enableLegacyCache
|
|
// @gate replayFailedUnitOfWorkWithInvokeGuardedCallback
|
|
it('are properly handled for layout effect destruction', async () => {
|
|
function ThrowsInLayoutEffectDestroy() {
|
|
Scheduler.unstable_yieldValue('ThrowsInLayoutEffectDestroy render');
|
|
React.useLayoutEffect(() => {
|
|
Scheduler.unstable_yieldValue(
|
|
'ThrowsInLayoutEffectDestroy useLayoutEffect create',
|
|
);
|
|
return () => {
|
|
Scheduler.unstable_yieldValue(
|
|
'ThrowsInLayoutEffectDestroy useLayoutEffect destroy',
|
|
);
|
|
throw Error('expected');
|
|
};
|
|
}, []);
|
|
return <span prop="ThrowsInLayoutEffectDestroy" />;
|
|
}
|
|
|
|
function App({children = null}) {
|
|
Scheduler.unstable_yieldValue('App render');
|
|
React.useLayoutEffect(() => {
|
|
Scheduler.unstable_yieldValue('App create layout');
|
|
return () => {
|
|
Scheduler.unstable_yieldValue('App destroy layout');
|
|
};
|
|
}, []);
|
|
return (
|
|
<>
|
|
<Suspense fallback={<Text text="Fallback" />}>
|
|
{children}
|
|
<ThrowsInLayoutEffectDestroy />
|
|
<Text text="Inside" />
|
|
</Suspense>
|
|
<Text text="Outside" />
|
|
</>
|
|
);
|
|
}
|
|
|
|
await act(async () => {
|
|
ReactNoop.render(
|
|
<ErrorBoundary fallback={<Text text="Error" />}>
|
|
<App />
|
|
</ErrorBoundary>,
|
|
);
|
|
});
|
|
expect(Scheduler).toHaveYielded([
|
|
'ErrorBoundary render: try',
|
|
'App render',
|
|
'ThrowsInLayoutEffectDestroy render',
|
|
'Text:Inside render',
|
|
'Text:Outside render',
|
|
'ThrowsInLayoutEffectDestroy useLayoutEffect create',
|
|
'Text:Inside create layout',
|
|
'Text:Outside create layout',
|
|
'App create layout',
|
|
'Text:Inside create passive',
|
|
'Text:Outside create passive',
|
|
]);
|
|
expect(ReactNoop.getChildren()).toEqual([
|
|
span('ThrowsInLayoutEffectDestroy'),
|
|
span('Inside'),
|
|
span('Outside'),
|
|
]);
|
|
|
|
// Schedule an update that suspends and triggers our error code.
|
|
await act(async () => {
|
|
ReactNoop.render(
|
|
<ErrorBoundary fallback={<Text text="Error" />}>
|
|
<App>
|
|
<AsyncText text="Async" ms={1000} />
|
|
</App>
|
|
</ErrorBoundary>,
|
|
);
|
|
});
|
|
expect(Scheduler).toHaveYielded([
|
|
'ErrorBoundary render: try',
|
|
'App render',
|
|
'Suspend:Async',
|
|
'ThrowsInLayoutEffectDestroy render',
|
|
'Text:Inside render',
|
|
'Text:Fallback render',
|
|
'Text:Outside render',
|
|
|
|
// Even though an error was thrown in useLayoutEffect destroy,
|
|
// subsequent layout effects should still be destroyed.
|
|
'ThrowsInLayoutEffectDestroy useLayoutEffect destroy',
|
|
'Text:Inside destroy layout',
|
|
|
|
// Finish the in-progress commit
|
|
'Text:Fallback create layout',
|
|
'Text:Fallback create passive',
|
|
|
|
// Destroy layout and passive effects in the errored tree.
|
|
'App destroy layout',
|
|
'Text:Fallback destroy layout',
|
|
'Text:Outside destroy layout',
|
|
'Text:Inside destroy passive',
|
|
'Text:Fallback destroy passive',
|
|
'Text:Outside destroy passive',
|
|
|
|
// Render fallback
|
|
'ErrorBoundary render: catch',
|
|
'Text:Error render',
|
|
'Text:Error create layout',
|
|
'Text:Error create passive',
|
|
]);
|
|
expect(ReactNoop.getChildren()).toEqual([span('Error')]);
|
|
});
|
|
});
|
|
|
|
// @gate enableLegacyCache
|
|
it('should be only destroy layout effects once if a tree suspends in multiple places', async () => {
|
|
class ClassText extends React.Component {
|
|
componentDidMount() {
|
|
const {text} = this.props;
|
|
Scheduler.unstable_yieldValue(`ClassText:${text} componentDidMount`);
|
|
}
|
|
componentDidUpdate() {
|
|
const {text} = this.props;
|
|
Scheduler.unstable_yieldValue(`ClassText:${text} componentDidUpdate`);
|
|
}
|
|
componentWillUnmount() {
|
|
const {text} = this.props;
|
|
Scheduler.unstable_yieldValue(
|
|
`ClassText:${text} componentWillUnmount`,
|
|
);
|
|
}
|
|
render() {
|
|
const {children, text} = this.props;
|
|
Scheduler.unstable_yieldValue(`ClassText:${text} render`);
|
|
return <span prop={text}>{children}</span>;
|
|
}
|
|
}
|
|
|
|
function App({children = null}) {
|
|
return (
|
|
<Suspense fallback={<ClassText text="Fallback" />}>
|
|
<Text text="Function" />
|
|
{children}
|
|
<ClassText text="Class" />
|
|
</Suspense>
|
|
);
|
|
}
|
|
|
|
await act(async () => {
|
|
ReactNoop.render(<App />);
|
|
});
|
|
expect(Scheduler).toHaveYielded([
|
|
'Text:Function render',
|
|
'ClassText:Class render',
|
|
'Text:Function create layout',
|
|
'ClassText:Class componentDidMount',
|
|
'Text:Function create passive',
|
|
]);
|
|
expect(ReactNoop.getChildren()).toEqual([
|
|
span('Function'),
|
|
span('Class'),
|
|
]);
|
|
|
|
// Schedule an update that causes React to suspend.
|
|
act(() => {
|
|
ReactNoop.render(
|
|
<App>
|
|
<AsyncText text="Async_1" ms={1000} />
|
|
<AsyncText text="Async_2" ms={2000} />
|
|
</App>,
|
|
);
|
|
});
|
|
expect(Scheduler).toHaveYielded([
|
|
'Text:Function render',
|
|
'Suspend:Async_1',
|
|
'Suspend:Async_2',
|
|
'ClassText:Class render',
|
|
'ClassText:Fallback render',
|
|
]);
|
|
expect(ReactNoop.getChildren()).toEqual([
|
|
span('Function'),
|
|
span('Class'),
|
|
]);
|
|
|
|
await advanceTimers(1000);
|
|
|
|
// Timing out should commit the fallback and destroy inner layout effects.
|
|
expect(Scheduler).toHaveYielded([
|
|
'Text:Function destroy layout',
|
|
'ClassText:Class componentWillUnmount',
|
|
'ClassText:Fallback componentDidMount',
|
|
]);
|
|
expect(ReactNoop.getChildren()).toEqual([
|
|
spanHidden('Function'),
|
|
spanHidden('Class'),
|
|
span('Fallback'),
|
|
]);
|
|
|
|
// Resolving the suspended resource should re-create inner layout effects.
|
|
await act(async () => {
|
|
await resolveText('Async_1');
|
|
});
|
|
expect(Scheduler).toHaveYielded([
|
|
'Text:Function render',
|
|
'AsyncText:Async_1 render',
|
|
'Suspend:Async_2',
|
|
'ClassText:Class render',
|
|
]);
|
|
expect(ReactNoop.getChildren()).toEqual([
|
|
spanHidden('Function'),
|
|
spanHidden('Class'),
|
|
span('Fallback'),
|
|
]);
|
|
|
|
// Resolving the suspended resource should re-create inner layout effects.
|
|
await act(async () => {
|
|
await resolveText('Async_2');
|
|
});
|
|
expect(Scheduler).toHaveYielded([
|
|
'Text:Function render',
|
|
'AsyncText:Async_1 render',
|
|
'AsyncText:Async_2 render',
|
|
'ClassText:Class render',
|
|
'ClassText:Fallback componentWillUnmount',
|
|
'Text:Function create layout',
|
|
'AsyncText:Async_1 create layout',
|
|
'AsyncText:Async_2 create layout',
|
|
'ClassText:Class componentDidMount',
|
|
'AsyncText:Async_1 create passive',
|
|
'AsyncText:Async_2 create passive',
|
|
]);
|
|
expect(ReactNoop.getChildren()).toEqual([
|
|
span('Function'),
|
|
span('Async_1'),
|
|
span('Async_2'),
|
|
span('Class'),
|
|
]);
|
|
|
|
await act(async () => {
|
|
ReactNoop.render(null);
|
|
});
|
|
expect(Scheduler).toHaveYielded([
|
|
'Text:Function destroy layout',
|
|
'AsyncText:Async_1 destroy layout',
|
|
'AsyncText:Async_2 destroy layout',
|
|
'ClassText:Class componentWillUnmount',
|
|
'Text:Function destroy passive',
|
|
'AsyncText:Async_1 destroy passive',
|
|
'AsyncText:Async_2 destroy passive',
|
|
]);
|
|
});
|
|
|
|
// @gate enableLegacyCache
|
|
it('should be only destroy layout effects once if a component suspends multiple times', async () => {
|
|
class ClassText extends React.Component {
|
|
componentDidMount() {
|
|
const {text} = this.props;
|
|
Scheduler.unstable_yieldValue(`ClassText:${text} componentDidMount`);
|
|
}
|
|
componentDidUpdate() {
|
|
const {text} = this.props;
|
|
Scheduler.unstable_yieldValue(`ClassText:${text} componentDidUpdate`);
|
|
}
|
|
componentWillUnmount() {
|
|
const {text} = this.props;
|
|
Scheduler.unstable_yieldValue(
|
|
`ClassText:${text} componentWillUnmount`,
|
|
);
|
|
}
|
|
render() {
|
|
const {children, text} = this.props;
|
|
Scheduler.unstable_yieldValue(`ClassText:${text} render`);
|
|
return <span prop={text}>{children}</span>;
|
|
}
|
|
}
|
|
|
|
let textToRead = null;
|
|
|
|
function Suspender() {
|
|
Scheduler.unstable_yieldValue(`Suspender "${textToRead}" render`);
|
|
if (textToRead !== null) {
|
|
readText(textToRead);
|
|
}
|
|
return <span prop="Suspender" />;
|
|
}
|
|
|
|
function App({children = null}) {
|
|
return (
|
|
<Suspense fallback={<ClassText text="Fallback" />}>
|
|
<Text text="Function" />
|
|
<Suspender />
|
|
<ClassText text="Class" />
|
|
</Suspense>
|
|
);
|
|
}
|
|
|
|
await act(async () => {
|
|
ReactNoop.render(<App />);
|
|
});
|
|
expect(Scheduler).toHaveYielded([
|
|
'Text:Function render',
|
|
'Suspender "null" render',
|
|
'ClassText:Class render',
|
|
'Text:Function create layout',
|
|
'ClassText:Class componentDidMount',
|
|
'Text:Function create passive',
|
|
]);
|
|
expect(ReactNoop.getChildren()).toEqual([
|
|
span('Function'),
|
|
span('Suspender'),
|
|
span('Class'),
|
|
]);
|
|
|
|
// Schedule an update that causes React to suspend.
|
|
textToRead = 'A';
|
|
act(() => {
|
|
ReactNoop.render(<App />);
|
|
});
|
|
expect(Scheduler).toHaveYielded([
|
|
'Text:Function render',
|
|
'Suspender "A" render',
|
|
'Suspend:A',
|
|
'ClassText:Class render',
|
|
'ClassText:Fallback render',
|
|
]);
|
|
expect(ReactNoop.getChildren()).toEqual([
|
|
span('Function'),
|
|
span('Suspender'),
|
|
span('Class'),
|
|
]);
|
|
|
|
await advanceTimers(1000);
|
|
|
|
// Timing out should commit the fallback and destroy inner layout effects.
|
|
expect(Scheduler).toHaveYielded([
|
|
'Text:Function destroy layout',
|
|
'ClassText:Class componentWillUnmount',
|
|
'ClassText:Fallback componentDidMount',
|
|
]);
|
|
expect(ReactNoop.getChildren()).toEqual([
|
|
spanHidden('Function'),
|
|
spanHidden('Suspender'),
|
|
spanHidden('Class'),
|
|
span('Fallback'),
|
|
]);
|
|
|
|
// Resolving the suspended resource should re-create inner layout effects.
|
|
textToRead = 'B';
|
|
await act(async () => {
|
|
await resolveText('A');
|
|
});
|
|
expect(Scheduler).toHaveYielded([
|
|
'Text:Function render',
|
|
'Suspender "B" render',
|
|
'Suspend:B',
|
|
'ClassText:Class render',
|
|
]);
|
|
expect(ReactNoop.getChildren()).toEqual([
|
|
spanHidden('Function'),
|
|
spanHidden('Suspender'),
|
|
spanHidden('Class'),
|
|
span('Fallback'),
|
|
]);
|
|
|
|
// Resolving the suspended resource should re-create inner layout effects.
|
|
await act(async () => {
|
|
await resolveText('B');
|
|
});
|
|
expect(Scheduler).toHaveYielded([
|
|
'Text:Function render',
|
|
'Suspender "B" render',
|
|
'ClassText:Class render',
|
|
'ClassText:Fallback componentWillUnmount',
|
|
'Text:Function create layout',
|
|
'ClassText:Class componentDidMount',
|
|
]);
|
|
expect(ReactNoop.getChildren()).toEqual([
|
|
span('Function'),
|
|
span('Suspender'),
|
|
span('Class'),
|
|
]);
|
|
|
|
await act(async () => {
|
|
ReactNoop.render(null);
|
|
});
|
|
expect(Scheduler).toHaveYielded([
|
|
'Text:Function destroy layout',
|
|
'ClassText:Class componentWillUnmount',
|
|
'Text:Function destroy passive',
|
|
]);
|
|
});
|
|
});
|
|
|
|
describe('refs within a tree that re-suspends in an update', () => {
|
|
function RefCheckerOuter({Component}) {
|
|
const refObject = React.useRef(null);
|
|
|
|
const manualRef = React.useMemo(() => ({current: null}), []);
|
|
const refCallback = React.useCallback(value => {
|
|
Scheduler.unstable_yieldValue(
|
|
`RefCheckerOuter refCallback value? ${value != null}`,
|
|
);
|
|
manualRef.current = value;
|
|
}, []);
|
|
|
|
Scheduler.unstable_yieldValue(`RefCheckerOuter render`);
|
|
|
|
React.useLayoutEffect(() => {
|
|
Scheduler.unstable_yieldValue(
|
|
`RefCheckerOuter create layout refObject? ${refObject.current !=
|
|
null} refCallback? ${manualRef.current != null}`,
|
|
);
|
|
return () => {
|
|
Scheduler.unstable_yieldValue(
|
|
`RefCheckerOuter destroy layout refObject? ${refObject.current !=
|
|
null} refCallback? ${manualRef.current != null}`,
|
|
);
|
|
};
|
|
}, []);
|
|
|
|
return (
|
|
<>
|
|
<Component ref={refObject} prop="refObject">
|
|
<RefCheckerInner forwardedRef={refObject} text="refObject" />
|
|
</Component>
|
|
<Component ref={refCallback} prop="refCallback">
|
|
<RefCheckerInner forwardedRef={manualRef} text="refCallback" />
|
|
</Component>
|
|
</>
|
|
);
|
|
}
|
|
|
|
function RefCheckerInner({forwardedRef, text}) {
|
|
Scheduler.unstable_yieldValue(`RefCheckerInner:${text} render`);
|
|
React.useLayoutEffect(() => {
|
|
Scheduler.unstable_yieldValue(
|
|
`RefCheckerInner:${text} create layout ref? ${forwardedRef.current !=
|
|
null}`,
|
|
);
|
|
return () => {
|
|
Scheduler.unstable_yieldValue(
|
|
`RefCheckerInner:${text} destroy layout ref? ${forwardedRef.current !=
|
|
null}`,
|
|
);
|
|
};
|
|
}, []);
|
|
return null;
|
|
}
|
|
|
|
// @gate enableLegacyCache
|
|
it('should not be cleared within legacy roots', async () => {
|
|
class ClassComponent extends React.Component {
|
|
render() {
|
|
Scheduler.unstable_yieldValue(
|
|
`ClassComponent:${this.props.prop} render`,
|
|
);
|
|
return this.props.children;
|
|
}
|
|
}
|
|
|
|
function App({children}) {
|
|
Scheduler.unstable_yieldValue(`App render`);
|
|
return (
|
|
<Suspense fallback={<Text text="Fallback" />}>
|
|
{children}
|
|
<RefCheckerOuter Component={ClassComponent} />
|
|
</Suspense>
|
|
);
|
|
}
|
|
|
|
act(() => {
|
|
ReactNoop.renderLegacySyncRoot(<App />);
|
|
});
|
|
expect(Scheduler).toHaveYielded([
|
|
'App render',
|
|
'RefCheckerOuter render',
|
|
'ClassComponent:refObject render',
|
|
'RefCheckerInner:refObject render',
|
|
'ClassComponent:refCallback render',
|
|
'RefCheckerInner:refCallback render',
|
|
'RefCheckerInner:refObject create layout ref? false',
|
|
'RefCheckerInner:refCallback create layout ref? false',
|
|
'RefCheckerOuter refCallback value? true',
|
|
'RefCheckerOuter create layout refObject? true refCallback? true',
|
|
]);
|
|
expect(ReactNoop.getChildren()).toEqual([]);
|
|
|
|
// Suspend the inner Suspense subtree (only inner effects should be destroyed)
|
|
act(() => {
|
|
ReactNoop.renderLegacySyncRoot(
|
|
<App children={<AsyncText text="Async" ms={1000} />} />,
|
|
);
|
|
});
|
|
await advanceTimers(1000);
|
|
expect(Scheduler).toHaveYielded([
|
|
'App render',
|
|
'Suspend:Async',
|
|
'RefCheckerOuter render',
|
|
'ClassComponent:refObject render',
|
|
'RefCheckerInner:refObject render',
|
|
'ClassComponent:refCallback render',
|
|
'RefCheckerInner:refCallback render',
|
|
'Text:Fallback render',
|
|
'Text:Fallback create layout',
|
|
'Text:Fallback create passive',
|
|
]);
|
|
expect(ReactNoop.getChildren()).toEqual([span('Fallback')]);
|
|
|
|
// Resolving the suspended resource should re-create inner layout effects.
|
|
await act(async () => {
|
|
await resolveText('Async');
|
|
});
|
|
expect(Scheduler).toHaveYielded([
|
|
'AsyncText:Async render',
|
|
'Text:Fallback destroy layout',
|
|
'AsyncText:Async create layout',
|
|
'Text:Fallback destroy passive',
|
|
'AsyncText:Async create passive',
|
|
]);
|
|
expect(ReactNoop.getChildren()).toEqual([span('Async')]);
|
|
|
|
await act(async () => {
|
|
ReactNoop.renderLegacySyncRoot(null);
|
|
});
|
|
expect(Scheduler).toHaveYielded([
|
|
'AsyncText:Async destroy layout',
|
|
'RefCheckerOuter destroy layout refObject? true refCallback? true',
|
|
'RefCheckerInner:refObject destroy layout ref? false',
|
|
'RefCheckerOuter refCallback value? false',
|
|
'RefCheckerInner:refCallback destroy layout ref? false',
|
|
'AsyncText:Async destroy passive',
|
|
]);
|
|
expect(ReactNoop.getChildren()).toEqual([]);
|
|
});
|
|
|
|
// @gate enableLegacyCache
|
|
it('should be cleared and reset for host components', async () => {
|
|
function App({children}) {
|
|
Scheduler.unstable_yieldValue(`App render`);
|
|
return (
|
|
<Suspense fallback={<Text text="Fallback" />}>
|
|
{children}
|
|
<RefCheckerOuter Component="span" />
|
|
</Suspense>
|
|
);
|
|
}
|
|
|
|
// Mount
|
|
await act(async () => {
|
|
ReactNoop.render(<App />);
|
|
});
|
|
expect(Scheduler).toHaveYielded([
|
|
'App render',
|
|
'RefCheckerOuter render',
|
|
'RefCheckerInner:refObject render',
|
|
'RefCheckerInner:refCallback render',
|
|
'RefCheckerInner:refObject create layout ref? false',
|
|
'RefCheckerInner:refCallback create layout ref? false',
|
|
'RefCheckerOuter refCallback value? true',
|
|
'RefCheckerOuter create layout refObject? true refCallback? true',
|
|
]);
|
|
expect(ReactNoop.getChildren()).toEqual([
|
|
span('refObject'),
|
|
span('refCallback'),
|
|
]);
|
|
|
|
// Suspend the inner Suspense subtree (only inner effects should be destroyed)
|
|
act(() => {
|
|
ReactNoop.render(
|
|
<App children={<AsyncText text="Async" ms={1000} />} />,
|
|
);
|
|
});
|
|
await advanceTimers(1000);
|
|
expect(Scheduler).toHaveYielded([
|
|
'App render',
|
|
'Suspend:Async',
|
|
'RefCheckerOuter render',
|
|
'RefCheckerInner:refObject render',
|
|
'RefCheckerInner:refCallback render',
|
|
'Text:Fallback render',
|
|
'RefCheckerOuter destroy layout refObject? true refCallback? true',
|
|
'RefCheckerInner:refObject destroy layout ref? false',
|
|
'RefCheckerOuter refCallback value? false',
|
|
'RefCheckerInner:refCallback destroy layout ref? false',
|
|
'Text:Fallback create layout',
|
|
]);
|
|
expect(ReactNoop.getChildren()).toEqual([
|
|
spanHidden('refObject'),
|
|
spanHidden('refCallback'),
|
|
span('Fallback'),
|
|
]);
|
|
|
|
// Resolving the suspended resource should re-create inner layout effects.
|
|
await act(async () => {
|
|
await resolveText('Async');
|
|
});
|
|
expect(Scheduler).toHaveYielded([
|
|
'Text:Fallback create passive',
|
|
'AsyncText:Async render',
|
|
'RefCheckerOuter render',
|
|
'RefCheckerInner:refObject render',
|
|
'RefCheckerInner:refCallback render',
|
|
'Text:Fallback destroy layout',
|
|
'AsyncText:Async create layout',
|
|
'RefCheckerInner:refObject create layout ref? false',
|
|
'RefCheckerInner:refCallback create layout ref? false',
|
|
'RefCheckerOuter refCallback value? true',
|
|
'RefCheckerOuter create layout refObject? true refCallback? true',
|
|
'Text:Fallback destroy passive',
|
|
'AsyncText:Async create passive',
|
|
]);
|
|
expect(ReactNoop.getChildren()).toEqual([
|
|
span('Async'),
|
|
span('refObject'),
|
|
span('refCallback'),
|
|
]);
|
|
|
|
await act(async () => {
|
|
ReactNoop.render(null);
|
|
});
|
|
expect(Scheduler).toHaveYielded([
|
|
'AsyncText:Async destroy layout',
|
|
'RefCheckerOuter destroy layout refObject? true refCallback? true',
|
|
'RefCheckerInner:refObject destroy layout ref? false',
|
|
'RefCheckerOuter refCallback value? false',
|
|
'RefCheckerInner:refCallback destroy layout ref? false',
|
|
'AsyncText:Async destroy passive',
|
|
]);
|
|
expect(ReactNoop.getChildren()).toEqual([]);
|
|
});
|
|
|
|
// @gate enableLegacyCache
|
|
it('should be cleared and reset for class components', async () => {
|
|
class ClassComponent extends React.Component {
|
|
render() {
|
|
Scheduler.unstable_yieldValue(
|
|
`ClassComponent:${this.props.prop} render`,
|
|
);
|
|
return this.props.children;
|
|
}
|
|
}
|
|
|
|
function App({children}) {
|
|
Scheduler.unstable_yieldValue(`App render`);
|
|
return (
|
|
<Suspense fallback={<Text text="Fallback" />}>
|
|
{children}
|
|
<RefCheckerOuter Component={ClassComponent} />
|
|
</Suspense>
|
|
);
|
|
}
|
|
|
|
// Mount
|
|
await act(async () => {
|
|
ReactNoop.render(<App />);
|
|
});
|
|
expect(Scheduler).toHaveYielded([
|
|
'App render',
|
|
'RefCheckerOuter render',
|
|
'ClassComponent:refObject render',
|
|
'RefCheckerInner:refObject render',
|
|
'ClassComponent:refCallback render',
|
|
'RefCheckerInner:refCallback render',
|
|
'RefCheckerInner:refObject create layout ref? false',
|
|
'RefCheckerInner:refCallback create layout ref? false',
|
|
'RefCheckerOuter refCallback value? true',
|
|
'RefCheckerOuter create layout refObject? true refCallback? true',
|
|
]);
|
|
expect(ReactNoop.getChildren()).toEqual([]);
|
|
|
|
// Suspend the inner Suspense subtree (only inner effects should be destroyed)
|
|
act(() => {
|
|
ReactNoop.render(
|
|
<App children={<AsyncText text="Async" ms={1000} />} />,
|
|
);
|
|
});
|
|
await advanceTimers(1000);
|
|
expect(Scheduler).toHaveYielded([
|
|
'App render',
|
|
'Suspend:Async',
|
|
'RefCheckerOuter render',
|
|
'ClassComponent:refObject render',
|
|
'RefCheckerInner:refObject render',
|
|
'ClassComponent:refCallback render',
|
|
'RefCheckerInner:refCallback render',
|
|
'Text:Fallback render',
|
|
'RefCheckerOuter destroy layout refObject? true refCallback? true',
|
|
'RefCheckerInner:refObject destroy layout ref? false',
|
|
'RefCheckerOuter refCallback value? false',
|
|
'RefCheckerInner:refCallback destroy layout ref? false',
|
|
'Text:Fallback create layout',
|
|
]);
|
|
expect(ReactNoop.getChildren()).toEqual([span('Fallback')]);
|
|
|
|
// Resolving the suspended resource should re-create inner layout effects.
|
|
await act(async () => {
|
|
await resolveText('Async');
|
|
});
|
|
expect(Scheduler).toHaveYielded([
|
|
'Text:Fallback create passive',
|
|
'AsyncText:Async render',
|
|
'RefCheckerOuter render',
|
|
'ClassComponent:refObject render',
|
|
'RefCheckerInner:refObject render',
|
|
'ClassComponent:refCallback render',
|
|
'RefCheckerInner:refCallback render',
|
|
'Text:Fallback destroy layout',
|
|
'AsyncText:Async create layout',
|
|
'RefCheckerInner:refObject create layout ref? false',
|
|
'RefCheckerInner:refCallback create layout ref? false',
|
|
'RefCheckerOuter refCallback value? true',
|
|
'RefCheckerOuter create layout refObject? true refCallback? true',
|
|
'Text:Fallback destroy passive',
|
|
'AsyncText:Async create passive',
|
|
]);
|
|
expect(ReactNoop.getChildren()).toEqual([span('Async')]);
|
|
|
|
await act(async () => {
|
|
ReactNoop.render(null);
|
|
});
|
|
expect(Scheduler).toHaveYielded([
|
|
'AsyncText:Async destroy layout',
|
|
'RefCheckerOuter destroy layout refObject? true refCallback? true',
|
|
'RefCheckerInner:refObject destroy layout ref? false',
|
|
'RefCheckerOuter refCallback value? false',
|
|
'RefCheckerInner:refCallback destroy layout ref? false',
|
|
'AsyncText:Async destroy passive',
|
|
]);
|
|
expect(ReactNoop.getChildren()).toEqual([]);
|
|
});
|
|
|
|
// @gate enableLegacyCache
|
|
it('should be cleared and reset for function components with useImperativeHandle', async () => {
|
|
const FunctionComponent = React.forwardRef((props, ref) => {
|
|
Scheduler.unstable_yieldValue('FunctionComponent render');
|
|
React.useImperativeHandle(
|
|
ref,
|
|
() => ({
|
|
// Noop
|
|
}),
|
|
[],
|
|
);
|
|
return props.children;
|
|
});
|
|
FunctionComponent.displayName = 'FunctionComponent';
|
|
|
|
function App({children}) {
|
|
Scheduler.unstable_yieldValue(`App render`);
|
|
return (
|
|
<Suspense fallback={<Text text="Fallback" />}>
|
|
{children}
|
|
<RefCheckerOuter Component={FunctionComponent} />
|
|
</Suspense>
|
|
);
|
|
}
|
|
|
|
// Mount
|
|
await act(async () => {
|
|
ReactNoop.render(<App />);
|
|
});
|
|
expect(Scheduler).toHaveYielded([
|
|
'App render',
|
|
'RefCheckerOuter render',
|
|
'FunctionComponent render',
|
|
'RefCheckerInner:refObject render',
|
|
'FunctionComponent render',
|
|
'RefCheckerInner:refCallback render',
|
|
'RefCheckerInner:refObject create layout ref? false',
|
|
'RefCheckerInner:refCallback create layout ref? false',
|
|
'RefCheckerOuter refCallback value? true',
|
|
'RefCheckerOuter create layout refObject? true refCallback? true',
|
|
]);
|
|
expect(ReactNoop.getChildren()).toEqual([]);
|
|
|
|
// Suspend the inner Suspense subtree (only inner effects should be destroyed)
|
|
act(() => {
|
|
ReactNoop.render(
|
|
<App children={<AsyncText text="Async" ms={1000} />} />,
|
|
);
|
|
});
|
|
await advanceTimers(1000);
|
|
expect(Scheduler).toHaveYielded([
|
|
'App render',
|
|
'Suspend:Async',
|
|
'RefCheckerOuter render',
|
|
'FunctionComponent render',
|
|
'RefCheckerInner:refObject render',
|
|
'FunctionComponent render',
|
|
'RefCheckerInner:refCallback render',
|
|
'Text:Fallback render',
|
|
'RefCheckerOuter destroy layout refObject? true refCallback? true',
|
|
'RefCheckerInner:refObject destroy layout ref? false',
|
|
'RefCheckerOuter refCallback value? false',
|
|
'RefCheckerInner:refCallback destroy layout ref? false',
|
|
'Text:Fallback create layout',
|
|
]);
|
|
expect(ReactNoop.getChildren()).toEqual([span('Fallback')]);
|
|
|
|
// Resolving the suspended resource should re-create inner layout effects.
|
|
await act(async () => {
|
|
await resolveText('Async');
|
|
});
|
|
expect(Scheduler).toHaveYielded([
|
|
'Text:Fallback create passive',
|
|
'AsyncText:Async render',
|
|
'RefCheckerOuter render',
|
|
'FunctionComponent render',
|
|
'RefCheckerInner:refObject render',
|
|
'FunctionComponent render',
|
|
'RefCheckerInner:refCallback render',
|
|
'Text:Fallback destroy layout',
|
|
'AsyncText:Async create layout',
|
|
'RefCheckerInner:refObject create layout ref? false',
|
|
'RefCheckerInner:refCallback create layout ref? false',
|
|
'RefCheckerOuter refCallback value? true',
|
|
'RefCheckerOuter create layout refObject? true refCallback? true',
|
|
'Text:Fallback destroy passive',
|
|
'AsyncText:Async create passive',
|
|
]);
|
|
expect(ReactNoop.getChildren()).toEqual([span('Async')]);
|
|
|
|
await act(async () => {
|
|
ReactNoop.render(null);
|
|
});
|
|
expect(Scheduler).toHaveYielded([
|
|
'AsyncText:Async destroy layout',
|
|
'RefCheckerOuter destroy layout refObject? true refCallback? true',
|
|
'RefCheckerInner:refObject destroy layout ref? false',
|
|
'RefCheckerOuter refCallback value? false',
|
|
'RefCheckerInner:refCallback destroy layout ref? false',
|
|
'AsyncText:Async destroy passive',
|
|
]);
|
|
expect(ReactNoop.getChildren()).toEqual([]);
|
|
});
|
|
|
|
// @gate enableLegacyCache
|
|
it('should not reset for user-managed values', async () => {
|
|
function RefChecker({forwardedRef}) {
|
|
Scheduler.unstable_yieldValue(`RefChecker render`);
|
|
React.useLayoutEffect(() => {
|
|
Scheduler.unstable_yieldValue(
|
|
`RefChecker create layout ref? ${forwardedRef.current === 'test'}`,
|
|
);
|
|
return () => {
|
|
Scheduler.unstable_yieldValue(
|
|
`RefChecker destroy layout ref? ${forwardedRef.current ===
|
|
'test'}`,
|
|
);
|
|
};
|
|
}, []);
|
|
return null;
|
|
}
|
|
|
|
function App({children = null}) {
|
|
const ref = React.useRef('test');
|
|
Scheduler.unstable_yieldValue(`App render`);
|
|
React.useLayoutEffect(() => {
|
|
Scheduler.unstable_yieldValue(
|
|
`App create layout ref? ${ref.current === 'test'}`,
|
|
);
|
|
return () => {
|
|
Scheduler.unstable_yieldValue(
|
|
`App destroy layout ref? ${ref.current === 'test'}`,
|
|
);
|
|
};
|
|
}, []);
|
|
return (
|
|
<Suspense fallback={<Text text="Fallback" />}>
|
|
{children}
|
|
<RefChecker forwardedRef={ref} />
|
|
</Suspense>
|
|
);
|
|
}
|
|
|
|
// Mount
|
|
await act(async () => {
|
|
ReactNoop.render(<App />);
|
|
});
|
|
expect(Scheduler).toHaveYielded([
|
|
'App render',
|
|
'RefChecker render',
|
|
'RefChecker create layout ref? true',
|
|
'App create layout ref? true',
|
|
]);
|
|
expect(ReactNoop.getChildren()).toEqual([]);
|
|
|
|
// Suspend the inner Suspense subtree (only inner effects should be destroyed)
|
|
act(() => {
|
|
ReactNoop.render(
|
|
<App children={<AsyncText text="Async" ms={1000} />} />,
|
|
);
|
|
});
|
|
await advanceTimers(1000);
|
|
expect(Scheduler).toHaveYielded([
|
|
'App render',
|
|
'Suspend:Async',
|
|
'RefChecker render',
|
|
'Text:Fallback render',
|
|
'RefChecker destroy layout ref? true',
|
|
'Text:Fallback create layout',
|
|
]);
|
|
expect(ReactNoop.getChildren()).toEqual([span('Fallback')]);
|
|
|
|
// Resolving the suspended resource should re-create inner layout effects.
|
|
await act(async () => {
|
|
await resolveText('Async');
|
|
});
|
|
expect(Scheduler).toHaveYielded([
|
|
'Text:Fallback create passive',
|
|
'AsyncText:Async render',
|
|
'RefChecker render',
|
|
'Text:Fallback destroy layout',
|
|
'AsyncText:Async create layout',
|
|
'RefChecker create layout ref? true',
|
|
'Text:Fallback destroy passive',
|
|
'AsyncText:Async create passive',
|
|
]);
|
|
expect(ReactNoop.getChildren()).toEqual([span('Async')]);
|
|
|
|
await act(async () => {
|
|
ReactNoop.render(null);
|
|
});
|
|
expect(Scheduler).toHaveYielded([
|
|
'App destroy layout ref? true',
|
|
'AsyncText:Async destroy layout',
|
|
'RefChecker destroy layout ref? true',
|
|
'AsyncText:Async destroy passive',
|
|
]);
|
|
expect(ReactNoop.getChildren()).toEqual([]);
|
|
});
|
|
|
|
describe('that throw errors', () => {
|
|
// @gate enableLegacyCache
|
|
// @gate replayFailedUnitOfWorkWithInvokeGuardedCallback
|
|
it('are properly handled in ref callbacks', async () => {
|
|
let useRefCallbackShouldThrow = false;
|
|
|
|
function ThrowsInRefCallback() {
|
|
Scheduler.unstable_yieldValue('ThrowsInRefCallback render');
|
|
const refCallback = React.useCallback(value => {
|
|
Scheduler.unstable_yieldValue(
|
|
'ThrowsInRefCallback refCallback ref? ' + !!value,
|
|
);
|
|
if (useRefCallbackShouldThrow) {
|
|
throw Error('expected');
|
|
}
|
|
}, []);
|
|
return <span ref={refCallback} prop="ThrowsInRefCallback" />;
|
|
}
|
|
|
|
function App({children = null}) {
|
|
Scheduler.unstable_yieldValue('App render');
|
|
React.useLayoutEffect(() => {
|
|
Scheduler.unstable_yieldValue('App create layout');
|
|
return () => {
|
|
Scheduler.unstable_yieldValue('App destroy layout');
|
|
};
|
|
}, []);
|
|
return (
|
|
<>
|
|
<Suspense fallback={<Text text="Fallback" />}>
|
|
{children}
|
|
<ThrowsInRefCallback />
|
|
<Text text="Inside" />
|
|
</Suspense>
|
|
<Text text="Outside" />
|
|
</>
|
|
);
|
|
}
|
|
|
|
await act(async () => {
|
|
ReactNoop.render(
|
|
<ErrorBoundary fallback={<Text text="Error" />}>
|
|
<App />
|
|
</ErrorBoundary>,
|
|
);
|
|
});
|
|
expect(Scheduler).toHaveYielded([
|
|
'ErrorBoundary render: try',
|
|
'App render',
|
|
'ThrowsInRefCallback render',
|
|
'Text:Inside render',
|
|
'Text:Outside render',
|
|
'ThrowsInRefCallback refCallback ref? true',
|
|
'Text:Inside create layout',
|
|
'Text:Outside create layout',
|
|
'App create layout',
|
|
'Text:Inside create passive',
|
|
'Text:Outside create passive',
|
|
]);
|
|
expect(ReactNoop.getChildren()).toEqual([
|
|
span('ThrowsInRefCallback'),
|
|
span('Inside'),
|
|
span('Outside'),
|
|
]);
|
|
|
|
// Schedule an update that causes React to suspend.
|
|
await act(async () => {
|
|
ReactNoop.render(
|
|
<ErrorBoundary fallback={<Text text="Error" />}>
|
|
<App>
|
|
<AsyncText text="Async" ms={1000} />
|
|
</App>
|
|
</ErrorBoundary>,
|
|
);
|
|
});
|
|
expect(Scheduler).toHaveYielded([
|
|
'ErrorBoundary render: try',
|
|
'App render',
|
|
'Suspend:Async',
|
|
'ThrowsInRefCallback render',
|
|
'Text:Inside render',
|
|
'Text:Fallback render',
|
|
'Text:Outside render',
|
|
'ThrowsInRefCallback refCallback ref? false',
|
|
'Text:Inside destroy layout',
|
|
'Text:Fallback create layout',
|
|
'Text:Fallback create passive',
|
|
]);
|
|
expect(ReactNoop.getChildren()).toEqual([
|
|
spanHidden('ThrowsInRefCallback'),
|
|
spanHidden('Inside'),
|
|
span('Fallback'),
|
|
span('Outside'),
|
|
]);
|
|
|
|
// Resolve the pending suspense and throw
|
|
useRefCallbackShouldThrow = true;
|
|
await act(async () => {
|
|
await resolveText('Async');
|
|
});
|
|
expect(Scheduler).toHaveYielded([
|
|
'AsyncText:Async render',
|
|
'ThrowsInRefCallback render',
|
|
'Text:Inside render',
|
|
|
|
// Even though an error was thrown in refCallback,
|
|
// subsequent layout effects should still be created.
|
|
'Text:Fallback destroy layout',
|
|
'AsyncText:Async create layout',
|
|
'ThrowsInRefCallback refCallback ref? true',
|
|
'Text:Inside create layout',
|
|
|
|
// Finish the in-progress commit
|
|
'Text:Fallback destroy passive',
|
|
'AsyncText:Async create passive',
|
|
|
|
// Destroy layout and passive effects in the errored tree.
|
|
'App destroy layout',
|
|
'AsyncText:Async destroy layout',
|
|
'ThrowsInRefCallback refCallback ref? false',
|
|
'Text:Inside destroy layout',
|
|
'Text:Outside destroy layout',
|
|
'AsyncText:Async destroy passive',
|
|
'Text:Inside destroy passive',
|
|
'Text:Outside destroy passive',
|
|
|
|
// Render fallback
|
|
'ErrorBoundary render: catch',
|
|
'Text:Error render',
|
|
'Text:Error create layout',
|
|
'Text:Error create passive',
|
|
]);
|
|
expect(ReactNoop.getChildren()).toEqual([span('Error')]);
|
|
});
|
|
});
|
|
});
|
|
});
|