Skip to content

Commit 8e2bde6

Browse files
authored
Add cache() API (#25506)
Like memo() but longer lived.
1 parent 9cdf8a9 commit 8e2bde6

8 files changed

+356
-60
lines changed

packages/react-reconciler/src/__tests__/ReactCache-test.js

+223-60
Original file line numberDiff line numberDiff line change
@@ -2,16 +2,17 @@ let React;
22
let ReactNoop;
33
let Cache;
44
let getCacheSignal;
5-
let getCacheForType;
65
let Scheduler;
76
let act;
87
let Suspense;
98
let Offscreen;
109
let useCacheRefresh;
1110
let startTransition;
1211
let useState;
12+
let cache;
1313

14-
let caches;
14+
let getTextCache;
15+
let textCaches;
1516
let seededCache;
1617

1718
describe('ReactCache', () => {
@@ -24,66 +25,68 @@ describe('ReactCache', () => {
2425
Scheduler = require('scheduler');
2526
act = require('jest-react').act;
2627
Suspense = React.Suspense;
28+
cache = React.experimental_cache;
2729
Offscreen = React.unstable_Offscreen;
2830
getCacheSignal = React.unstable_getCacheSignal;
29-
getCacheForType = React.unstable_getCacheForType;
3031
useCacheRefresh = React.unstable_useCacheRefresh;
3132
startTransition = React.startTransition;
3233
useState = React.useState;
3334

34-
caches = [];
35+
textCaches = [];
3536
seededCache = null;
36-
});
37-
38-
function createTextCache() {
39-
if (seededCache !== null) {
40-
// Trick to seed a cache before it exists.
41-
// TODO: Need a built-in API to seed data before the initial render (i.e.
42-
// not a refresh because nothing has mounted yet).
43-
const cache = seededCache;
44-
seededCache = null;
45-
return cache;
46-
}
4737

48-
const data = new Map();
49-
const version = caches.length + 1;
50-
const cache = {
51-
version,
52-
data,
53-
resolve(text) {
54-
const record = data.get(text);
55-
if (record === undefined) {
56-
const newRecord = {
57-
status: 'resolved',
58-
value: text,
59-
cleanupScheduled: false,
60-
};
61-
data.set(text, newRecord);
62-
} else if (record.status === 'pending') {
63-
record.value.resolve();
38+
if (gate(flags => flags.enableCache)) {
39+
getTextCache = cache(() => {
40+
if (seededCache !== null) {
41+
// Trick to seed a cache before it exists.
42+
// TODO: Need a built-in API to seed data before the initial render (i.e.
43+
// not a refresh because nothing has mounted yet).
44+
const textCache = seededCache;
45+
seededCache = null;
46+
return textCache;
6447
}
65-
},
66-
reject(text, error) {
67-
const record = data.get(text);
68-
if (record === undefined) {
69-
const newRecord = {
70-
status: 'rejected',
71-
value: error,
72-
cleanupScheduled: false,
73-
};
74-
data.set(text, newRecord);
75-
} else if (record.status === 'pending') {
76-
record.value.reject();
77-
}
78-
},
79-
};
80-
caches.push(cache);
81-
return cache;
82-
}
48+
49+
const data = new Map();
50+
const version = textCaches.length + 1;
51+
const textCache = {
52+
version,
53+
data,
54+
resolve(text) {
55+
const record = data.get(text);
56+
if (record === undefined) {
57+
const newRecord = {
58+
status: 'resolved',
59+
value: text,
60+
cleanupScheduled: false,
61+
};
62+
data.set(text, newRecord);
63+
} else if (record.status === 'pending') {
64+
record.value.resolve();
65+
}
66+
},
67+
reject(text, error) {
68+
const record = data.get(text);
69+
if (record === undefined) {
70+
const newRecord = {
71+
status: 'rejected',
72+
value: error,
73+
cleanupScheduled: false,
74+
};
75+
data.set(text, newRecord);
76+
} else if (record.status === 'pending') {
77+
record.value.reject();
78+
}
79+
},
80+
};
81+
textCaches.push(textCache);
82+
return textCache;
83+
});
84+
}
85+
});
8386

8487
function readText(text) {
8588
const signal = getCacheSignal();
86-
const textCache = getCacheForType(createTextCache);
89+
const textCache = getTextCache();
8790
const record = textCache.data.get(text);
8891
if (record !== undefined) {
8992
if (!record.cleanupScheduled) {
@@ -160,18 +163,18 @@ describe('ReactCache', () => {
160163

161164
function seedNextTextCache(text) {
162165
if (seededCache === null) {
163-
seededCache = createTextCache();
166+
seededCache = getTextCache();
164167
}
165168
seededCache.resolve(text);
166169
}
167170

168171
function resolveMostRecentTextCache(text) {
169-
if (caches.length === 0) {
172+
if (textCaches.length === 0) {
170173
throw Error('Cache does not exist.');
171174
} else {
172175
// Resolve the most recently created cache. An older cache can by
173-
// resolved with `caches[index].resolve(text)`.
174-
caches[caches.length - 1].resolve(text);
176+
// resolved with `textCaches[index].resolve(text)`.
177+
textCaches[textCaches.length - 1].resolve(text);
175178
}
176179
}
177180

@@ -815,9 +818,18 @@ describe('ReactCache', () => {
815818

816819
// @gate experimental || www
817820
test('refresh a cache with seed data', async () => {
818-
let refresh;
821+
let refreshWithSeed;
819822
function App() {
820-
refresh = useCacheRefresh();
823+
const refresh = useCacheRefresh();
824+
const [seed, setSeed] = useState({fn: null});
825+
if (seed.fn) {
826+
seed.fn();
827+
seed.fn = null;
828+
}
829+
refreshWithSeed = fn => {
830+
setSeed({fn});
831+
refresh();
832+
};
821833
return <AsyncText showVersion={true} text="A" />;
822834
}
823835

@@ -845,11 +857,14 @@ describe('ReactCache', () => {
845857
await act(async () => {
846858
// Refresh the cache with seeded data, like you would receive from a
847859
// server mutation.
848-
// TODO: Seeding multiple typed caches. Should work by calling `refresh`
860+
// TODO: Seeding multiple typed textCaches. Should work by calling `refresh`
849861
// multiple times with different key/value pairs
850-
const cache = createTextCache();
851-
cache.resolve('A');
852-
startTransition(() => refresh(createTextCache, cache));
862+
startTransition(() =>
863+
refreshWithSeed(() => {
864+
const textCache = getTextCache();
865+
textCache.resolve('A');
866+
}),
867+
);
853868
});
854869
// The root should re-render without a cache miss.
855870
// The cache is not cleared up yet, since it's still reference by the root
@@ -1624,4 +1639,152 @@ describe('ReactCache', () => {
16241639
expect(Scheduler).toHaveYielded(['More']);
16251640
expect(root).toMatchRenderedOutput(<div hidden={true}>More</div>);
16261641
});
1642+
1643+
// @gate enableCache
1644+
it('cache objects and primitive arguments and a mix of them', async () => {
1645+
const root = ReactNoop.createRoot();
1646+
const types = cache((a, b) => ({a: typeof a, b: typeof b}));
1647+
function Print({a, b}) {
1648+
return types(a, b).a + ' ' + types(a, b).b + ' ';
1649+
}
1650+
function Same({a, b}) {
1651+
const x = types(a, b);
1652+
const y = types(a, b);
1653+
return (x === y).toString() + ' ';
1654+
}
1655+
function FlippedOrder({a, b}) {
1656+
return (types(a, b) === types(b, a)).toString() + ' ';
1657+
}
1658+
function FewerArgs({a, b}) {
1659+
return (types(a, b) === types(a)).toString() + ' ';
1660+
}
1661+
function MoreArgs({a, b}) {
1662+
return (types(a) === types(a, b)).toString() + ' ';
1663+
}
1664+
await act(async () => {
1665+
root.render(
1666+
<>
1667+
<Print a="e" b="f" />
1668+
<Same a="a" b="b" />
1669+
<FlippedOrder a="c" b="d" />
1670+
<FewerArgs a="e" b="f" />
1671+
<MoreArgs a="g" b="h" />
1672+
</>,
1673+
);
1674+
});
1675+
expect(root).toMatchRenderedOutput('string string true false false false ');
1676+
await act(async () => {
1677+
root.render(
1678+
<>
1679+
<Print a="e" b={null} />
1680+
<Same a="a" b={null} />
1681+
<FlippedOrder a="c" b={null} />
1682+
<FewerArgs a="e" b={null} />
1683+
<MoreArgs a="g" b={null} />
1684+
</>,
1685+
);
1686+
});
1687+
expect(root).toMatchRenderedOutput('string object true false false false ');
1688+
const obj = {};
1689+
await act(async () => {
1690+
root.render(
1691+
<>
1692+
<Print a="e" b={obj} />
1693+
<Same a="a" b={obj} />
1694+
<FlippedOrder a="c" b={obj} />
1695+
<FewerArgs a="e" b={obj} />
1696+
<MoreArgs a="g" b={obj} />
1697+
</>,
1698+
);
1699+
});
1700+
expect(root).toMatchRenderedOutput('string object true false false false ');
1701+
const sameObj = {};
1702+
await act(async () => {
1703+
root.render(
1704+
<>
1705+
<Print a={sameObj} b={sameObj} />
1706+
<Same a={sameObj} b={sameObj} />
1707+
<FlippedOrder a={sameObj} b={sameObj} />
1708+
<FewerArgs a={sameObj} b={sameObj} />
1709+
<MoreArgs a={sameObj} b={sameObj} />
1710+
</>,
1711+
);
1712+
});
1713+
expect(root).toMatchRenderedOutput('object object true true false false ');
1714+
const objA = {};
1715+
const objB = {};
1716+
await act(async () => {
1717+
root.render(
1718+
<>
1719+
<Print a={objA} b={objB} />
1720+
<Same a={objA} b={objB} />
1721+
<FlippedOrder a={objA} b={objB} />
1722+
<FewerArgs a={objA} b={objB} />
1723+
<MoreArgs a={objA} b={objB} />
1724+
</>,
1725+
);
1726+
});
1727+
expect(root).toMatchRenderedOutput('object object true false false false ');
1728+
const sameSymbol = Symbol();
1729+
await act(async () => {
1730+
root.render(
1731+
<>
1732+
<Print a={sameSymbol} b={sameSymbol} />
1733+
<Same a={sameSymbol} b={sameSymbol} />
1734+
<FlippedOrder a={sameSymbol} b={sameSymbol} />
1735+
<FewerArgs a={sameSymbol} b={sameSymbol} />
1736+
<MoreArgs a={sameSymbol} b={sameSymbol} />
1737+
</>,
1738+
);
1739+
});
1740+
expect(root).toMatchRenderedOutput('symbol symbol true true false false ');
1741+
const notANumber = +'nan';
1742+
await act(async () => {
1743+
root.render(
1744+
<>
1745+
<Print a={1} b={notANumber} />
1746+
<Same a={1} b={notANumber} />
1747+
<FlippedOrder a={1} b={notANumber} />
1748+
<FewerArgs a={1} b={notANumber} />
1749+
<MoreArgs a={1} b={notANumber} />
1750+
</>,
1751+
);
1752+
});
1753+
expect(root).toMatchRenderedOutput('number number true false false false ');
1754+
});
1755+
1756+
// @gate enableCache
1757+
it('cached functions that throw should cache the error', async () => {
1758+
const root = ReactNoop.createRoot();
1759+
const throws = cache(v => {
1760+
throw new Error(v);
1761+
});
1762+
let x;
1763+
let y;
1764+
let z;
1765+
function Test() {
1766+
try {
1767+
throws(1);
1768+
} catch (e) {
1769+
x = e;
1770+
}
1771+
try {
1772+
throws(1);
1773+
} catch (e) {
1774+
y = e;
1775+
}
1776+
try {
1777+
throws(2);
1778+
} catch (e) {
1779+
z = e;
1780+
}
1781+
1782+
return 'Blank';
1783+
}
1784+
await act(async () => {
1785+
root.render(<Test />);
1786+
});
1787+
expect(x).toBe(y);
1788+
expect(z).not.toBe(x);
1789+
});
16271790
});

packages/react/index.classic.fb.js

+1
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@ export {
3232
isValidElement,
3333
lazy,
3434
memo,
35+
experimental_cache,
3536
startTransition,
3637
startTransition as unstable_startTransition, // TODO: Remove once call sights updated to startTransition
3738
unstable_Cache,

packages/react/index.experimental.js

+1
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ export {
2929
isValidElement,
3030
lazy,
3131
memo,
32+
experimental_cache,
3233
startTransition,
3334
unstable_Cache,
3435
unstable_DebugTracingMode,

packages/react/index.js

+1
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,7 @@ export {
5454
isValidElement,
5555
lazy,
5656
memo,
57+
experimental_cache,
5758
startTransition,
5859
unstable_Cache,
5960
unstable_DebugTracingMode,

packages/react/index.modern.fb.js

+1
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@ export {
3131
isValidElement,
3232
lazy,
3333
memo,
34+
experimental_cache,
3435
startTransition,
3536
startTransition as unstable_startTransition, // TODO: Remove once call sights updated to startTransition
3637
unstable_Cache,

0 commit comments

Comments
 (0)