let React;
let ReactNoop;
let Cache;
let getCacheSignal;
let Scheduler;
let assertLog;
let act;
let Suspense;
let Offscreen;
let useCacheRefresh;
let startTransition;
let useState;
let cache;
let getTextCache;
let textCaches;
let seededCache;
describe('ReactCache', () => {
beforeEach(() => {
jest.resetModules();
React = require('react');
ReactNoop = require('react-noop-renderer');
Cache = React.unstable_Cache;
Scheduler = require('scheduler');
act = require('internal-test-utils').act;
Suspense = React.Suspense;
cache = React.cache;
Offscreen = React.unstable_Offscreen;
getCacheSignal = React.unstable_getCacheSignal;
useCacheRefresh = React.unstable_useCacheRefresh;
startTransition = React.startTransition;
useState = React.useState;
const InternalTestUtils = require('internal-test-utils');
assertLog = InternalTestUtils.assertLog;
textCaches = [];
seededCache = null;
if (gate(flags => flags.enableCache)) {
getTextCache = cache(() => {
if (seededCache !== null) {
const textCache = seededCache;
seededCache = null;
return textCache;
}
const data = new Map();
const version = textCaches.length + 1;
const textCache = {
version,
data,
resolve(text) {
const record = data.get(text);
if (record === undefined) {
const newRecord = {
status: 'resolved',
value: text,
cleanupScheduled: false,
};
data.set(text, newRecord);
} else if (record.status === 'pending') {
record.value.resolve();
}
},
reject(text, error) {
const record = data.get(text);
if (record === undefined) {
const newRecord = {
status: 'rejected',
value: error,
cleanupScheduled: false,
};
data.set(text, newRecord);
} else if (record.status === 'pending') {
record.value.reject();
}
},
};
textCaches.push(textCache);
return textCache;
});
}
});
function readText(text) {
const signal = getCacheSignal ? getCacheSignal() : null;
const textCache = getTextCache();
const record = textCache.data.get(text);
if (record !== undefined) {
if (!record.cleanupScheduled) {
record.cleanupScheduled = true;
if (getCacheSignal) {
signal.addEventListener('abort', () => {
Scheduler.log(`Cache cleanup: ${text} [v${textCache.version}]`);
});
}
}
switch (record.status) {
case 'pending':
throw record.value;
case 'rejected':
throw record.value;
case 'resolved':
return textCache.version;
}
} else {
Scheduler.log(`Cache miss! [${text}]`);
let resolve;
let reject;
const thenable = new Promise((res, rej) => {
resolve = res;
reject = rej;
}).then(
value => {
if (newRecord.status === 'pending') {
newRecord.status = 'resolved';
newRecord.value = value;
}
},
error => {
if (newRecord.status === 'pending') {
newRecord.status = 'rejected';
newRecord.value = error;
}
},
);
thenable.resolve = resolve;
thenable.reject = reject;
const newRecord = {
status: 'pending',
value: thenable,
cleanupScheduled: true,
};
textCache.data.set(text, newRecord);
if (getCacheSignal) {
signal.addEventListener('abort', () => {
Scheduler.log(`Cache cleanup: ${text} [v${textCache.version}]`);
});
}
throw thenable;
}
}
function Text({text}) {
Scheduler.log(text);
return text;
}
function AsyncText({text, showVersion}) {
const version = readText(text);
const fullText = showVersion ? `${text} [v${version}]` : text;
Scheduler.log(fullText);
return fullText;
}
function seedNextTextCache(text) {
if (seededCache === null) {
seededCache = getTextCache();
}
seededCache.resolve(text);
}
function resolveMostRecentTextCache(text) {
if (textCaches.length === 0) {
throw Error('Cache does not exist.');
} else {
textCaches[textCaches.length - 1].resolve(text);
}
}
test('render Cache component', async () => {
const root = ReactNoop.createRoot();
await act(() => {
root.render(<Cache>Hi</Cache>);
});
expect(root).toMatchRenderedOutput('Hi');
});
test('mount new data', async () => {
const root = ReactNoop.createRoot();
await act(() => {
root.render(
<Cache>
<Suspense fallback={<Text text="Loading..." />}>
<AsyncText text="A" />
</Suspense>
</Cache>,
);
});
assertLog(['Cache miss! [A]', 'Loading...']);
expect(root).toMatchRenderedOutput('Loading...');
await act(() => {
resolveMostRecentTextCache('A');
});
assertLog(['A']);
expect(root).toMatchRenderedOutput('A');
await act(() => {
root.render('Bye');
});
assertLog([]);
expect(root).toMatchRenderedOutput('Bye');
});
test('root acts as implicit cache boundary', async () => {
const root = ReactNoop.createRoot();
await act(() => {
root.render(
<Suspense fallback={<Text text="Loading..." />}>
<AsyncText text="A" />
</Suspense>,
);
});
assertLog(['Cache miss! [A]', 'Loading...']);
expect(root).toMatchRenderedOutput('Loading...');
await act(() => {
resolveMostRecentTextCache('A');
});
assertLog(['A']);
expect(root).toMatchRenderedOutput('A');
await act(() => {
root.render('Bye');
});
assertLog([]);
expect(root).toMatchRenderedOutput('Bye');
});
test('multiple new Cache boundaries in the same mount share the same, fresh root cache', async () => {
function App() {
return (
<>
<Cache>
<Suspense fallback={<Text text="Loading..." />}>
<AsyncText text="A" />
</Suspense>
</Cache>
<Cache>
<Suspense fallback={<Text text="Loading..." />}>
<AsyncText text="A" />
</Suspense>
</Cache>
</>
);
}
const root = ReactNoop.createRoot();
await act(() => {
root.render(<App showMore={false} />);
});
assertLog(['Cache miss! [A]', 'Loading...', 'Loading...']);
expect(root).toMatchRenderedOutput('Loading...Loading...');
await act(() => {
resolveMostRecentTextCache('A');
});
assertLog(['A', 'A']);
expect(root).toMatchRenderedOutput('AA');
await act(() => {
root.render('Bye');
});
assertLog([]);
expect(root).toMatchRenderedOutput('Bye');
});
test('multiple new Cache boundaries in the same update share the same, fresh cache', async () => {
function App({showMore}) {
return showMore ? (
<>
<Cache>
<Suspense fallback={<Text text="Loading..." />}>
<AsyncText text="A" />
</Suspense>
</Cache>
<Cache>
<Suspense fallback={<Text text="Loading..." />}>
<AsyncText text="A" />
</Suspense>
</Cache>
</>
) : (
'(empty)'
);
}
const root = ReactNoop.createRoot();
await act(() => {
root.render(<App showMore={false} />);
});
assertLog([]);
expect(root).toMatchRenderedOutput('(empty)');
await act(() => {
root.render(<App showMore={true} />);
});
assertLog(['Cache miss! [A]', 'Loading...', 'Loading...']);
expect(root).toMatchRenderedOutput('Loading...Loading...');
await act(() => {
resolveMostRecentTextCache('A');
});
assertLog(['A', 'A']);
expect(root).toMatchRenderedOutput('AA');
await act(() => {
root.render('Bye');
});
assertLog(['Cache cleanup: A [v1]']);
expect(root).toMatchRenderedOutput('Bye');
});
test(
'nested cache boundaries share the same cache as the root during ' +
'the initial render',
async () => {
function App() {
return (
<Suspense fallback={<Text text="Loading..." />}>
<AsyncText text="A" />
<Cache>
<AsyncText text="A" />
</Cache>
</Suspense>
);
}
const root = ReactNoop.createRoot();
await act(() => {
root.render(<App />);
});
assertLog(['Cache miss! [A]', 'Loading...']);
expect(root).toMatchRenderedOutput('Loading...');
await act(() => {
resolveMostRecentTextCache('A');
});
assertLog(['A', 'A']);
expect(root).toMatchRenderedOutput('AA');
await act(() => {
root.render('Bye');
});
assertLog([]);
expect(root).toMatchRenderedOutput('Bye');
},
);
test('new content inside an existing Cache boundary should re-use already cached data', async () => {
function App({showMore}) {
return (
<Cache>
<Suspense fallback={<Text text="Loading..." />}>
<AsyncText showVersion={true} text="A" />
</Suspense>
{showMore ? (
<Suspense fallback={<Text text="Loading..." />}>
<AsyncText showVersion={true} text="A" />
</Suspense>
) : null}
</Cache>
);
}
const root = ReactNoop.createRoot();
await act(() => {
seedNextTextCache('A');
root.render(<App showMore={false} />);
});
assertLog(['A [v1]']);
expect(root).toMatchRenderedOutput('A [v1]');
await act(() => {
root.render(<App showMore={true} />);
});
assertLog([
'A [v1]',
'A [v1]',
]);
expect(root).toMatchRenderedOutput('A [v1]A [v1]');
await act(() => {
root.render('Bye');
});
assertLog([]);
expect(root).toMatchRenderedOutput('Bye');
});
test('a new Cache boundary uses fresh cache', async () => {
function App({showMore}) {
return (
<Cache>
<Suspense fallback={<Text text="Loading..." />}>
<AsyncText showVersion={true} text="A" />
</Suspense>
{showMore ? (
<Cache>
<Suspense fallback={<Text text="Loading..." />}>
<AsyncText showVersion={true} text="A" />
</Suspense>
</Cache>
) : null}
</Cache>
);
}
const root = ReactNoop.createRoot();
await act(() => {
seedNextTextCache('A');
root.render(<App showMore={false} />);
});
assertLog(['A [v1]']);
expect(root).toMatchRenderedOutput('A [v1]');
await act(() => {
root.render(<App showMore={true} />);
});
assertLog([
'A [v1]',
'Cache miss! [A]',
'Loading...',
]);
expect(root).toMatchRenderedOutput('A [v1]Loading...');
await act(() => {
resolveMostRecentTextCache('A');
});
assertLog(['A [v2]']);
expect(root).toMatchRenderedOutput('A [v1]A [v2]');
await act(() => {
root.render('Bye!');
});
assertLog(['Cache cleanup: A [v2]']);
expect(root).toMatchRenderedOutput('Bye!');
});
test('inner/outer cache boundaries uses the same cache instance on initial render', async () => {
const root = ReactNoop.createRoot();
function App() {
return (
<Cache>
<Suspense fallback={<Text text="Loading shell..." />}>
{/* The shell reads A */}
<Shell>
{/* The inner content reads both A and B */}
<Suspense fallback={<Text text="Loading content..." />}>
<Cache>
<Content />
</Cache>
</Suspense>
</Shell>
</Suspense>
</Cache>
);
}
function Shell({children}) {
readText('A');
return (
<>
<div>
<Text text="Shell" />
</div>
<div>{children}</div>
</>
);
}
function Content() {
readText('A');
readText('B');
return <Text text="Content" />;
}
await act(() => {
root.render(<App />);
});
assertLog(['Cache miss! [A]', 'Loading shell...']);
expect(root).toMatchRenderedOutput('Loading shell...');
await act(() => {
resolveMostRecentTextCache('A');
});
assertLog([
'Shell',
'Cache miss! [B]',
'Loading content...',
]);
expect(root).toMatchRenderedOutput(
<>
<div>Shell</div>
<div>Loading content...</div>
</>,
);
await act(() => {
resolveMostRecentTextCache('B');
});
assertLog(['Content']);
expect(root).toMatchRenderedOutput(
<>
<div>Shell</div>
<div>Content</div>
</>,
);
await act(() => {
root.render('Bye');
});
assertLog([]);
expect(root).toMatchRenderedOutput('Bye');
});
test('inner/ outer cache boundaries added in the same update use the same cache instance', async () => {
const root = ReactNoop.createRoot();
function App({showMore}) {
return showMore ? (
<Cache>
<Suspense fallback={<Text text="Loading shell..." />}>
{/* The shell reads A */}
<Shell>
{/* The inner content reads both A and B */}
<Suspense fallback={<Text text="Loading content..." />}>
<Cache>
<Content />
</Cache>
</Suspense>
</Shell>
</Suspense>
</Cache>
) : (
'(empty)'
);
}
function Shell({children}) {
readText('A');
return (
<>
<div>
<Text text="Shell" />
</div>
<div>{children}</div>
</>
);
}
function Content() {
readText('A');
readText('B');
return <Text text="Content" />;
}
await act(() => {
root.render(<App showMore={false} />);
});
assertLog([]);
expect(root).toMatchRenderedOutput('(empty)');
await act(() => {
root.render(<App showMore={true} />);
});
assertLog(['Cache miss! [A]', 'Loading shell...']);
expect(root).toMatchRenderedOutput('Loading shell...');
await act(() => {
resolveMostRecentTextCache('A');
});
assertLog([
'Shell',
'Cache miss! [B]',
'Loading content...',
]);
expect(root).toMatchRenderedOutput(
<>
<div>Shell</div>
<div>Loading content...</div>
</>,
);
await act(() => {
resolveMostRecentTextCache('B');
});
assertLog(['Content']);
expect(root).toMatchRenderedOutput(
<>
<div>Shell</div>
<div>Content</div>
</>,
);
await act(() => {
root.render('Bye');
});
assertLog(['Cache cleanup: A [v1]', 'Cache cleanup: B [v1]']);
expect(root).toMatchRenderedOutput('Bye');
});
test('refresh a cache boundary', async () => {
let refresh;
function App() {
refresh = useCacheRefresh();
return <AsyncText showVersion={true} text="A" />;
}
const root = ReactNoop.createRoot();
await act(() => {
root.render(
<Suspense fallback={<Text text="Loading..." />}>
<App />
</Suspense>,
);
});
assertLog(['Cache miss! [A]', 'Loading...']);
expect(root).toMatchRenderedOutput('Loading...');
await act(() => {
resolveMostRecentTextCache('A');
});
assertLog(['A [v1]']);
expect(root).toMatchRenderedOutput('A [v1]');
await act(() => {
startTransition(() => refresh());
});
assertLog(['Cache miss! [A]', 'Loading...']);
expect(root).toMatchRenderedOutput('A [v1]');
await act(() => {
resolveMostRecentTextCache('A');
});
if (getCacheSignal) {
assertLog(['A [v2]', 'Cache cleanup: A [v1]']);
} else {
assertLog(['A [v2]']);
}
expect(root).toMatchRenderedOutput('A [v2]');
await act(() => {
root.render('Bye');
});
expect(root).toMatchRenderedOutput('Bye');
});
test('refresh the root cache', async () => {
let refresh;
function App() {
refresh = useCacheRefresh();
return <AsyncText showVersion={true} text="A" />;
}
const root = ReactNoop.createRoot();
await act(() => {
root.render(
<Suspense fallback={<Text text="Loading..." />}>
<App />
</Suspense>,
);
});
assertLog(['Cache miss! [A]', 'Loading...']);
expect(root).toMatchRenderedOutput('Loading...');
await act(() => {
resolveMostRecentTextCache('A');
});
assertLog(['A [v1]']);
expect(root).toMatchRenderedOutput('A [v1]');
await act(() => {
startTransition(() => refresh());
});
assertLog(['Cache miss! [A]', 'Loading...']);
expect(root).toMatchRenderedOutput('A [v1]');
await act(() => {
resolveMostRecentTextCache('A');
});
assertLog(['A [v2]', 'Cache cleanup: A [v1]']);
expect(root).toMatchRenderedOutput('A [v2]');
await act(() => {
root.render('Bye');
});
assertLog([]);
expect(root).toMatchRenderedOutput('Bye');
});
test('refresh the root cache without a transition', async () => {
let refresh;
function App() {
refresh = useCacheRefresh();
return <AsyncText showVersion={true} text="A" />;
}
const root = ReactNoop.createRoot();
await act(() => {
root.render(
<Suspense fallback={<Text text="Loading..." />}>
<App />
</Suspense>,
);
});
assertLog(['Cache miss! [A]', 'Loading...']);
expect(root).toMatchRenderedOutput('Loading...');
await act(() => {
resolveMostRecentTextCache('A');
});
assertLog(['A [v1]']);
expect(root).toMatchRenderedOutput('A [v1]');
await act(() => {
refresh();
});
assertLog([
'Cache miss! [A]',
'Loading...',
'Cache cleanup: A [v1]',
]);
expect(root).toMatchRenderedOutput('Loading...');
await act(() => {
resolveMostRecentTextCache('A');
});
assertLog(['A [v2]']);
expect(root).toMatchRenderedOutput('A [v2]');
await act(() => {
root.render('Bye');
});
assertLog([]);
expect(root).toMatchRenderedOutput('Bye');
});
test('refresh a cache with seed data', async () => {
let refreshWithSeed;
function App() {
const refresh = useCacheRefresh();
const [seed, setSeed] = useState({fn: null});
if (seed.fn) {
seed.fn();
seed.fn = null;
}
refreshWithSeed = fn => {
setSeed({fn});
refresh();
};
return <AsyncText showVersion={true} text="A" />;
}
const root = ReactNoop.createRoot();
await act(() => {
root.render(
<Cache>
<Suspense fallback={<Text text="Loading..." />}>
<App />
</Suspense>
</Cache>,
);
});
assertLog(['Cache miss! [A]', 'Loading...']);
expect(root).toMatchRenderedOutput('Loading...');
await act(() => {
resolveMostRecentTextCache('A');
});
assertLog(['A [v1]']);
expect(root).toMatchRenderedOutput('A [v1]');
await act(() => {
startTransition(() =>
refreshWithSeed(() => {
const textCache = getTextCache();
textCache.resolve('A');
}),
);
});
assertLog(['A [v2]']);
expect(root).toMatchRenderedOutput('A [v2]');
await act(() => {
root.render('Bye');
});
assertLog(['Cache cleanup: A [v2]']);
expect(root).toMatchRenderedOutput('Bye');
});
test('refreshing a parent cache also refreshes its children', async () => {
let refreshShell;
function RefreshShell() {
refreshShell = useCacheRefresh();
return null;
}
function App({showMore}) {
return (
<Cache>
<RefreshShell />
<Suspense fallback={<Text text="Loading..." />}>
<AsyncText showVersion={true} text="A" />
</Suspense>
{showMore ? (
<Cache>
<Suspense fallback={<Text text="Loading..." />}>
<AsyncText showVersion={true} text="A" />
</Suspense>
</Cache>
) : null}
</Cache>
);
}
const root = ReactNoop.createRoot();
await act(() => {
seedNextTextCache('A');
root.render(<App showMore={false} />);
});
assertLog(['A [v1]']);
expect(root).toMatchRenderedOutput('A [v1]');
await act(() => {
seedNextTextCache('A');
root.render(<App showMore={true} />);
});
assertLog([
'A [v1]',
'A [v2]',
]);
expect(root).toMatchRenderedOutput('A [v1]A [v2]');
await act(() => {
startTransition(() => refreshShell());
});
assertLog(['Cache miss! [A]', 'Loading...', 'Loading...']);
expect(root).toMatchRenderedOutput('A [v1]A [v2]');
await act(() => {
resolveMostRecentTextCache('A');
});
assertLog([
'A [v3]',
'A [v3]',
'Cache cleanup: A [v2]',
]);
expect(root).toMatchRenderedOutput('A [v3]A [v3]');
await act(() => {
root.render('Bye!');
});
assertLog(['Cache cleanup: A [v3]']);
expect(root).toMatchRenderedOutput('Bye!');
});
test(
'refreshing a cache boundary does not refresh the other boundaries ' +
'that mounted at the same time (i.e. the ones that share the same cache)',
async () => {
let refreshFirstBoundary;
function RefreshFirstBoundary() {
refreshFirstBoundary = useCacheRefresh();
return null;
}
function App({showMore}) {
return showMore ? (
<>
<Cache>
<Suspense fallback={<Text text="Loading..." />}>
<RefreshFirstBoundary />
<AsyncText showVersion={true} text="A" />
</Suspense>
</Cache>
<Cache>
<Suspense fallback={<Text text="Loading..." />}>
<AsyncText showVersion={true} text="A" />
</Suspense>
</Cache>
</>
) : null;
}
const root = ReactNoop.createRoot();
await act(() => {
root.render(<App showMore={false} />);
});
await act(() => {
root.render(<App showMore={true} />);
});
assertLog(['Cache miss! [A]', 'Loading...', 'Loading...']);
expect(root).toMatchRenderedOutput('Loading...Loading...');
await act(() => {
resolveMostRecentTextCache('A');
});
assertLog(['A [v1]', 'A [v1]']);
expect(root).toMatchRenderedOutput('A [v1]A [v1]');
await act(async () => {
await refreshFirstBoundary();
});
assertLog(['Cache miss! [A]', 'Loading...']);
await act(() => {
resolveMostRecentTextCache('A');
});
assertLog(['A [v2]']);
expect(root).toMatchRenderedOutput('A [v2]A [v1]');
await act(() => {
root.render('Bye!');
});
assertLog(['Cache cleanup: A [v2]', 'Cache cleanup: A [v1]']);
expect(root).toMatchRenderedOutput('Bye!');
},
);
test(
'mount a new Cache boundary in a sibling while simultaneously ' +
'resolving a Suspense boundary',
async () => {
function App({showMore}) {
return (
<>
{showMore ? (
<Suspense fallback={<Text text="Loading..." />}>
<Cache>
<AsyncText showVersion={true} text="A" />
</Cache>
</Suspense>
) : null}
<Suspense fallback={<Text text="Loading..." />}>
<Cache>
{' '}
<AsyncText showVersion={true} text="A" />{' '}
<AsyncText showVersion={true} text="B" />
</Cache>
</Suspense>
</>
);
}
const root = ReactNoop.createRoot();
await act(() => {
root.render(<App showMore={false} />);
});
assertLog(['Cache miss! [A]', 'Cache miss! [B]', 'Loading...']);
expect(root).toMatchRenderedOutput('Loading...');
await act(() => {
resolveMostRecentTextCache('A');
resolveMostRecentTextCache('B');
root.render(<App showMore={true} />);
});
assertLog([
'Cache miss! [A]',
'Loading...',
'A [v1]',
'B [v1]',
]);
expect(root).toMatchRenderedOutput('Loading... A [v1] B [v1]');
await act(() => {
resolveMostRecentTextCache('A');
});
assertLog(['A [v2]']);
expect(root).toMatchRenderedOutput('A [v2] A [v1] B [v1]');
await act(() => {
root.render('Bye!');
});
assertLog(['Cache cleanup: A [v2]']);
expect(root).toMatchRenderedOutput('Bye!');
},
);
test('cache pool is cleared once transitions that depend on it commit their shell', async () => {
function Child({text}) {
return (
<Cache>
<AsyncText showVersion={true} text={text} />
</Cache>
);
}
const root = ReactNoop.createRoot();
await act(() => {
root.render(
<Suspense fallback={<Text text="Loading..." />}>(empty)</Suspense>,
);
});
assertLog([]);
expect(root).toMatchRenderedOutput('(empty)');
await act(() => {
startTransition(() => {
root.render(
<Suspense fallback={<Text text="Loading..." />}>
<Child text="A" />
</Suspense>,
);
});
});
assertLog(['Cache miss! [A]', 'Loading...']);
expect(root).toMatchRenderedOutput('(empty)');
await act(() => {
startTransition(() => {
root.render(
<Suspense fallback={<Text text="Loading..." />}>
<Child text="A" />
<Child text="A" />
</Suspense>,
);
});
});
assertLog([
'Loading...',
]);
expect(root).toMatchRenderedOutput('(empty)');
await act(() => {
resolveMostRecentTextCache('A');
});
assertLog(['A [v1]', 'A [v1]']);
expect(root).toMatchRenderedOutput('A [v1]A [v1]');
await act(() => {
startTransition(() => {
root.render(
<Suspense fallback={<Text text="Loading..." />}>
<Child text="A" />
<Child text="A" />
<Child text="A" />
</Suspense>,
);
});
});
assertLog([
'A [v1]',
'A [v1]',
'Cache miss! [A]',
'Loading...',
]);
expect(root).toMatchRenderedOutput('A [v1]A [v1]');
await act(() => {
resolveMostRecentTextCache('A');
});
assertLog(['A [v1]', 'A [v1]', 'A [v2]']);
expect(root).toMatchRenderedOutput('A [v1]A [v1]A [v2]');
await act(() => {
root.render('Bye!');
});
assertLog(['Cache cleanup: A [v1]', 'Cache cleanup: A [v2]']);
expect(root).toMatchRenderedOutput('Bye!');
});
test('cache pool is not cleared by arbitrary commits', async () => {
function App() {
return (
<>
<ShowMore />
<Unrelated />
</>
);
}
let showMore;
function ShowMore() {
const [shouldShow, _showMore] = useState(false);
showMore = () => _showMore(true);
return (
<>
<Suspense fallback={<Text text="Loading..." />}>
{shouldShow ? (
<Cache>
<AsyncText showVersion={true} text="A" />
</Cache>
) : null}
</Suspense>
</>
);
}
let updateUnrelated;
function Unrelated() {
const [count, _updateUnrelated] = useState(0);
updateUnrelated = _updateUnrelated;
return <Text text={String(count)} />;
}
const root = ReactNoop.createRoot();
await act(() => {
root.render(<App />);
});
assertLog(['0']);
expect(root).toMatchRenderedOutput('0');
await act(() => {
startTransition(() => {
showMore();
});
});
assertLog(['Cache miss! [A]', 'Loading...']);
expect(root).toMatchRenderedOutput('0');
await act(() => {
updateUnrelated(1);
});
assertLog([
'1',
'Loading...',
]);
expect(root).toMatchRenderedOutput('1');
await act(() => {
resolveMostRecentTextCache('A');
});
assertLog(['A [v1]']);
expect(root).toMatchRenderedOutput('A [v1]1');
await act(() => {
root.render('Bye!');
});
assertLog(['Cache cleanup: A [v1]']);
expect(root).toMatchRenderedOutput('Bye!');
});
test('cache boundary uses a fresh cache when its key changes', async () => {
const root = ReactNoop.createRoot();
seedNextTextCache('A');
await act(() => {
root.render(
<Suspense fallback="Loading...">
<Cache key="A">
<AsyncText showVersion={true} text="A" />
</Cache>
</Suspense>,
);
});
assertLog(['A [v1]']);
expect(root).toMatchRenderedOutput('A [v1]');
seedNextTextCache('B');
await act(() => {
root.render(
<Suspense fallback="Loading...">
<Cache key="B">
<AsyncText showVersion={true} text="B" />
</Cache>
</Suspense>,
);
});
assertLog(['B [v2]']);
expect(root).toMatchRenderedOutput('B [v2]');
await act(() => {
root.render('Bye!');
});
assertLog(['Cache cleanup: B [v2]']);
expect(root).toMatchRenderedOutput('Bye!');
});
test('overlapping transitions after an initial mount use the same fresh cache', async () => {
const root = ReactNoop.createRoot();
await act(() => {
root.render(
<Suspense fallback="Loading...">
<Cache key="A">
<AsyncText showVersion={true} text="A" />
</Cache>
</Suspense>,
);
});
assertLog(['Cache miss! [A]']);
expect(root).toMatchRenderedOutput('Loading...');
await act(() => {
resolveMostRecentTextCache('A');
});
assertLog(['A [v1]']);
expect(root).toMatchRenderedOutput('A [v1]');
await act(() => {
startTransition(() => {
root.render(
<Suspense fallback="Loading...">
<Cache key="B">
<AsyncText showVersion={true} text="B" />
</Cache>
</Suspense>,
);
});
});
assertLog(['Cache miss! [B]']);
expect(root).toMatchRenderedOutput('A [v1]');
await act(() => {
startTransition(() => {
root.render(
<Suspense fallback="Loading...">
<Cache key="C">
<AsyncText showVersion={true} text="C" />
</Cache>
</Suspense>,
);
});
});
assertLog(['Cache miss! [C]']);
expect(root).toMatchRenderedOutput('A [v1]');
await act(() => {
resolveMostRecentTextCache('C');
});
assertLog(['C [v2]']);
expect(root).toMatchRenderedOutput('C [v2]');
await act(() => {
root.render('Bye!');
});
assertLog(['Cache cleanup: B [v2]', 'Cache cleanup: C [v2]']);
expect(root).toMatchRenderedOutput('Bye!');
});
test('overlapping updates after an initial mount use the same fresh cache', async () => {
const root = ReactNoop.createRoot();
await act(() => {
root.render(
<Suspense fallback="Loading...">
<Cache key="A">
<AsyncText showVersion={true} text="A" />
</Cache>
</Suspense>,
);
});
assertLog(['Cache miss! [A]']);
expect(root).toMatchRenderedOutput('Loading...');
await act(() => {
resolveMostRecentTextCache('A');
});
assertLog(['A [v1]']);
expect(root).toMatchRenderedOutput('A [v1]');
await act(() => {
root.render(
<Suspense fallback="Loading...">
<Cache key="B">
<AsyncText showVersion={true} text="B" />
</Cache>
</Suspense>,
);
});
assertLog(['Cache miss! [B]']);
expect(root).toMatchRenderedOutput('Loading...');
await act(() => {
root.render(
<Suspense fallback="Loading...">
<Cache key="C">
<AsyncText showVersion={true} text="C" />
</Cache>
</Suspense>,
);
});
assertLog(['Cache miss! [C]']);
expect(root).toMatchRenderedOutput('Loading...');
await act(() => {
resolveMostRecentTextCache('C');
});
assertLog(['C [v2]']);
expect(root).toMatchRenderedOutput('C [v2]');
await act(() => {
root.render('Bye!');
});
assertLog(['Cache cleanup: B [v2]', 'Cache cleanup: C [v2]']);
expect(root).toMatchRenderedOutput('Bye!');
});
test('cleans up cache only used in an aborted transition', async () => {
const root = ReactNoop.createRoot();
seedNextTextCache('A');
await act(() => {
root.render(
<Suspense fallback="Loading...">
<Cache key="A">
<AsyncText showVersion={true} text="A" />
</Cache>
</Suspense>,
);
});
assertLog(['A [v1]']);
expect(root).toMatchRenderedOutput('A [v1]');
await act(() => {
startTransition(() => {
root.render(
<Suspense fallback="Loading...">
<Cache key="B">
<AsyncText showVersion={true} text="B" />
</Cache>
</Suspense>,
);
});
});
assertLog(['Cache miss! [B]']);
expect(root).toMatchRenderedOutput('A [v1]');
await act(() => {
startTransition(() => {
root.render(
<Suspense fallback="Loading...">
<Cache key="A">
<AsyncText showVersion={true} text="A" />
</Cache>
</Suspense>,
);
});
});
assertLog(['A [v1]', 'Cache cleanup: B [v2]']);
expect(root).toMatchRenderedOutput('A [v1]');
await act(() => {
root.render('Bye!');
});
assertLog([]);
expect(root).toMatchRenderedOutput('Bye!');
});
test.skip('if a root cache refresh never commits its fresh cache is released', async () => {
const root = ReactNoop.createRoot();
let refresh;
function Example({text}) {
refresh = useCacheRefresh();
return <AsyncText showVersion={true} text={text} />;
}
seedNextTextCache('A');
await act(() => {
root.render(
<Suspense fallback="Loading...">
<Example text="A" />
</Suspense>,
);
});
assertLog(['A [v1]']);
expect(root).toMatchRenderedOutput('A [v1]');
await act(() => {
startTransition(() => {
refresh();
});
});
assertLog(['Cache miss! [A]']);
expect(root).toMatchRenderedOutput('A [v1]');
await act(() => {
root.render('Bye!');
});
assertLog([
'Cache cleanup: A [v2]',
]);
expect(root).toMatchRenderedOutput('Bye!');
});
test.skip('if a cache boundary refresh never commits its fresh cache is released', async () => {
const root = ReactNoop.createRoot();
let refresh;
function Example({text}) {
refresh = useCacheRefresh();
return <AsyncText showVersion={true} text={text} />;
}
seedNextTextCache('A');
await act(() => {
root.render(
<Suspense fallback="Loading...">
<Cache>
<Example text="A" />
</Cache>
</Suspense>,
);
});
assertLog(['A [v1]']);
expect(root).toMatchRenderedOutput('A [v1]');
await act(() => {
startTransition(() => {
refresh();
});
});
assertLog(['Cache miss! [A]']);
expect(root).toMatchRenderedOutput('A [v1]');
await act(() => {
root.render('Bye!');
});
assertLog([
'Cache cleanup: A [v2]',
]);
expect(root).toMatchRenderedOutput('Bye!');
});
test('prerender a new cache boundary inside an Offscreen tree', async () => {
function App({prerenderMore}) {
return (
<Offscreen mode="hidden">
<div>
{prerenderMore ? (
<Cache>
<AsyncText text="More" />
</Cache>
) : null}
</div>
</Offscreen>
);
}
const root = ReactNoop.createRoot();
await act(() => {
root.render(<App prerenderMore={false} />);
});
assertLog([]);
expect(root).toMatchRenderedOutput(<div hidden={true} />);
seedNextTextCache('More');
await act(() => {
root.render(<App prerenderMore={true} />);
});
assertLog(['More']);
expect(root).toMatchRenderedOutput(<div hidden={true}>More</div>);
});
it('cache objects and primitive arguments and a mix of them', async () => {
const root = ReactNoop.createRoot();
const types = cache((a, b) => ({a: typeof a, b: typeof b}));
function Print({a, b}) {
return types(a, b).a + ' ' + types(a, b).b + ' ';
}
function Same({a, b}) {
const x = types(a, b);
const y = types(a, b);
return (x === y).toString() + ' ';
}
function FlippedOrder({a, b}) {
return (types(a, b) === types(b, a)).toString() + ' ';
}
function FewerArgs({a, b}) {
return (types(a, b) === types(a)).toString() + ' ';
}
function MoreArgs({a, b}) {
return (types(a) === types(a, b)).toString() + ' ';
}
await act(() => {
root.render(
<>
<Print a="e" b="f" />
<Same a="a" b="b" />
<FlippedOrder a="c" b="d" />
<FewerArgs a="e" b="f" />
<MoreArgs a="g" b="h" />
</>,
);
});
expect(root).toMatchRenderedOutput('string string true false false false ');
await act(() => {
root.render(
<>
<Print a="e" b={null} />
<Same a="a" b={null} />
<FlippedOrder a="c" b={null} />
<FewerArgs a="e" b={null} />
<MoreArgs a="g" b={null} />
</>,
);
});
expect(root).toMatchRenderedOutput('string object true false false false ');
const obj = {};
await act(() => {
root.render(
<>
<Print a="e" b={obj} />
<Same a="a" b={obj} />
<FlippedOrder a="c" b={obj} />
<FewerArgs a="e" b={obj} />
<MoreArgs a="g" b={obj} />
</>,
);
});
expect(root).toMatchRenderedOutput('string object true false false false ');
const sameObj = {};
await act(() => {
root.render(
<>
<Print a={sameObj} b={sameObj} />
<Same a={sameObj} b={sameObj} />
<FlippedOrder a={sameObj} b={sameObj} />
<FewerArgs a={sameObj} b={sameObj} />
<MoreArgs a={sameObj} b={sameObj} />
</>,
);
});
expect(root).toMatchRenderedOutput('object object true true false false ');
const objA = {};
const objB = {};
await act(() => {
root.render(
<>
<Print a={objA} b={objB} />
<Same a={objA} b={objB} />
<FlippedOrder a={objA} b={objB} />
<FewerArgs a={objA} b={objB} />
<MoreArgs a={objA} b={objB} />
</>,
);
});
expect(root).toMatchRenderedOutput('object object true false false false ');
const sameSymbol = Symbol();
await act(() => {
root.render(
<>
<Print a={sameSymbol} b={sameSymbol} />
<Same a={sameSymbol} b={sameSymbol} />
<FlippedOrder a={sameSymbol} b={sameSymbol} />
<FewerArgs a={sameSymbol} b={sameSymbol} />
<MoreArgs a={sameSymbol} b={sameSymbol} />
</>,
);
});
expect(root).toMatchRenderedOutput('symbol symbol true true false false ');
const notANumber = +'nan';
await act(() => {
root.render(
<>
<Print a={1} b={notANumber} />
<Same a={1} b={notANumber} />
<FlippedOrder a={1} b={notANumber} />
<FewerArgs a={1} b={notANumber} />
<MoreArgs a={1} b={notANumber} />
</>,
);
});
expect(root).toMatchRenderedOutput('number number true false false false ');
});
it('cached functions that throw should cache the error', async () => {
const root = ReactNoop.createRoot();
const throws = cache(v => {
throw new Error(v);
});
let x;
let y;
let z;
function Test() {
try {
throws(1);
} catch (e) {
x = e;
}
try {
throws(1);
} catch (e) {
y = e;
}
try {
throws(2);
} catch (e) {
z = e;
}
return 'Blank';
}
await act(() => {
root.render(<Test />);
});
expect(x).toBe(y);
expect(z).not.toBe(x);
});
});