Skip to content

Commit e88d439

Browse files
committed
Adds a feature flag to control whether the client cache function is just a passthrough. before we land breaking chagnes for the next major it will be off and then we can flag it on when we want to break it.
1 parent 596827f commit e88d439

10 files changed

+161
-135
lines changed

packages/react/src/ReactCacheClient.js

Lines changed: 23 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -7,21 +7,28 @@
77
* @flow
88
*/
99

10+
import {disableClientCache} from 'shared/ReactFeatureFlags';
11+
import {cache as cacheImpl} from './ReactCacheImpl';
12+
1013
export function cache<A: Iterable<mixed>, T>(fn: (...A) => T): (...A) => T {
11-
// On the client (i.e. not a Server Components environment) `cache` has
12-
// no caching behavior. We just return the function as-is.
13-
//
14-
// We intend to implement client caching in a future major release. In the
15-
// meantime, it's only exposed as an API so that Shared Components can use
16-
// per-request caching on the server without breaking on the client. But it
17-
// does mean they need to be aware of the behavioral difference.
18-
//
19-
// The rest of the behavior is the same as the server implementation — it
20-
// returns a new reference, extra properties like `displayName` are not
21-
// preserved, the length of the new function is 0, etc. That way apps can't
22-
// accidentally depend on those details.
23-
return function () {
24-
// $FlowFixMe[incompatible-call]: We don't want to use rest arguments since we transpile the code.
25-
return fn.apply(null, arguments);
26-
};
14+
if (disableClientCache) {
15+
// On the client (i.e. not a Server Components environment) `cache` has
16+
// no caching behavior. We just return the function as-is.
17+
//
18+
// We intend to implement client caching in a future major release. In the
19+
// meantime, it's only exposed as an API so that Shared Components can use
20+
// per-request caching on the server without breaking on the client. But it
21+
// does mean they need to be aware of the behavioral difference.
22+
//
23+
// The rest of the behavior is the same as the server implementation — it
24+
// returns a new reference, extra properties like `displayName` are not
25+
// preserved, the length of the new function is 0, etc. That way apps can't
26+
// accidentally depend on those details.
27+
return function () {
28+
// $FlowFixMe[incompatible-call]: We don't want to use rest arguments since we transpile the code.
29+
return fn.apply(null, arguments);
30+
};
31+
} else {
32+
return cacheImpl(fn);
33+
}
2734
}

packages/react/src/ReactCacheImpl.js

