diff --git a/packages/react-reconciler/src/__tests__/ReactCache-test.js b/packages/react-reconciler/src/__tests__/ReactCache-test.js
index 970bf940f27cd..6bf494b1e0ad9 100644
--- a/packages/react-reconciler/src/__tests__/ReactCache-test.js
+++ b/packages/react-reconciler/src/__tests__/ReactCache-test.js
@@ -2,7 +2,6 @@ let React;
let ReactNoop;
let Cache;
let getCacheSignal;
-let getCacheForType;
let Scheduler;
let act;
let Suspense;
@@ -10,8 +9,10 @@ let Offscreen;
let useCacheRefresh;
let startTransition;
let useState;
+let cache;
-let caches;
+let getTextCache;
+let textCaches;
let seededCache;
describe('ReactCache', () => {
@@ -24,66 +25,68 @@ describe('ReactCache', () => {
Scheduler = require('scheduler');
act = require('jest-react').act;
Suspense = React.Suspense;
+ cache = React.experimental_cache;
Offscreen = React.unstable_Offscreen;
getCacheSignal = React.unstable_getCacheSignal;
- getCacheForType = React.unstable_getCacheForType;
useCacheRefresh = React.unstable_useCacheRefresh;
startTransition = React.startTransition;
useState = React.useState;
- caches = [];
+ textCaches = [];
seededCache = null;
- });
-
- 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,
- cleanupScheduled: false,
- };
- data.set(text, newRecord);
- } else if (record.status === 'pending') {
- record.value.resolve();
+ if (gate(flags => flags.enableCache)) {
+ getTextCache = cache(() => {
+ 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 textCache = seededCache;
+ seededCache = null;
+ return textCache;
}
- },
- 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();
- }
- },
- };
- caches.push(cache);
- return cache;
- }
+
+ 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();
- const textCache = getCacheForType(createTextCache);
+ const textCache = getTextCache();
const record = textCache.data.get(text);
if (record !== undefined) {
if (!record.cleanupScheduled) {
@@ -160,18 +163,18 @@ describe('ReactCache', () => {
function seedNextTextCache(text) {
if (seededCache === null) {
- seededCache = createTextCache();
+ seededCache = getTextCache();
}
seededCache.resolve(text);
}
function resolveMostRecentTextCache(text) {
- if (caches.length === 0) {
+ if (textCaches.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);
+ // resolved with `textCaches[index].resolve(text)`.
+ textCaches[textCaches.length - 1].resolve(text);
}
}
@@ -815,9 +818,18 @@ describe('ReactCache', () => {
// @gate experimental || www
test('refresh a cache with seed data', async () => {
- let refresh;
+ let refreshWithSeed;
function App() {
- refresh = useCacheRefresh();
+ const refresh = useCacheRefresh();
+ const [seed, setSeed] = useState({fn: null});
+ if (seed.fn) {
+ seed.fn();
+ seed.fn = null;
+ }
+ refreshWithSeed = fn => {
+ setSeed({fn});
+ refresh();
+ };
return ;
}
@@ -845,11 +857,14 @@ describe('ReactCache', () => {
await act(async () => {
// Refresh the cache with seeded data, like you would receive from a
// server mutation.
- // TODO: Seeding multiple typed caches. Should work by calling `refresh`
+ // TODO: Seeding multiple typed textCaches. Should work by calling `refresh`
// multiple times with different key/value pairs
- const cache = createTextCache();
- cache.resolve('A');
- startTransition(() => refresh(createTextCache, cache));
+ startTransition(() =>
+ refreshWithSeed(() => {
+ const textCache = getTextCache();
+ textCache.resolve('A');
+ }),
+ );
});
// The root should re-render without a cache miss.
// The cache is not cleared up yet, since it's still reference by the root
@@ -1624,4 +1639,152 @@ describe('ReactCache', () => {
expect(Scheduler).toHaveYielded(['More']);
expect(root).toMatchRenderedOutput(
More
);
});
+
+ // @gate enableCache
+ 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(async () => {
+ root.render(
+ <>
+
+
+
+
+
+ >,
+ );
+ });
+ expect(root).toMatchRenderedOutput('string string true false false false ');
+ await act(async () => {
+ root.render(
+ <>
+
+
+
+
+
+ >,
+ );
+ });
+ expect(root).toMatchRenderedOutput('string object true false false false ');
+ const obj = {};
+ await act(async () => {
+ root.render(
+ <>
+
+
+
+
+
+ >,
+ );
+ });
+ expect(root).toMatchRenderedOutput('string object true false false false ');
+ const sameObj = {};
+ await act(async () => {
+ root.render(
+ <>
+
+
+
+
+
+ >,
+ );
+ });
+ expect(root).toMatchRenderedOutput('object object true true false false ');
+ const objA = {};
+ const objB = {};
+ await act(async () => {
+ root.render(
+ <>
+
+
+
+
+
+ >,
+ );
+ });
+ expect(root).toMatchRenderedOutput('object object true false false false ');
+ const sameSymbol = Symbol();
+ await act(async () => {
+ root.render(
+ <>
+
+
+
+
+
+ >,
+ );
+ });
+ expect(root).toMatchRenderedOutput('symbol symbol true true false false ');
+ const notANumber = +'nan';
+ await act(async () => {
+ root.render(
+ <>
+
+
+
+
+
+ >,
+ );
+ });
+ expect(root).toMatchRenderedOutput('number number true false false false ');
+ });
+
+ // @gate enableCache
+ 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(async () => {
+ root.render();
+ });
+ expect(x).toBe(y);
+ expect(z).not.toBe(x);
+ });
});
diff --git a/packages/react/index.classic.fb.js b/packages/react/index.classic.fb.js
index b18fc3d7ad30f..7b62dda775c67 100644
--- a/packages/react/index.classic.fb.js
+++ b/packages/react/index.classic.fb.js
@@ -32,6 +32,7 @@ export {
isValidElement,
lazy,
memo,
+ experimental_cache,
startTransition,
startTransition as unstable_startTransition, // TODO: Remove once call sights updated to startTransition
unstable_Cache,
diff --git a/packages/react/index.experimental.js b/packages/react/index.experimental.js
index 2d36e38836144..095e0898ffb52 100644
--- a/packages/react/index.experimental.js
+++ b/packages/react/index.experimental.js
@@ -29,6 +29,7 @@ export {
isValidElement,
lazy,
memo,
+ experimental_cache,
startTransition,
unstable_Cache,
unstable_DebugTracingMode,
diff --git a/packages/react/index.js b/packages/react/index.js
index 9db87fa8921ab..25c4ba1b1904f 100644
--- a/packages/react/index.js
+++ b/packages/react/index.js
@@ -54,6 +54,7 @@ export {
isValidElement,
lazy,
memo,
+ experimental_cache,
startTransition,
unstable_Cache,
unstable_DebugTracingMode,
diff --git a/packages/react/index.modern.fb.js b/packages/react/index.modern.fb.js
index ad440565309f8..11f6ba663a76a 100644
--- a/packages/react/index.modern.fb.js
+++ b/packages/react/index.modern.fb.js
@@ -31,6 +31,7 @@ export {
isValidElement,
lazy,
memo,
+ experimental_cache,
startTransition,
startTransition as unstable_startTransition, // TODO: Remove once call sights updated to startTransition
unstable_Cache,
diff --git a/packages/react/src/React.js b/packages/react/src/React.js
index 5edcd9e83049b..df84fbeaebf44 100644
--- a/packages/react/src/React.js
+++ b/packages/react/src/React.js
@@ -35,6 +35,7 @@ import {createContext} from './ReactContext';
import {lazy} from './ReactLazy';
import {forwardRef} from './ReactForwardRef';
import {memo} from './ReactMemo';
+import {cache} from './ReactCache';
import {
getCacheSignal,
getCacheForType,
@@ -100,6 +101,7 @@ export {
forwardRef,
lazy,
memo,
+ cache as experimental_cache,
useCallback,
useContext,
useEffect,
diff --git a/packages/react/src/ReactCache.js b/packages/react/src/ReactCache.js
new file mode 100644
index 0000000000000..610fd2c301b1d
--- /dev/null
+++ b/packages/react/src/ReactCache.js
@@ -0,0 +1,126 @@
+/**
+ * Copyright (c) Facebook, Inc. and its affiliates.
+ *
+ * This source code is licensed under the MIT license found in the
+ * LICENSE file in the root directory of this source tree.
+ *
+ * @flow
+ */
+
+import ReactCurrentCache from './ReactCurrentCache';
+
+const UNTERMINATED = 0;
+const TERMINATED = 1;
+const ERRORED = 2;
+
+type UnterminatedCacheNode = {
+ s: 0,
+ v: void,
+ o: null | WeakMap>,
+ p: null | Map>,
+};
+
+type TerminatedCacheNode = {
+ s: 1,
+ v: T,
+ o: null | WeakMap>,
+ p: null | Map>,
+};
+
+type ErroredCacheNode = {
+ s: 2,
+ v: mixed,
+ o: null | WeakMap>,
+ p: null | Map>,
+};
+
+type CacheNode =
+ | TerminatedCacheNode
+ | UnterminatedCacheNode
+ | ErroredCacheNode;
+
+function createCacheRoot(): WeakMap> {
+ return new WeakMap();
+}
+
+function createCacheNode(): CacheNode {
+ return {
+ s: UNTERMINATED,
+ v: undefined,
+ o: null,
+ p: null,
+ };
+}
+
+export function cache, T>(fn: (...A) => T): (...A) => T {
+ return function() {
+ const dispatcher = ReactCurrentCache.current;
+ if (!dispatcher) {
+ // If there is no dispatcher, then we treat this as not being cached.
+ // $FlowFixMe: We don't want to use rest arguments since we transpile the code.
+ return fn.apply(null, arguments);
+ }
+ const fnMap = dispatcher.getCacheForType(createCacheRoot);
+ const fnNode = fnMap.get(fn);
+ let cacheNode: CacheNode;
+ if (fnNode === undefined) {
+ cacheNode = createCacheNode();
+ fnMap.set(fn, cacheNode);
+ } else {
+ cacheNode = fnNode;
+ }
+ for (let i = 0, l = arguments.length; i < l; i++) {
+ const arg = arguments[i];
+ if (
+ typeof arg === 'function' ||
+ (typeof arg === 'object' && arg !== null)
+ ) {
+ // Objects go into a WeakMap
+ let objectCache = cacheNode.o;
+ if (objectCache === null) {
+ cacheNode.o = objectCache = new WeakMap();
+ }
+ const objectNode = objectCache.get(arg);
+ if (objectNode === undefined) {
+ cacheNode = createCacheNode();
+ objectCache.set(arg, cacheNode);
+ } else {
+ cacheNode = objectNode;
+ }
+ } else {
+ // Primitives go into a regular Map
+ let primitiveCache = cacheNode.p;
+ if (primitiveCache === null) {
+ cacheNode.p = primitiveCache = new Map();
+ }
+ const primitiveNode = primitiveCache.get(arg);
+ if (primitiveNode === undefined) {
+ cacheNode = createCacheNode();
+ primitiveCache.set(arg, cacheNode);
+ } else {
+ cacheNode = primitiveNode;
+ }
+ }
+ }
+ if (cacheNode.s === TERMINATED) {
+ return cacheNode.v;
+ }
+ if (cacheNode.s === ERRORED) {
+ throw cacheNode.v;
+ }
+ try {
+ // $FlowFixMe: We don't want to use rest arguments since we transpile the code.
+ const result = fn.apply(null, arguments);
+ const terminatedNode: TerminatedCacheNode = (cacheNode: any);
+ terminatedNode.s = TERMINATED;
+ terminatedNode.v = result;
+ return result;
+ } catch (error) {
+ // We store the first error that's thrown and rethrow it.
+ const erroredNode: ErroredCacheNode = (cacheNode: any);
+ erroredNode.s = ERRORED;
+ erroredNode.v = error;
+ throw error;
+ }
+ };
+}
diff --git a/packages/react/src/ReactSharedSubset.experimental.js b/packages/react/src/ReactSharedSubset.experimental.js
index b05ec07302ed3..684030d6daa0d 100644
--- a/packages/react/src/ReactSharedSubset.experimental.js
+++ b/packages/react/src/ReactSharedSubset.experimental.js
@@ -24,6 +24,7 @@ export {
isValidElement,
lazy,
memo,
+ experimental_cache,
startTransition,
unstable_DebugTracingMode,
unstable_getCacheSignal,