diff --git a/packages/react/src/ReactCacheClient.js b/packages/react/src/ReactCacheClient.js index e752a110a5cf3..9e8658bdc891b 100644 --- a/packages/react/src/ReactCacheClient.js +++ b/packages/react/src/ReactCacheClient.js @@ -7,21 +7,28 @@ * @flow */ +import {disableClientCache} from 'shared/ReactFeatureFlags'; +import {cache as cacheImpl} from './ReactCacheImpl'; + export function cache, T>(fn: (...A) => T): (...A) => T { - // On the client (i.e. not a Server Components environment) `cache` has - // no caching behavior. We just return the function as-is. - // - // We intend to implement client caching in a future major release. In the - // meantime, it's only exposed as an API so that Shared Components can use - // per-request caching on the server without breaking on the client. But it - // does mean they need to be aware of the behavioral difference. - // - // The rest of the behavior is the same as the server implementation — it - // returns a new reference, extra properties like `displayName` are not - // preserved, the length of the new function is 0, etc. That way apps can't - // accidentally depend on those details. - return function () { - // $FlowFixMe[incompatible-call]: We don't want to use rest arguments since we transpile the code. - return fn.apply(null, arguments); - }; + if (disableClientCache) { + // On the client (i.e. not a Server Components environment) `cache` has + // no caching behavior. We just return the function as-is. + // + // We intend to implement client caching in a future major release. In the + // meantime, it's only exposed as an API so that Shared Components can use + // per-request caching on the server without breaking on the client. But it + // does mean they need to be aware of the behavioral difference. + // + // The rest of the behavior is the same as the server implementation — it + // returns a new reference, extra properties like `displayName` are not + // preserved, the length of the new function is 0, etc. That way apps can't + // accidentally depend on those details. + return function () { + // $FlowFixMe[incompatible-call]: We don't want to use rest arguments since we transpile the code. + return fn.apply(null, arguments); + }; + } else { + return cacheImpl(fn); + } } diff --git a/packages/react/src/ReactCacheImpl.js b/packages/react/src/ReactCacheImpl.js new file mode 100644 index 0000000000000..c998aa4c87b26 --- /dev/null +++ b/packages/react/src/ReactCacheImpl.js @@ -0,0 +1,128 @@ +/** + * Copyright (c) Meta Platforms, 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, // status, represents whether the cached computation returned a value or threw an error + v: undefined, // value, either the cached result or an error, depending on s + o: null, // object cache, a WeakMap where non-primitive arguments are stored + p: null, // primitive cache, a regular Map where primitive arguments are stored. + }; +} + +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[incompatible-call]: We don't want to use rest arguments since we transpile the code. + return fn.apply(null, arguments); + } + const fnMap: WeakMap> = 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[incompatible-call]: 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/ReactCacheServer.js b/packages/react/src/ReactCacheServer.js index c998aa4c87b26..dd90d8de2a999 100644 --- a/packages/react/src/ReactCacheServer.js +++ b/packages/react/src/ReactCacheServer.js @@ -7,122 +7,4 @@ * @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, // status, represents whether the cached computation returned a value or threw an error - v: undefined, // value, either the cached result or an error, depending on s - o: null, // object cache, a WeakMap where non-primitive arguments are stored - p: null, // primitive cache, a regular Map where primitive arguments are stored. - }; -} - -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[incompatible-call]: We don't want to use rest arguments since we transpile the code. - return fn.apply(null, arguments); - } - const fnMap: WeakMap> = 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[incompatible-call]: 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; - } - }; -} +export {cache} from './ReactCacheImpl'; diff --git a/packages/shared/ReactFeatureFlags.js b/packages/shared/ReactFeatureFlags.js index 845ff9645a838..491ffaf0d5f99 100644 --- a/packages/shared/ReactFeatureFlags.js +++ b/packages/shared/ReactFeatureFlags.js @@ -164,6 +164,9 @@ export const enableCustomElementPropertySupport = __NEXT_MAJOR__; // request for certain browsers. export const enableFilterEmptyStringAttributesDOM = __NEXT_MAJOR__; +// Disabled caching behavior of `react/cache` in client runtimes. +export const disableClientCache = false; + // ----------------------------------------------------------------------------- // Chopping Block // diff --git a/packages/shared/forks/ReactFeatureFlags.native-fb.js b/packages/shared/forks/ReactFeatureFlags.native-fb.js index d98e4e508101f..38f7c8f7b522c 100644 --- a/packages/shared/forks/ReactFeatureFlags.native-fb.js +++ b/packages/shared/forks/ReactFeatureFlags.native-fb.js @@ -92,6 +92,7 @@ export const enableFizzExternalRuntime = false; export const enableAsyncActions = false; export const enableUseDeferredValueInitialArg = true; +export const disableClientCache = true; // Flow magic to verify the exports of this file match the original version. ((((null: any): ExportsType): FeatureFlagsType): ExportsType); diff --git a/packages/shared/forks/ReactFeatureFlags.native-oss.js b/packages/shared/forks/ReactFeatureFlags.native-oss.js index a8fb8c9ad740e..1cb8d615c0e9e 100644 --- a/packages/shared/forks/ReactFeatureFlags.native-oss.js +++ b/packages/shared/forks/ReactFeatureFlags.native-oss.js @@ -84,6 +84,7 @@ export const alwaysThrottleRetries = true; export const useMicrotasksForSchedulingInFabric = false; export const passChildrenWhenCloningPersistedNodes = false; export const enableUseDeferredValueInitialArg = __EXPERIMENTAL__; +export const disableClientCache = true; // Flow magic to verify the exports of this file match the original version. ((((null: any): ExportsType): FeatureFlagsType): ExportsType); diff --git a/packages/shared/forks/ReactFeatureFlags.test-renderer.js b/packages/shared/forks/ReactFeatureFlags.test-renderer.js index fa019f07600d3..6c8fdbaf126b7 100644 --- a/packages/shared/forks/ReactFeatureFlags.test-renderer.js +++ b/packages/shared/forks/ReactFeatureFlags.test-renderer.js @@ -84,6 +84,7 @@ export const alwaysThrottleRetries = true; export const useMicrotasksForSchedulingInFabric = false; export const passChildrenWhenCloningPersistedNodes = false; export const enableUseDeferredValueInitialArg = __EXPERIMENTAL__; +export const disableClientCache = true; // Flow magic to verify the exports of this file match the original version. ((((null: any): ExportsType): FeatureFlagsType): ExportsType); diff --git a/packages/shared/forks/ReactFeatureFlags.test-renderer.native.js b/packages/shared/forks/ReactFeatureFlags.test-renderer.native.js index 347ff62abf9af..4da2af35ced60 100644 --- a/packages/shared/forks/ReactFeatureFlags.test-renderer.native.js +++ b/packages/shared/forks/ReactFeatureFlags.test-renderer.native.js @@ -81,6 +81,7 @@ export const alwaysThrottleRetries = true; export const useMicrotasksForSchedulingInFabric = false; export const passChildrenWhenCloningPersistedNodes = false; export const enableUseDeferredValueInitialArg = __EXPERIMENTAL__; +export const disableClientCache = true; // Flow magic to verify the exports of this file match the original version. ((((null: any): ExportsType): FeatureFlagsType): ExportsType); diff --git a/packages/shared/forks/ReactFeatureFlags.test-renderer.www.js b/packages/shared/forks/ReactFeatureFlags.test-renderer.www.js index 236d46b4a5756..12f78667bfd14 100644 --- a/packages/shared/forks/ReactFeatureFlags.test-renderer.www.js +++ b/packages/shared/forks/ReactFeatureFlags.test-renderer.www.js @@ -84,6 +84,7 @@ export const alwaysThrottleRetries = true; export const useMicrotasksForSchedulingInFabric = false; export const passChildrenWhenCloningPersistedNodes = false; export const enableUseDeferredValueInitialArg = true; +export const disableClientCache = true; // Flow magic to verify the exports of this file match the original version. ((((null: any): ExportsType): FeatureFlagsType): ExportsType); diff --git a/packages/shared/forks/ReactFeatureFlags.www.js b/packages/shared/forks/ReactFeatureFlags.www.js index 15f5b993c8254..c7ab9d420d193 100644 --- a/packages/shared/forks/ReactFeatureFlags.www.js +++ b/packages/shared/forks/ReactFeatureFlags.www.js @@ -111,6 +111,7 @@ export const useMicrotasksForSchedulingInFabric = false; export const passChildrenWhenCloningPersistedNodes = false; export const enableAsyncDebugInfo = false; +export const disableClientCache = true; // Flow magic to verify the exports of this file match the original version. ((((null: any): ExportsType): FeatureFlagsType): ExportsType);