Lines changed: 128 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,128 @@
1+
/**
2+
* Copyright (c) Meta Platforms, Inc. and its affiliates.
3+
*
4+
* This source code is licensed under the MIT license found in the
5+
* LICENSE file in the root directory of this source tree.
6+
*
7+
* @flow
8+
*/
9+
10+
import ReactCurrentCache from './ReactCurrentCache';
11+
12+
const UNTERMINATED = 0;
13+
const TERMINATED = 1;
14+
const ERRORED = 2;
15+
16+
type UnterminatedCacheNode<T> = {
17+
s: 0,
18+
v: void,
19+
o: null | WeakMap<Function | Object, CacheNode<T>>,
20+
p: null | Map<string | number | null | void | symbol | boolean, CacheNode<T>>,
21+
};
22+
23+
type TerminatedCacheNode<T> = {
24+
s: 1,
25+
v: T,
26+
o: null | WeakMap<Function | Object, CacheNode<T>>,
27+
p: null | Map<string | number | null | void | symbol | boolean, CacheNode<T>>,
28+
};
29+
30+
type ErroredCacheNode<T> = {
31+
s: 2,
32+
v: mixed,
33+
o: null | WeakMap<Function | Object, CacheNode<T>>,
34+
p: null | Map<string | number | null | void | symbol | boolean, CacheNode<T>>,
35+
};
36+
37+
type CacheNode<T> =
38+
| TerminatedCacheNode<T>
39+
| UnterminatedCacheNode<T>
40+
| ErroredCacheNode<T>;
41+
42+
function createCacheRoot<T>(): WeakMap<Function | Object, CacheNode<T>> {
43+
return new WeakMap();
44+
}
45+
46+
function createCacheNode<T>(): CacheNode<T> {
47+
return {
48+
s: UNTERMINATED, // status, represents whether the cached computation returned a value or threw an error
49+
v: undefined, // value, either the cached result or an error, depending on s
50+
o: null, // object cache, a WeakMap where non-primitive arguments are stored
51+
p: null, // primitive cache, a regular Map where primitive arguments are stored.
52+
};
53+
}
54+
55+
export function cache<A: Iterable<mixed>, T>(fn: (...A) => T): (...A) => T {
56+
return function () {
57+
const dispatcher = ReactCurrentCache.current;
58+
if (!dispatcher) {
59+
// If there is no dispatcher, then we treat this as not being cached.
60+
// $FlowFixMe[incompatible-call]: We don't want to use rest arguments since we transpile the code.
61+
return fn.apply(null, arguments);
62+
}
63+
const fnMap: WeakMap<any, CacheNode<T>> = dispatcher.getCacheForType(
64+
createCacheRoot,
65+
);
66+
const fnNode = fnMap.get(fn);
67+
let cacheNode: CacheNode<T>;
68+
if (fnNode === undefined) {
69+
cacheNode = createCacheNode();
70+
fnMap.set(fn, cacheNode);
71+
} else {
72+
cacheNode = fnNode;
73+
}
74+
for (let i = 0, l = arguments.length; i < l; i++) {
75+
const arg = arguments[i];
76+
if (
77+
typeof arg === 'function' ||
78+
(typeof arg === 'object' && arg !== null)
79+
) {
80+
// Objects go into a WeakMap
81+
let objectCache = cacheNode.o;
82+
if (objectCache === null) {
83+
cacheNode.o = objectCache = new WeakMap();
84+
}
85+
const objectNode = objectCache.get(arg);
86+
if (objectNode === undefined) {
87+
cacheNode = createCacheNode();
88+
objectCache.set(arg, cacheNode);
89+
} else {
90+
cacheNode = objectNode;
91+
}
92+
} else {
93+
// Primitives go into a regular Map
94+
let primitiveCache = cacheNode.p;
95+
if (primitiveCache === null) {
96+
cacheNode.p = primitiveCache = new Map();
97+
}
98+
const primitiveNode = primitiveCache.get(arg);
99+
if (primitiveNode === undefined) {
100+
cacheNode = createCacheNode();
101+
primitiveCache.set(arg, cacheNode);
102+
} else {
103+
cacheNode = primitiveNode;
104+
}
105+
}
106+
}
107+
if (cacheNode.s === TERMINATED) {
108+
return cacheNode.v;
109+
}
110+
if (cacheNode.s === ERRORED) {
111+
throw cacheNode.v;
112+
}
113+
try {
114+
// $FlowFixMe[incompatible-call]: We don't want to use rest arguments since we transpile the code.
115+
const result = fn.apply(null, arguments);
116+
const terminatedNode: TerminatedCacheNode<T> = (cacheNode: any);
117+
terminatedNode.s = TERMINATED;
118+
terminatedNode.v = result;
119+
return result;
120+
} catch (error) {
121+
// We store the first error that's thrown and rethrow it.
122+
const erroredNode: ErroredCacheNode<T> = (cacheNode: any);
123+
erroredNode.s = ERRORED;
124+
erroredNode.v = error;
125+
throw error;
126+
}
127+
};
128+
}

packages/react/src/ReactCacheServer.js

Lines changed: 1 addition & 119 deletions
Original file line numberDiff line numberDiff line change
@@ -7,122 +7,4 @@
77
* @flow
88
*/
99

