Skip to content

Commit dc77c77

Browse files
committed
Add cache() API
Like memo() but longer lived.
1 parent 71f2c8c commit dc77c77

File tree

8 files changed

+354
-60
lines changed

8 files changed

+354
-60
lines changed

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

Lines changed: 221 additions & 60 deletions
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,66 @@ 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-
});
3737

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-
}
38+
getTextCache = cache(() => {
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 textCache = seededCache;
44+
seededCache = null;
45+
return textCache;
46+
}
4747

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();
64-
}
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+
const data = new Map();
49+
const version = textCaches.length + 1;
50+
const textCache = {
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();
64+
}
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+
textCaches.push(textCache);
81+
return textCache;
82+
});
83+
});
8384

8485
function readText(text) {
8586
const signal = getCacheSignal();
86-
const textCache = getCacheForType(createTextCache);
87+
const textCache = getTextCache();
8788
const record = textCache.data.get(text);
8889
if (record !== undefined) {
8990
if (!record.cleanupScheduled) {
@@ -160,18 +161,18 @@ describe('ReactCache', () => {
160161

161162
function seedNextTextCache(text) {
162163
if (seededCache === null) {
163-
seededCache = createTextCache();
164+
seededCache = getTextCache();
164165
}
165166
seededCache.resolve(text);
166167
}
167168

168169
function resolveMostRecentTextCache(text) {
169-
if (caches.length === 0) {
170+
if (textCaches.length === 0) {
170171
throw Error('Cache does not exist.');
171172
} else {
172173
// 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);
174+
// resolved with `textCaches[index].resolve(text)`.
175+
textCaches[textCaches.length - 1].resolve(text);
175176
}
176177
}
177178

@@ -815,9 +816,18 @@ describe('ReactCache', () => {
815816

816817
// @gate experimental || www
817818
test('refresh a cache with seed data', async () => {
818-
let refresh;
819+
let refreshWithSeed;
819820
function App() {
820-
refresh = useCacheRefresh();
821+
const refresh = useCacheRefresh();
822+
const [seed, setSeed] = useState({fn: null});
823+
if (seed.fn) {
824+
seed.fn();
825+
seed.fn = null;
826+
}
827+
refreshWithSeed = fn => {
828+
setSeed({fn});
829+
refresh();
830+
};
821831
return <AsyncText showVersion={true} text="A" />;
822832
}
823833

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

packages/react/index.classic.fb.js

Lines changed: 1 addition & 0 deletions
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

Lines changed: 1 addition & 0 deletions
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

Lines changed: 1 addition & 0 deletions
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

Lines changed: 1 addition & 0 deletions
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,

packages/react/src/React.js

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@ import {createContext} from './ReactContext';
3535
import {lazy} from './ReactLazy';
3636
import {forwardRef} from './ReactForwardRef';
3737
import {memo} from './ReactMemo';
38+
import {cache} from './ReactCache';
3839
import {
3940
getCacheSignal,
4041
getCacheForType,
@@ -100,6 +101,7 @@ export {
100101
forwardRef,
101102
lazy,
102103
memo,
104+
cache as experimental_cache,
103105
useCallback,
104106
useContext,
105107
useEffect,

0 commit comments

Comments
 (0)