let React;
let ReactNoop;
let Cache;
let getCacheSignal;
let getCacheForType;
let Scheduler;
let assertLog;
let act;
let Suspense;
let Activity;
let useCacheRefresh;
let startTransition;
let useState;
let textCaches;
let seededCache;
describe('ReactCacheElement', () => {
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;
Activity = React.unstable_Activity;
getCacheSignal = React.unstable_getCacheSignal;
getCacheForType = React.unstable_getCacheForType;
useCacheRefresh = React.unstable_useCacheRefresh;
startTransition = React.startTransition;
useState = React.useState;
const InternalTestUtils = require('internal-test-utils');
assertLog = InternalTestUtils.assertLog;
textCaches = [];
seededCache = null;
});
function createTextCache() {
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 = getCacheForType(createTextCache);
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 = createTextCache();
}
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 refresh;
function App() {
refresh = useCacheRefresh();
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(() => {
const textCache = createTextCache();
textCache.resolve('A');
startTransition(() => refresh(createTextCache, textCache));
});
});
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]', '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 Activity tree', async () => {
function App({prerenderMore}) {
return (
<Activity mode="hidden">
<div>
{prerenderMore ? (
<Cache>
<AsyncText text="More" />
</Cache>
) : null}
</div>
</Activity>
);
}
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>);
});
});