10-
import ReactCurrentCache from './ReactCurrentCache';
11-
12-
const UNTERMINATED = 0;
13-
const TERMINATED = 1;
14-
const ERRORED = 2;
15-
16-
type UnterminatedCacheNode<T> = {
17-
s: 0,
18-
v: void,
19-
o: null | WeakMap<Function | Object, CacheNode<T>>,
20-
p: null | Map<string | number | null | void | symbol | boolean, CacheNode<T>>,
21-
};
22-
23-
type TerminatedCacheNode<T> = {
24-
s: 1,
25-
v: T,
26-
o: null | WeakMap<Function | Object, CacheNode<T>>,
27-
p: null | Map<string | number | null | void | symbol | boolean, CacheNode<T>>,
28-
};
29-
30-
type ErroredCacheNode<T> = {
31-
s: 2,
32-
v: mixed,
33-
o: null | WeakMap<Function | Object, CacheNode<T>>,
34-
p: null | Map<string | number | null | void | symbol | boolean, CacheNode<T>>,
35-
};
36-
37-
type CacheNode<T> =
38-
| TerminatedCacheNode<T>
39-
| UnterminatedCacheNode<T>
40-
| ErroredCacheNode<T>;
41-
42-
function createCacheRoot<T>(): WeakMap<Function | Object, CacheNode<T>> {
43-
return new WeakMap();
44-
}
45-
46-
function createCacheNode<T>(): CacheNode<T> {
47-
return {
48-
s: UNTERMINATED, // status, represents whether the cached computation returned a value or threw an error
49-
v: undefined, // value, either the cached result or an error, depending on s
50-
o: null, // object cache, a WeakMap where non-primitive arguments are stored
51-
p: null, // primitive cache, a regular Map where primitive arguments are stored.
52-
};
53-
}
54-
55-
export function cache<A: Iterable<mixed>, T>(fn: (...A) => T): (...A) => T {
56-
return function () {
57-
const dispatcher = ReactCurrentCache.current;
58-
if (!dispatcher) {
59-
// If there is no dispatcher, then we treat this as not being cached.
60-
// $FlowFixMe[incompatible-call]: We don't want to use rest arguments since we transpile the code.
61-
return fn.apply(null, arguments);
62-
}
63-
const fnMap: WeakMap<any, CacheNode<T>> = dispatcher.getCacheForType(
64-
createCacheRoot,
65-
);
66-
const fnNode = fnMap.get(fn);
67-
let cacheNode: CacheNode<T>;
68-
if (fnNode === undefined) {
69-
cacheNode = createCacheNode();
70-
fnMap.set(fn, cacheNode);
71-
} else {
72-
cacheNode = fnNode;
73-
}
74-
for (let i = 0, l = arguments.length; i < l; i++) {
75-
const arg = arguments[i];
76-
if (
77-
typeof arg === 'function' ||
78-
(typeof arg === 'object' && arg !== null)
79-
) {
80-
// Objects go into a WeakMap
81-
let objectCache = cacheNode.o;
82-
if (objectCache === null) {
83-
cacheNode.o = objectCache = new WeakMap();
84-
}
85-
const objectNode = objectCache.get(arg);
86-
if (objectNode === undefined) {
87-
cacheNode = createCacheNode();
88-
objectCache.set(arg, cacheNode);
89-
} else {
90-
cacheNode = objectNode;
91-
}
92-
} else {
93-
// Primitives go into a regular Map
94-
let primitiveCache = cacheNode.p;
95-
if (primitiveCache === null) {
96-
cacheNode.p = primitiveCache = new Map();
97-
}
98-
const primitiveNode = primitiveCache.get(arg);
99-
if (primitiveNode === undefined) {
100-
cacheNode = createCacheNode();
101-
primitiveCache.set(arg, cacheNode);
102-
} else {
103-
cacheNode = primitiveNode;
104-
}
105-
}
106-
}
107-
if (cacheNode.s === TERMINATED) {
108-
return cacheNode.v;
109-
}
110-
if (cacheNode.s === ERRORED) {
111-
throw cacheNode.v;
112-
}
113-
try {
114-
// $FlowFixMe[incompatible-call]: We don't want to use rest arguments since we transpile the code.
115-
const result = fn.apply(null, arguments);
116-
const terminatedNode: TerminatedCacheNode<T> = (cacheNode: any);
117-
terminatedNode.s = TERMINATED;
118-
terminatedNode.v = result;
119-
return result;
120-
} catch (error) {
121-
// We store the first error that's thrown and rethrow it.
122-
const erroredNode: ErroredCacheNode<T> = (cacheNode: any);
123-
erroredNode.s = ERRORED;
124-
erroredNode.v = error;
125-
throw error;
126-
}
127-
};
128-
}
10+
export {cache} from './ReactCacheImpl';

packages/shared/ReactFeatureFlags.js

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -164,6 +164,9 @@ export const enableCustomElementPropertySupport = __NEXT_MAJOR__;
164164
// request for certain browsers.
165165
export const enableFilterEmptyStringAttributesDOM = __NEXT_MAJOR__;
166166

167+
// Disabled caching behavior of `react/cache` in client runtimes.
168+
export const disableClientCache = false;
169+
167170
// -----------------------------------------------------------------------------
168171
// Chopping Block
169172
//

packages/shared/forks/ReactFeatureFlags.native-fb.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -92,6 +92,7 @@ export const enableFizzExternalRuntime = false;
9292

9393
export const enableAsyncActions = false;
9494
export const enableUseDeferredValueInitialArg = true;
95+
export const disableClientCache = true;
9596

9697
// Flow magic to verify the exports of this file match the original version.
9798
((((null: any): ExportsType): FeatureFlagsType): ExportsType);

packages/shared/forks/ReactFeatureFlags.native-oss.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -84,6 +84,7 @@ export const alwaysThrottleRetries = true;
8484
export const useMicrotasksForSchedulingInFabric = false;
8585
export const passChildrenWhenCloningPersistedNodes = false;
8686
export const enableUseDeferredValueInitialArg = __EXPERIMENTAL__;
87+
export const disableClientCache = true;
8788

8889
// Flow magic to verify the exports of this file match the original version.
8990
((((null: any): ExportsType): FeatureFlagsType): ExportsType);

packages/shared/forks/ReactFeatureFlags.test-renderer.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -84,6 +84,7 @@ export const alwaysThrottleRetries = true;
8484
export const useMicrotasksForSchedulingInFabric = false;
8585
export const passChildrenWhenCloningPersistedNodes = false;
8686
export const enableUseDeferredValueInitialArg = __EXPERIMENTAL__;
87+
export const disableClientCache = true;
8788

8889
// Flow magic to verify the exports of this file match the original version.
8990
((((null: any): ExportsType): FeatureFlagsType): ExportsType);

packages/shared/forks/ReactFeatureFlags.test-renderer.native.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -81,6 +81,7 @@ export const alwaysThrottleRetries = true;
8181
export const useMicrotasksForSchedulingInFabric = false;
8282
export const passChildrenWhenCloningPersistedNodes = false;
8383
export const enableUseDeferredValueInitialArg = __EXPERIMENTAL__;
84+
export const disableClientCache = true;
8485

8586
// Flow magic to verify the exports of this file match the original version.
8687
((((null: any): ExportsType): FeatureFlagsType): ExportsType);

packages/shared/forks/ReactFeatureFlags.test-renderer.www.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -84,6 +84,7 @@ export const alwaysThrottleRetries = true;
8484
export const useMicrotasksForSchedulingInFabric = false;
8585
export const passChildrenWhenCloningPersistedNodes = false;
8686
export const enableUseDeferredValueInitialArg = true;
87+
export const disableClientCache = true;
8788

8889
// Flow magic to verify the exports of this file match the original version.
8990
((((null: any): ExportsType): FeatureFlagsType): ExportsType);

packages/shared/forks/ReactFeatureFlags.www.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -111,6 +111,7 @@ export const useMicrotasksForSchedulingInFabric = false;
111111
export const passChildrenWhenCloningPersistedNodes = false;
112112

113113
export const enableAsyncDebugInfo = false;
114+
export const disableClientCache = true;
114115

115116
// Flow magic to verify the exports of this file match the original version.
116117
((((null: any): ExportsType): FeatureFlagsType): ExportsType);

0 commit comments

Comments
 (0)