@samitouri / QOS-React-2 / commits / fa9bea0c41

Initial implementation of cache cleanup (#22510)

This is an initial, partial implementation of a cleanup mechanism for the experimental Cache API. The idea is that consumers of the Cache API can register to be informed when a given Cache instance is no longer needed so that they can perform associated cleanup tasks to free resources stored in the cache. A canonical example would be cancelling pending network requests. An overview of the high-level changes: * Changes the `Cache` type from a Map of cache instances to be an object with the original Map of instances, a reference count (to count roughly "active references" to the cache instances - more below), and an AbortController. * Adds a new public API, `unstable_getCacheSignal(): AbortSignal`, which is callable during render. It returns an AbortSignal tied to the lifetime of the cache - developers can listen for the 'abort' event on the signal, which React now triggers when a given cache instance is no longer referenced. * Note that `AbortSignal` is a web standard that is supported by other platform APIs; for example a signal can be passed to `fetch()` to trigger cancellation of an HTTP request. * Implements the above - triggering the 'abort' event - by handling passive mount/unmount for HostRoot and CacheComponent fiber nodes. Cases handled: * Aborted transitions: we clean up a new cache created for an aborted transition * Suspense: we retain a fresh cache instance until a suspended tree resolves For follow-ups: * When a subsequent cache refresh is issued before a previous refresh completes, the refreshes are queued. Fresh cache instances for previous refreshes in the queue should be cleared, retaining only the most recent cache. I plan to address this in a follow-up PR. * If a refresh is cancelled, the fresh cache should be cleaned up.

Joseph Savona committed Oct 21, 2021 at 14:11 UTC fa9bea0c41ccfef5b528ef9b5517607f9f94c52a
29 files changed +1482 -199
packages/react-devtools-shared/src/__tests__/__snapshots__/profilingCache-test.js.snap
+2 -2
@@ -73,7 +73,7 @@ Object {
73 },
74 },
75 "duration": 15,
76 - "effectDuration": null,
76 + "effectDuration": 0,
77 "fiberActualDurations": Map {
78 1 => 15,
79 2 => 15,
@@ -86,7 +86,7 @@ Object {
86 3 => 3,
87 4 => 2,
88 },
89 - "passiveEffectDuration": null,
89 + "passiveEffectDuration": 0,
90 "priorityLevel": "Immediate",
91 "timestamp": 15,
92 "updaters": Array [
packages/react-dom/src/server/ReactPartialRendererHooks.js
+5
@@ -216,6 +216,10 @@ export function resetHooksState(): void {
216 workInProgressHook = null;
217 }
218
219 +function getCacheSignal() {
220 + throw new Error('Not implemented.');
221 +}
222 +
223 function getCacheForType<T>(resourceType: () => T): T {
224 throw new Error('Not implemented.');
225 }
@@ -551,6 +555,7 @@ export const Dispatcher: DispatcherType = {
555 };
556
557 if (enableCache) {
558 + Dispatcher.getCacheSignal = getCacheSignal;
559 Dispatcher.getCacheForType = getCacheForType;
560 Dispatcher.useCacheRefresh = useCacheRefresh;
561 }
packages/react-reconciler/src/ReactFiberCacheComponent.new.js
+80 -5
@@ -18,8 +18,13 @@ import {REACT_CONTEXT_TYPE} from 'shared/ReactSymbols';
18 import {isPrimaryRenderer} from './ReactFiberHostConfig';
19 import {createCursor, push, pop} from './ReactFiberStack.new';
20 import {pushProvider, popProvider} from './ReactFiberNewContext.new';
21 +import * as Scheduler from 'scheduler';
22
22 -export type Cache = Map<() => mixed, mixed>;
23 +export type Cache = {|
24 + controller: AbortController,
25 + data: Map<() => mixed, mixed>,
26 + refCount: number,
27 +|};
28
29 export type CacheComponentState = {|
30 +parent: Cache,
@@ -31,6 +36,13 @@ export type SpawnedCachePool = {|
36 +pool: Cache,
37 |};
38
39 +// Intentionally not named imports because Rollup would
40 +// use dynamic dispatch for CommonJS interop named imports.
41 +const {
42 + unstable_scheduleCallback: scheduleCallback,
43 + unstable_NormalPriority: NormalPriority,
44 +} = Scheduler;
45 +
46 export const CacheContext: ReactContext<Cache> = enableCache
47 ? {
48 $$typeof: REACT_CONTEXT_TYPE,
@@ -57,6 +69,58 @@ let pooledCache: Cache | null = null;
69 // cache from the render that suspended.
70 const prevFreshCacheOnStack: StackCursor<Cache | null> = createCursor(null);
71
72 +// Creates a new empty Cache instance with a ref-count of 0. The caller is responsible
73 +// for retaining the cache once it is in use (retainCache), and releasing the cache
74 +// once it is no longer needed (releaseCache).
75 +export function createCache(): Cache {
76 + if (!enableCache) {
77 + return (null: any);
78 + }
79 + const cache: Cache = {
80 + controller: new AbortController(),
81 + data: new Map(),
82 + refCount: 0,
83 + };
84 +
85 + return cache;
86 +}
87 +
88 +export function retainCache(cache: Cache) {
89 + if (!enableCache) {
90 + return;
91 + }
92 + if (__DEV__) {
93 + if (cache.controller.signal.aborted) {
94 + console.warn(
95 + 'A cache instance was retained after it was already freed. ' +
96 + 'This likely indicates a bug in React.',
97 + );
98 + }
99 + }
100 + cache.refCount++;
101 +}
102 +
103 +// Cleanup a cache instance, potentially freeing it if there are no more references
104 +export function releaseCache(cache: Cache) {
105 + if (!enableCache) {
106 + return;
107 + }
108 + cache.refCount--;
109 + if (__DEV__) {
110 + if (cache.refCount < 0) {
111 + console.warn(
112 + 'A cache instance was released after it was already freed. ' +
113 + 'This likely indicates a bug in React.',
114 + );
115 + }
116 + }
117 + if (cache.refCount === 0) {
118 + scheduleCallback(NormalPriority, () => {
119 + cache.controller.abort();
120 + });
121 + }
122 +}
123 +
124 export function pushCacheProvider(workInProgress: Fiber, cache: Cache) {
125 if (!enableCache) {
126 return;
@@ -78,8 +142,14 @@ export function requestCacheFromPool(renderLanes: Lanes): Cache {
142 if (pooledCache !== null) {
143 return pooledCache;
144 }
81 - // Create a fresh cache.
82 - pooledCache = new Map();
145 + // Create a fresh cache. The pooled cache must be owned - it is freed
146 + // in releaseRootPooledCache() - but the cache instance handed out
147 + // is retained/released in the commit phase of the component that
148 + // references is (ie the host root, cache boundary, suspense component)
149 + // Ie, pooledCache is conceptually an Option<Arc<Cache>> (owned),
150 + // whereas the return value of this function is a &Arc<Cache> (borrowed).
151 + pooledCache = createCache();
152 + retainCache(pooledCache);
153 return pooledCache;
154 }
155
@@ -91,7 +161,13 @@ export function pushRootCachePool(root: FiberRoot) {
161 // from `root.pooledCache`. If it's currently `null`, we will lazily
162 // initialize it the first type it's requested. However, we only mutate
163 // the root itself during the complete/unwind phase of the HostRoot.
94 - pooledCache = root.pooledCache;
164 + const rootCache = root.pooledCache;
165 + if (rootCache != null) {
166 + pooledCache = rootCache;
167 + root.pooledCache = null;
168 + } else {
169 + pooledCache = null;
170 + }
171 }
172
173 export function popRootCachePool(root: FiberRoot, renderLanes: Lanes) {
@@ -157,7 +233,6 @@ export function getSuspendedCachePool(): SpawnedCachePool | null {
233 if (!enableCache) {
234 return null;
235 }
160 -
236 // We check the cache on the stack first, since that's the one any new Caches
237 // would have accessed.
238 let pool = pooledCache;
packages/react-reconciler/src/ReactFiberCacheComponent.old.js
+80 -5
@@ -18,8 +18,13 @@ import {REACT_CONTEXT_TYPE} from 'shared/ReactSymbols';
18 import {isPrimaryRenderer} from './ReactFiberHostConfig';
19 import {createCursor, push, pop} from './ReactFiberStack.old';
20 import {pushProvider, popProvider} from './ReactFiberNewContext.old';
21 +import * as Scheduler from 'scheduler';
22
22 -export type Cache = Map<() => mixed, mixed>;
23 +export type Cache = {|
24 + controller: AbortController,
25 + data: Map<() => mixed, mixed>,
26 + refCount: number,
27 +|};
28
29 export type CacheComponentState = {|
30 +parent: Cache,
@@ -31,6 +36,13 @@ export type SpawnedCachePool = {|
36 +pool: Cache,
37 |};
38
39 +// Intentionally not named imports because Rollup would
40 +// use dynamic dispatch for CommonJS interop named imports.
41 +const {
42 + unstable_scheduleCallback: scheduleCallback,
43 + unstable_NormalPriority: NormalPriority,
44 +} = Scheduler;
45 +
46 export const CacheContext: ReactContext<Cache> = enableCache
47 ? {
48 $$typeof: REACT_CONTEXT_TYPE,
@@ -57,6 +69,58 @@ let pooledCache: Cache | null = null;
69 // cache from the render that suspended.
70 const prevFreshCacheOnStack: StackCursor<Cache | null> = createCursor(null);
71
72 +// Creates a new empty Cache instance with a ref-count of 0. The caller is responsible
73 +// for retaining the cache once it is in use (retainCache), and releasing the cache
74 +// once it is no longer needed (releaseCache).
75 +export function createCache(): Cache {
76 + if (!enableCache) {
77 + return (null: any);
78 + }
79 + const cache: Cache = {
80 + controller: new AbortController(),
81 + data: new Map(),
82 + refCount: 0,
83 + };
84 +
85 + return cache;
86 +}
87 +
88 +export function retainCache(cache: Cache) {
89 + if (!enableCache) {
90 + return;
91 + }
92 + if (__DEV__) {
93 + if (cache.controller.signal.aborted) {
94 + console.warn(
95 + 'A cache instance was retained after it was already freed. ' +
96 + 'This likely indicates a bug in React.',
97 + );
98 + }
99 + }
100 + cache.refCount++;
101 +}
102 +
103 +// Cleanup a cache instance, potentially freeing it if there are no more references
104 +export function releaseCache(cache: Cache) {
105 + if (!enableCache) {
106 + return;
107 + }
108 + cache.refCount--;
109 + if (__DEV__) {
110 + if (cache.refCount < 0) {
111 + console.warn(
112 + 'A cache instance was released after it was already freed. ' +
113 + 'This likely indicates a bug in React.',
114 + );
115 + }
116 + }
117 + if (cache.refCount === 0) {
118 + scheduleCallback(NormalPriority, () => {
119 + cache.controller.abort();
120 + });
121 + }
122 +}
123 +
124 export function pushCacheProvider(workInProgress: Fiber, cache: Cache) {
125 if (!enableCache) {
126 return;
@@ -78,8 +142,14 @@ export function requestCacheFromPool(renderLanes: Lanes): Cache {
142 if (pooledCache !== null) {
143 return pooledCache;
144 }
81 - // Create a fresh cache.
82 - pooledCache = new Map();
145 + // Create a fresh cache. The pooled cache must be owned - it is freed
146 + // in releaseRootPooledCache() - but the cache instance handed out
147 + // is retained/released in the commit phase of the component that
148 + // references is (ie the host root, cache boundary, suspense component)
149 + // Ie, pooledCache is conceptually an Option<Arc<Cache>> (owned),
150 + // whereas the return value of this function is a &Arc<Cache> (borrowed).
151 + pooledCache = createCache();
152 + retainCache(pooledCache);
153 return pooledCache;
154 }
155
@@ -91,7 +161,13 @@ export function pushRootCachePool(root: FiberRoot) {
161 // from `root.pooledCache`. If it's currently `null`, we will lazily
162 // initialize it the first type it's requested. However, we only mutate
163 // the root itself during the complete/unwind phase of the HostRoot.
94 - pooledCache = root.pooledCache;
164 + const rootCache = root.pooledCache;
165 + if (rootCache != null) {
166 + pooledCache = rootCache;
167 + root.pooledCache = null;
168 + } else {
169 + pooledCache = null;
170 + }
171 }
172
173 export function popRootCachePool(root: FiberRoot, renderLanes: Lanes) {
@@ -157,7 +233,6 @@ export function getSuspendedCachePool(): SpawnedCachePool | null {
233 if (!enableCache) {
234 return null;
235 }
160 -
236 // We check the cache on the stack first, since that's the one any new Caches
237 // would have accessed.
238 let pool = pooledCache;
packages/react-reconciler/src/ReactFiberCommitWork.new.js
+117
@@ -24,6 +24,7 @@ import type {FunctionComponentUpdateQueue} from './ReactFiberHooks.new';
24 import type {Wakeable} from 'shared/ReactTypes';
25 import type {OffscreenState} from './ReactFiberOffscreenComponent';
26 import type {HookFlags} from './ReactHookEffectTags';
27 +import type {Cache} from './ReactFiberCacheComponent.new';
28
29 import {
30 enableCreateEventHandleAPI,
@@ -39,6 +40,7 @@ import {
40 enableSuspenseLayoutEffectSemantics,
41 enableUpdaterTracking,
42 warnAboutCallbackRefReturningFunction,
43 + enableCache,
44 } from 'shared/ReactFeatureFlags';
45 import {
46 FunctionComponent,
@@ -58,6 +60,7 @@ import {
60 ScopeComponent,
61 OffscreenComponent,
62 LegacyHiddenComponent,
63 + CacheComponent,
64 } from './ReactWorkTags';
65 import {detachDeletedInstance} from './ReactFiberHostConfig';
66 import {
@@ -153,6 +156,7 @@ import {
156 markComponentLayoutEffectUnmountStarted,
157 markComponentLayoutEffectUnmountStopped,
158 } from './SchedulingProfiler';
159 +import {releaseCache, retainCache} from './ReactFiberCacheComponent.new';
160
161 let didWarnAboutUndefinedSnapshotBeforeUpdate: Set<mixed> | null = null;
162 if (__DEV__) {
@@ -2665,6 +2669,82 @@ function commitPassiveMountOnFiber(
2669 }
2670 break;
2671 }
2672 + case HostRoot: {
2673 + if (enableCache) {
2674 + let previousCache: Cache | null = null;
2675 + if (finishedWork.alternate !== null) {
2676 + previousCache = finishedWork.alternate.memoizedState.cache;
2677 + }
2678 + const nextCache = finishedWork.memoizedState.cache;
2679 + // Retain/release the root cache.
2680 + // Note that on initial mount, previousCache and nextCache will be the same
2681 + // and this retain won't occur. To counter this, we instead retain the HostRoot's
2682 + // initial cache when creating the root itself (see createFiberRoot() in
2683 + // ReactFiberRoot.js). Subsequent updates that change the cache are reflected
2684 + // here, such that previous/next caches are retained correctly.
2685 + if (nextCache !== previousCache) {
2686 + retainCache(nextCache);
2687 + if (previousCache != null) {
2688 + releaseCache(previousCache);
2689 + }
2690 + }
2691 + }
2692 + break;
2693 + }
2694 + case LegacyHiddenComponent:
2695 + case OffscreenComponent: {
2696 + if (enableCache) {
2697 + let previousCache: Cache | null = null;
2698 + if (
2699 + finishedWork.alternate !== null &&
2700 + finishedWork.alternate.memoizedState !== null &&
2701 + finishedWork.alternate.memoizedState.cachePool !== null
2702 + ) {
2703 + previousCache = finishedWork.alternate.memoizedState.cachePool.pool;
2704 + }
2705 + let nextCache: Cache | null = null;
2706 + if (
2707 + finishedWork.memoizedState !== null &&
2708 + finishedWork.memoizedState.cachePool !== null
2709 + ) {
2710 + nextCache = finishedWork.memoizedState.cachePool.pool;
2711 + }
2712 + // Retain/release the cache used for pending (suspended) nodes.
2713 + // Note that this is only reached in the non-suspended/visible case:
2714 + // when the content is suspended/hidden, the retain/release occurs
2715 + // via the parent Suspense component (see case above).
2716 + if (nextCache !== previousCache) {
2717 + if (nextCache != null) {
2718 + retainCache(nextCache);
2719 + }
2720 + if (previousCache != null) {
2721 + releaseCache(previousCache);
2722 + }
2723 + }
2724 + }
2725 + break;
2726 + }
2727 + case CacheComponent: {
2728 + if (enableCache) {
2729 + let previousCache: Cache | null = null;
2730 + if (finishedWork.alternate !== null) {
2731 + previousCache = finishedWork.alternate.memoizedState.cache;
2732 + }
2733 + const nextCache = finishedWork.memoizedState.cache;
2734 + // Retain/release the cache. In theory the cache component
2735 + // could be "borrowing" a cache instance owned by some parent,
2736 + // in which case we could avoid retaining/releasing. But it
2737 + // is non-trivial to determine when that is the case, so we
2738 + // always retain/release.
2739 + if (nextCache !== previousCache) {
2740 + retainCache(nextCache);
2741 + if (previousCache != null) {
2742 + releaseCache(previousCache);
2743 + }
2744 + }
2745 + }
2746 + break;
2747 + }
2748 }
2749 }
2750
@@ -2871,6 +2951,43 @@ function commitPassiveUnmountInsideDeletedTreeOnFiber(
2951 }
2952 break;
2953 }
2954 + // TODO: run passive unmount effects when unmounting a root.
2955 + // Because passive unmount effects are not currently run,
2956 + // the cache instance owned by the root will never be freed.
2957 + // When effects are run, the cache should be freed here:
2958 + // case HostRoot: {
2959 + // if (enableCache) {
2960 + // const cache = current.memoizedState.cache;
2961 + // releaseCache(cache);
2962 + // }
2963 + // break;
2964 + // }
2965 + case LegacyHiddenComponent:
2966 + case OffscreenComponent: {
2967 + if (enableCache) {
2968 + if (
2969 + current.memoizedState !== null &&
2970 + current.memoizedState.cachePool !== null
2971 + ) {
2972 + const cache: Cache = current.memoizedState.cachePool.pool;
2973 + // Retain/release the cache used for pending (suspended) nodes.
2974 + // Note that this is only reached in the non-suspended/visible case:
2975 + // when the content is suspended/hidden, the retain/release occurs
2976 + // via the parent Suspense component (see case above).
2977 + if (cache != null) {
2978 + retainCache(cache);
2979 + }
2980 + }
2981 + }
2982 + break;
2983 + }
2984 + case CacheComponent: {
2985 + if (enableCache) {
2986 + const cache = current.memoizedState.cache;
2987 + releaseCache(cache);
2988 + }
2989 + break;
2990 + }
2991 }
2992 }
2993
packages/react-reconciler/src/ReactFiberCommitWork.old.js
+117
@@ -24,6 +24,7 @@ import type {FunctionComponentUpdateQueue} from './ReactFiberHooks.old';
24 import type {Wakeable} from 'shared/ReactTypes';
25 import type {OffscreenState} from './ReactFiberOffscreenComponent';
26 import type {HookFlags} from './ReactHookEffectTags';
27 +import type {Cache} from './ReactFiberCacheComponent.old';
28
29 import {
30 enableCreateEventHandleAPI,
@@ -39,6 +40,7 @@ import {
40 enableSuspenseLayoutEffectSemantics,
41 enableUpdaterTracking,
42 warnAboutCallbackRefReturningFunction,
43 + enableCache,
44 } from 'shared/ReactFeatureFlags';
45 import {
46 FunctionComponent,
@@ -58,6 +60,7 @@ import {
60 ScopeComponent,
61 OffscreenComponent,
62 LegacyHiddenComponent,
63 + CacheComponent,
64 } from './ReactWorkTags';
65 import {detachDeletedInstance} from './ReactFiberHostConfig';
66 import {
@@ -153,6 +156,7 @@ import {
156 markComponentLayoutEffectUnmountStarted,
157 markComponentLayoutEffectUnmountStopped,
158 } from './SchedulingProfiler';
159 +import {releaseCache, retainCache} from './ReactFiberCacheComponent.old';
160
161 let didWarnAboutUndefinedSnapshotBeforeUpdate: Set<mixed> | null = null;
162 if (__DEV__) {
@@ -2665,6 +2669,82 @@ function commitPassiveMountOnFiber(
2669 }
2670 break;
2671 }
2672 + case HostRoot: {
2673 + if (enableCache) {
2674 + let previousCache: Cache | null = null;
2675 + if (finishedWork.alternate !== null) {
2676 + previousCache = finishedWork.alternate.memoizedState.cache;
2677 + }
2678 + const nextCache = finishedWork.memoizedState.cache;
2679 + // Retain/release the root cache.
2680 + // Note that on initial mount, previousCache and nextCache will be the same
2681 + // and this retain won't occur. To counter this, we instead retain the HostRoot's
2682 + // initial cache when creating the root itself (see createFiberRoot() in
2683 + // ReactFiberRoot.js). Subsequent updates that change the cache are reflected
2684 + // here, such that previous/next caches are retained correctly.
2685 + if (nextCache !== previousCache) {
2686 + retainCache(nextCache);
2687 + if (previousCache != null) {
2688 + releaseCache(previousCache);
2689 + }
2690 + }
2691 + }
2692 + break;
2693 + }
2694 + case LegacyHiddenComponent:
2695 + case OffscreenComponent: {
2696 + if (enableCache) {
2697 + let previousCache: Cache | null = null;
2698 + if (
2699 + finishedWork.alternate !== null &&
2700 + finishedWork.alternate.memoizedState !== null &&
2701 + finishedWork.alternate.memoizedState.cachePool !== null
2702 + ) {
2703 + previousCache = finishedWork.alternate.memoizedState.cachePool.pool;
2704 + }
2705 + let nextCache: Cache | null = null;
2706 + if (
2707 + finishedWork.memoizedState !== null &&
2708 + finishedWork.memoizedState.cachePool !== null
2709 + ) {
2710 + nextCache = finishedWork.memoizedState.cachePool.pool;
2711 + }
2712 + // Retain/release the cache used for pending (suspended) nodes.
2713 + // Note that this is only reached in the non-suspended/visible case:
2714 + // when the content is suspended/hidden, the retain/release occurs
2715 + // via the parent Suspense component (see case above).
2716 + if (nextCache !== previousCache) {
2717 + if (nextCache != null) {
2718 + retainCache(nextCache);
2719 + }
2720 + if (previousCache != null) {
2721 + releaseCache(previousCache);
2722 + }
2723 + }
2724 + }
2725 + break;
2726 + }
2727 + case CacheComponent: {
2728 + if (enableCache) {
2729 + let previousCache: Cache | null = null;
2730 + if (finishedWork.alternate !== null) {
2731 + previousCache = finishedWork.alternate.memoizedState.cache;
2732 + }
2733 + const nextCache = finishedWork.memoizedState.cache;
2734 + // Retain/release the cache. In theory the cache component
2735 + // could be "borrowing" a cache instance owned by some parent,
2736 + // in which case we could avoid retaining/releasing. But it
2737 + // is non-trivial to determine when that is the case, so we
2738 + // always retain/release.
2739 + if (nextCache !== previousCache) {
2740 + retainCache(nextCache);
2741 + if (previousCache != null) {
2742 + releaseCache(previousCache);
2743 + }
2744 + }
2745 + }
2746 + break;
2747 + }
2748 }
2749 }
2750
@@ -2871,6 +2951,43 @@ function commitPassiveUnmountInsideDeletedTreeOnFiber(
2951 }
2952 break;
2953 }
2954 + // TODO: run passive unmount effects when unmounting a root.
2955 + // Because passive unmount effects are not currently run,
2956 + // the cache instance owned by the root will never be freed.
2957 + // When effects are run, the cache should be freed here:
2958 + // case HostRoot: {
2959 + // if (enableCache) {
2960 + // const cache = current.memoizedState.cache;
2961 + // releaseCache(cache);
2962 + // }
2963 + // break;
2964 + // }
2965 + case LegacyHiddenComponent:
2966 + case OffscreenComponent: {
2967 + if (enableCache) {
2968 + if (
2969 + current.memoizedState !== null &&
2970 + current.memoizedState.cachePool !== null
2971 + ) {
2972 + const cache: Cache = current.memoizedState.cachePool.pool;
2973 + // Retain/release the cache used for pending (suspended) nodes.
2974 + // Note that this is only reached in the non-suspended/visible case:
2975 + // when the content is suspended/hidden, the retain/release occurs
2976 + // via the parent Suspense component (see case above).
2977 + if (cache != null) {
2978 + retainCache(cache);
2979 + }
2980 + }
2981 + }
2982 + break;
2983 + }
2984 + case CacheComponent: {
2985 + if (enableCache) {
2986 + const cache = current.memoizedState.cache;
2987 + releaseCache(cache);
2988 + }
2989 + break;
2990 + }
2991 }
2992 }
2993
packages/react-reconciler/src/ReactFiberCompleteWork.new.js
+59
@@ -72,6 +72,7 @@ import {
72 ChildDeletion,
73 StaticMask,
74 MutationMask,
75 + Passive,
76 } from './ReactFiberFlags';
77
78 import {
@@ -848,7 +849,15 @@ function completeWork(
849 if (enableCache) {
850 popRootCachePool(fiberRoot, renderLanes);
851
852 + let previousCache: Cache | null = null;
853 + if (workInProgress.alternate !== null) {
854 + previousCache = workInProgress.alternate.memoizedState.cache;
855 + }
856 const cache: Cache = workInProgress.memoizedState.cache;
857 + if (cache !== previousCache) {
858 + // Run passive effects to retain/release the cache.
859 + workInProgress.flags |= Passive;
860 + }
861 popCacheProvider(workInProgress, cache);
862 }
863 popHostContainer(workInProgress);
@@ -1089,6 +1098,29 @@ function completeWork(
1098 prevDidTimeout = prevState !== null;
1099 }
1100
1101 + if (enableCache && nextDidTimeout) {
1102 + const offscreenFiber: Fiber = (workInProgress.child: any);
1103 + let previousCache: Cache | null = null;
1104 + if (
1105 + offscreenFiber.alternate !== null &&
1106 + offscreenFiber.alternate.memoizedState !== null &&
1107 + offscreenFiber.alternate.memoizedState.cachePool !== null
1108 + ) {
1109 + previousCache = offscreenFiber.alternate.memoizedState.cachePool.pool;
1110 + }
1111 + let cache: Cache | null = null;
1112 + if (
1113 + offscreenFiber.memoizedState !== null &&
1114 + offscreenFiber.memoizedState.cachePool !== null
1115 + ) {
1116 + cache = offscreenFiber.memoizedState.cachePool.pool;
1117 + }
1118 + if (cache !== previousCache) {
1119 + // Run passive effects to retain/release the cache.
1120 + offscreenFiber.flags |= Passive;
1121 + }
1122 + }
1123 +
1124 // If the suspended state of the boundary changes, we need to schedule
1125 // an effect to toggle the subtree's visibility. When we switch from
1126 // fallback -> primary, the inner Offscreen fiber schedules this effect
@@ -1465,6 +1497,25 @@ function completeWork(
1497 }
1498
1499 if (enableCache) {
1500 + let previousCache: Cache | null = null;
1501 + if (
1502 + workInProgress.alternate !== null &&
1503 + workInProgress.alternate.memoizedState !== null &&
1504 + workInProgress.alternate.memoizedState.cachePool !== null
1505 + ) {
1506 + previousCache = workInProgress.alternate.memoizedState.cachePool.pool;
1507 + }
1508 + let cache: Cache | null = null;
1509 + if (
1510 + workInProgress.memoizedState !== null &&
1511 + workInProgress.memoizedState.cachePool !== null
1512 + ) {
1513 + cache = workInProgress.memoizedState.cachePool.pool;
1514 + }
1515 + if (cache !== previousCache) {
1516 + // Run passive effects to retain/release the cache.
1517 + workInProgress.flags |= Passive;
1518 + }
1519 const spawnedCachePool: SpawnedCachePool | null = (workInProgress.updateQueue: any);
1520 if (spawnedCachePool !== null) {
1521 popCachePool(workInProgress);
@@ -1475,7 +1526,15 @@ function completeWork(
1526 }
1527 case CacheComponent: {
1528 if (enableCache) {
1529 + let previousCache: Cache | null = null;
1530 + if (workInProgress.alternate !== null) {
1531 + previousCache = workInProgress.alternate.memoizedState.cache;
1532 + }
1533 const cache: Cache = workInProgress.memoizedState.cache;
1534 + if (cache !== previousCache) {
1535 + // Run passive effects to retain/release the cache.
1536 + workInProgress.flags |= Passive;
1537 + }
1538 popCacheProvider(workInProgress, cache);
1539 bubbleProperties(workInProgress);
1540 return null;
packages/react-reconciler/src/ReactFiberCompleteWork.old.js
+59
@@ -72,6 +72,7 @@ import {
72 ChildDeletion,
73 StaticMask,
74 MutationMask,
75 + Passive,
76 } from './ReactFiberFlags';
77
78 import {
@@ -848,7 +849,15 @@ function completeWork(
849 if (enableCache) {
850 popRootCachePool(fiberRoot, renderLanes);
851
852 + let previousCache: Cache | null = null;
853 + if (workInProgress.alternate !== null) {
854 + previousCache = workInProgress.alternate.memoizedState.cache;
855 + }
856 const cache: Cache = workInProgress.memoizedState.cache;
857 + if (cache !== previousCache) {
858 + // Run passive effects to retain/release the cache.
859 + workInProgress.flags |= Passive;
860 + }
861 popCacheProvider(workInProgress, cache);
862 }
863 popHostContainer(workInProgress);
@@ -1089,6 +1098,29 @@ function completeWork(
1098 prevDidTimeout = prevState !== null;
1099 }
1100
1101 + if (enableCache && nextDidTimeout) {
1102 + const offscreenFiber: Fiber = (workInProgress.child: any);
1103 + let previousCache: Cache | null = null;
1104 + if (
1105 + offscreenFiber.alternate !== null &&
1106 + offscreenFiber.alternate.memoizedState !== null &&
1107 + offscreenFiber.alternate.memoizedState.cachePool !== null
1108 + ) {
1109 + previousCache = offscreenFiber.alternate.memoizedState.cachePool.pool;
1110 + }
1111 + let cache: Cache | null = null;
1112 + if (
1113 + offscreenFiber.memoizedState !== null &&
1114 + offscreenFiber.memoizedState.cachePool !== null
1115 + ) {
1116 + cache = offscreenFiber.memoizedState.cachePool.pool;
1117 + }
1118 + if (cache !== previousCache) {
1119 + // Run passive effects to retain/release the cache.
1120 + offscreenFiber.flags |= Passive;
1121 + }
1122 + }
1123 +
1124 // If the suspended state of the boundary changes, we need to schedule
1125 // an effect to toggle the subtree's visibility. When we switch from
1126 // fallback -> primary, the inner Offscreen fiber schedules this effect
@@ -1465,6 +1497,25 @@ function completeWork(
1497 }
1498
1499 if (enableCache) {
1500 + let previousCache: Cache | null = null;
1501 + if (
1502 + workInProgress.alternate !== null &&
1503 + workInProgress.alternate.memoizedState !== null &&
1504 + workInProgress.alternate.memoizedState.cachePool !== null
1505 + ) {
1506 + previousCache = workInProgress.alternate.memoizedState.cachePool.pool;
1507 + }
1508 + let cache: Cache | null = null;
1509 + if (
1510 + workInProgress.memoizedState !== null &&
1511 + workInProgress.memoizedState.cachePool !== null
1512 + ) {
1513 + cache = workInProgress.memoizedState.cachePool.pool;
1514 + }
1515 + if (cache !== previousCache) {
1516 + // Run passive effects to retain/release the cache.
1517 + workInProgress.flags |= Passive;
1518 + }
1519 const spawnedCachePool: SpawnedCachePool | null = (workInProgress.updateQueue: any);
1520 if (spawnedCachePool !== null) {
1521 popCachePool(workInProgress);
@@ -1475,7 +1526,15 @@ function completeWork(
1526 }
1527 case CacheComponent: {
1528 if (enableCache) {
1529 + let previousCache: Cache | null = null;
1530 + if (workInProgress.alternate !== null) {
1531 + previousCache = workInProgress.alternate.memoizedState.cache;
1532 + }
1533 const cache: Cache = workInProgress.memoizedState.cache;
1534 + if (cache !== previousCache) {
1535 + // Run passive effects to retain/release the cache.
1536 + workInProgress.flags |= Passive;
1537 + }
1538 popCacheProvider(workInProgress, cache);
1539 bubbleProperties(workInProgress);
1540 return null;
packages/react-reconciler/src/ReactFiberHooks.new.js
+30 -5
@@ -110,7 +110,7 @@ import {
110 import {getIsRendering} from './ReactCurrentFiber';
111 import {logStateUpdateScheduled} from './DebugTracing';
112 import {markStateUpdateScheduled} from './SchedulingProfiler';
113 -import {CacheContext} from './ReactFiberCacheComponent.new';
113 +import {createCache, CacheContext} from './ReactFiberCacheComponent.new';
114 import {
115 createUpdate as createLegacyQueueUpdate,
116 enqueueUpdate as enqueueLegacyQueueUpdate,
@@ -2124,6 +2124,9 @@ function updateRefresh() {
2124 }
2125
2126 function refreshCache<T>(fiber: Fiber, seedKey: ?() => T, seedValue: T) {
2127 + if (!enableCache) {
2128 + return;
2129 + }
2130 // TODO: Does Cache work in legacy mode? Should decide and write a test.
2131 // TODO: Consider warning if the refresh is at discrete priority, or if we
2132 // otherwise suspect that it wasn't batched properly.
@@ -2139,11 +2142,14 @@ function refreshCache<T>(fiber: Fiber, seedKey: ?() => T, seedValue: T) {
2142 entangleLegacyQueueTransitions(root, provider, lane);
2143 }
2144
2142 - const seededCache = new Map();
2145 + // TODO: If a refresh never commits, the new cache created here must be
2146 + // released. A simple case is start refreshing a cache boundary, but then
2147 + // unmount that boundary before the refresh completes.
2148 + const seededCache = createCache();
2149 if (seedKey !== null && seedKey !== undefined && root !== null) {
2150 // Seed the cache with the value passed by the caller. This could be
2151 // from a server mutation, or it could be a streaming response.
2146 - seededCache.set(seedKey, seedValue);
2152 + seededCache.data.set(seedKey, seedValue);
2153 }
2154
2155 // Schedule an update on the cache boundary to trigger a refresh.
@@ -2390,15 +2396,23 @@ function markUpdateInDevTools(fiber, lane, action) {
2396 }
2397 }
2398
2399 +function getCacheSignal(): AbortSignal {
2400 + if (!enableCache) {
2401 + throw new Error('Not implemented.');
2402 + }
2403 + const cache: Cache = readContext(CacheContext);
2404 + return cache.controller.signal;
2405 +}
2406 +
2407 function getCacheForType<T>(resourceType: () => T): T {
2408 if (!enableCache) {
2409 throw new Error('Not implemented.');
2410 }
2411 const cache: Cache = readContext(CacheContext);
2398 - let cacheForType: T | void = (cache.get(resourceType): any);
2412 + let cacheForType: T | void = (cache.data.get(resourceType): any);
2413 if (cacheForType === undefined) {
2414 cacheForType = resourceType();
2401 - cache.set(resourceType, cacheForType);
2415 + cache.data.set(resourceType, cacheForType);
2416 }
2417 return cacheForType;
2418 }
@@ -2426,6 +2440,7 @@ export const ContextOnlyDispatcher: Dispatcher = {
2440 unstable_isNewReconciler: enableNewReconciler,
2441 };
2442 if (enableCache) {
2443 + (ContextOnlyDispatcher: Dispatcher).getCacheSignal = getCacheSignal;
2444 (ContextOnlyDispatcher: Dispatcher).getCacheForType = getCacheForType;
2445 (ContextOnlyDispatcher: Dispatcher).useCacheRefresh = throwInvalidHookError;
2446 }
@@ -2453,6 +2468,7 @@ const HooksDispatcherOnMount: Dispatcher = {
2468 unstable_isNewReconciler: enableNewReconciler,
2469 };
2470 if (enableCache) {
2471 + (HooksDispatcherOnMount: Dispatcher).getCacheSignal = getCacheSignal;
2472 (HooksDispatcherOnMount: Dispatcher).getCacheForType = getCacheForType;
2473 (HooksDispatcherOnMount: Dispatcher).useCacheRefresh = mountRefresh;
2474 }
@@ -2480,6 +2496,7 @@ const HooksDispatcherOnUpdate: Dispatcher = {
2496 unstable_isNewReconciler: enableNewReconciler,
2497 };
2498 if (enableCache) {
2499 + (HooksDispatcherOnUpdate: Dispatcher).getCacheSignal = getCacheSignal;
2500 (HooksDispatcherOnUpdate: Dispatcher).getCacheForType = getCacheForType;
2501 (HooksDispatcherOnUpdate: Dispatcher).useCacheRefresh = updateRefresh;
2502 }
@@ -2507,6 +2524,7 @@ const HooksDispatcherOnRerender: Dispatcher = {
2524 unstable_isNewReconciler: enableNewReconciler,
2525 };
2526 if (enableCache) {
2527 + (HooksDispatcherOnRerender: Dispatcher).getCacheSignal = getCacheSignal;
2528 (HooksDispatcherOnRerender: Dispatcher).getCacheForType = getCacheForType;
2529 (HooksDispatcherOnRerender: Dispatcher).useCacheRefresh = updateRefresh;
2530 }
@@ -2677,6 +2695,7 @@ if (__DEV__) {
2695 unstable_isNewReconciler: enableNewReconciler,
2696 };
2697 if (enableCache) {
2698 + (HooksDispatcherOnMountInDEV: Dispatcher).getCacheSignal = getCacheSignal;
2699 (HooksDispatcherOnMountInDEV: Dispatcher).getCacheForType = getCacheForType;
2700 (HooksDispatcherOnMountInDEV: Dispatcher).useCacheRefresh = function useCacheRefresh() {
2701 currentHookNameInDev = 'useCacheRefresh';
@@ -2818,6 +2837,7 @@ if (__DEV__) {
2837 unstable_isNewReconciler: enableNewReconciler,
2838 };
2839 if (enableCache) {
2840 + (HooksDispatcherOnMountWithHookTypesInDEV: Dispatcher).getCacheSignal = getCacheSignal;
2841 (HooksDispatcherOnMountWithHookTypesInDEV: Dispatcher).getCacheForType = getCacheForType;
2842 (HooksDispatcherOnMountWithHookTypesInDEV: Dispatcher).useCacheRefresh = function useCacheRefresh() {
2843 currentHookNameInDev = 'useCacheRefresh';
@@ -2959,6 +2979,7 @@ if (__DEV__) {
2979 unstable_isNewReconciler: enableNewReconciler,
2980 };
2981 if (enableCache) {
2982 + (HooksDispatcherOnUpdateInDEV: Dispatcher).getCacheSignal = getCacheSignal;
2983 (HooksDispatcherOnUpdateInDEV: Dispatcher).getCacheForType = getCacheForType;
2984 (HooksDispatcherOnUpdateInDEV: Dispatcher).useCacheRefresh = function useCacheRefresh() {
2985 currentHookNameInDev = 'useCacheRefresh';
@@ -3101,6 +3122,7 @@ if (__DEV__) {
3122 unstable_isNewReconciler: enableNewReconciler,
3123 };
3124 if (enableCache) {
3125 + (HooksDispatcherOnRerenderInDEV: Dispatcher).getCacheSignal = getCacheSignal;
3126 (HooksDispatcherOnRerenderInDEV: Dispatcher).getCacheForType = getCacheForType;
3127 (HooksDispatcherOnRerenderInDEV: Dispatcher).useCacheRefresh = function useCacheRefresh() {
3128 currentHookNameInDev = 'useCacheRefresh';
@@ -3259,6 +3281,7 @@ if (__DEV__) {
3281 unstable_isNewReconciler: enableNewReconciler,
3282 };
3283 if (enableCache) {
3284 + (InvalidNestedHooksDispatcherOnMountInDEV: Dispatcher).getCacheSignal = getCacheSignal;
3285 (InvalidNestedHooksDispatcherOnMountInDEV: Dispatcher).getCacheForType = getCacheForType;
3286 (InvalidNestedHooksDispatcherOnMountInDEV: Dispatcher).useCacheRefresh = function useCacheRefresh() {
3287 currentHookNameInDev = 'useCacheRefresh';
@@ -3417,6 +3440,7 @@ if (__DEV__) {
3440 unstable_isNewReconciler: enableNewReconciler,
3441 };
3442 if (enableCache) {
3443 + (InvalidNestedHooksDispatcherOnUpdateInDEV: Dispatcher).getCacheSignal = getCacheSignal;
3444 (InvalidNestedHooksDispatcherOnUpdateInDEV: Dispatcher).getCacheForType = getCacheForType;
3445 (InvalidNestedHooksDispatcherOnUpdateInDEV: Dispatcher).useCacheRefresh = function useCacheRefresh() {
3446 currentHookNameInDev = 'useCacheRefresh';
@@ -3576,6 +3600,7 @@ if (__DEV__) {
3600 unstable_isNewReconciler: enableNewReconciler,
3601 };
3602 if (enableCache) {
3603 + (InvalidNestedHooksDispatcherOnRerenderInDEV: Dispatcher).getCacheSignal = getCacheSignal;
3604 (InvalidNestedHooksDispatcherOnRerenderInDEV: Dispatcher).getCacheForType = getCacheForType;
3605 (InvalidNestedHooksDispatcherOnRerenderInDEV: Dispatcher).useCacheRefresh = function useCacheRefresh() {
3606 currentHookNameInDev = 'useCacheRefresh';
packages/react-reconciler/src/ReactFiberHooks.old.js
+30 -5
@@ -110,7 +110,7 @@ import {
110 import {getIsRendering} from './ReactCurrentFiber';
111 import {logStateUpdateScheduled} from './DebugTracing';
112 import {markStateUpdateScheduled} from './SchedulingProfiler';
113 -import {CacheContext} from './ReactFiberCacheComponent.old';
113 +import {createCache, CacheContext} from './ReactFiberCacheComponent.old';
114 import {
115 createUpdate as createLegacyQueueUpdate,
116 enqueueUpdate as enqueueLegacyQueueUpdate,
@@ -2124,6 +2124,9 @@ function updateRefresh() {
2124 }
2125
2126 function refreshCache<T>(fiber: Fiber, seedKey: ?() => T, seedValue: T) {
2127 + if (!enableCache) {
2128 + return;
2129 + }
2130 // TODO: Does Cache work in legacy mode? Should decide and write a test.
2131 // TODO: Consider warning if the refresh is at discrete priority, or if we
2132 // otherwise suspect that it wasn't batched properly.
@@ -2139,11 +2142,14 @@ function refreshCache<T>(fiber: Fiber, seedKey: ?() => T, seedValue: T) {
2142 entangleLegacyQueueTransitions(root, provider, lane);
2143 }
2144
2142 - const seededCache = new Map();
2145 + // TODO: If a refresh never commits, the new cache created here must be
2146 + // released. A simple case is start refreshing a cache boundary, but then
2147 + // unmount that boundary before the refresh completes.
2148 + const seededCache = createCache();
2149 if (seedKey !== null && seedKey !== undefined && root !== null) {
2150 // Seed the cache with the value passed by the caller. This could be
2151 // from a server mutation, or it could be a streaming response.
2146 - seededCache.set(seedKey, seedValue);
2152 + seededCache.data.set(seedKey, seedValue);
2153 }
2154
2155 // Schedule an update on the cache boundary to trigger a refresh.
@@ -2390,15 +2396,23 @@ function markUpdateInDevTools(fiber, lane, action) {
2396 }
2397 }
2398
2399 +function getCacheSignal(): AbortSignal {
2400 + if (!enableCache) {
2401 + throw new Error('Not implemented.');
2402 + }
2403 + const cache: Cache = readContext(CacheContext);
2404 + return cache.controller.signal;
2405 +}
2406 +
2407 function getCacheForType<T>(resourceType: () => T): T {
2408 if (!enableCache) {
2409 throw new Error('Not implemented.');
2410 }
2411 const cache: Cache = readContext(CacheContext);
2398 - let cacheForType: T | void = (cache.get(resourceType): any);
2412 + let cacheForType: T | void = (cache.data.get(resourceType): any);
2413 if (cacheForType === undefined) {
2414 cacheForType = resourceType();
2401 - cache.set(resourceType, cacheForType);
2415 + cache.data.set(resourceType, cacheForType);
2416 }
2417 return cacheForType;
2418 }
@@ -2426,6 +2440,7 @@ export const ContextOnlyDispatcher: Dispatcher = {
2440 unstable_isNewReconciler: enableNewReconciler,
2441 };
2442 if (enableCache) {
2443 + (ContextOnlyDispatcher: Dispatcher).getCacheSignal = getCacheSignal;
2444 (ContextOnlyDispatcher: Dispatcher).getCacheForType = getCacheForType;
2445 (ContextOnlyDispatcher: Dispatcher).useCacheRefresh = throwInvalidHookError;
2446 }
@@ -2453,6 +2468,7 @@ const HooksDispatcherOnMount: Dispatcher = {
2468 unstable_isNewReconciler: enableNewReconciler,
2469 };
2470 if (enableCache) {
2471 + (HooksDispatcherOnMount: Dispatcher).getCacheSignal = getCacheSignal;
2472 (HooksDispatcherOnMount: Dispatcher).getCacheForType = getCacheForType;
2473 (HooksDispatcherOnMount: Dispatcher).useCacheRefresh = mountRefresh;
2474 }
@@ -2480,6 +2496,7 @@ const HooksDispatcherOnUpdate: Dispatcher = {
2496 unstable_isNewReconciler: enableNewReconciler,
2497 };
2498 if (enableCache) {
2499 + (HooksDispatcherOnUpdate: Dispatcher).getCacheSignal = getCacheSignal;
2500 (HooksDispatcherOnUpdate: Dispatcher).getCacheForType = getCacheForType;
2501 (HooksDispatcherOnUpdate: Dispatcher).useCacheRefresh = updateRefresh;
2502 }
@@ -2507,6 +2524,7 @@ const HooksDispatcherOnRerender: Dispatcher = {
2524 unstable_isNewReconciler: enableNewReconciler,
2525 };
2526 if (enableCache) {
2527 + (HooksDispatcherOnRerender: Dispatcher).getCacheSignal = getCacheSignal;
2528 (HooksDispatcherOnRerender: Dispatcher).getCacheForType = getCacheForType;
2529 (HooksDispatcherOnRerender: Dispatcher).useCacheRefresh = updateRefresh;
2530 }
@@ -2677,6 +2695,7 @@ if (__DEV__) {
2695 unstable_isNewReconciler: enableNewReconciler,
2696 };
2697 if (enableCache) {
2698 + (HooksDispatcherOnMountInDEV: Dispatcher).getCacheSignal = getCacheSignal;
2699 (HooksDispatcherOnMountInDEV: Dispatcher).getCacheForType = getCacheForType;
2700 (HooksDispatcherOnMountInDEV: Dispatcher).useCacheRefresh = function useCacheRefresh() {
2701 currentHookNameInDev = 'useCacheRefresh';
@@ -2818,6 +2837,7 @@ if (__DEV__) {
2837 unstable_isNewReconciler: enableNewReconciler,
2838 };
2839 if (enableCache) {
2840 + (HooksDispatcherOnMountWithHookTypesInDEV: Dispatcher).getCacheSignal = getCacheSignal;
2841 (HooksDispatcherOnMountWithHookTypesInDEV: Dispatcher).getCacheForType = getCacheForType;
2842 (HooksDispatcherOnMountWithHookTypesInDEV: Dispatcher).useCacheRefresh = function useCacheRefresh() {
2843 currentHookNameInDev = 'useCacheRefresh';
@@ -2959,6 +2979,7 @@ if (__DEV__) {
2979 unstable_isNewReconciler: enableNewReconciler,
2980 };
2981 if (enableCache) {
2982 + (HooksDispatcherOnUpdateInDEV: Dispatcher).getCacheSignal = getCacheSignal;
2983 (HooksDispatcherOnUpdateInDEV: Dispatcher).getCacheForType = getCacheForType;
2984 (HooksDispatcherOnUpdateInDEV: Dispatcher).useCacheRefresh = function useCacheRefresh() {
2985 currentHookNameInDev = 'useCacheRefresh';
@@ -3101,6 +3122,7 @@ if (__DEV__) {
3122 unstable_isNewReconciler: enableNewReconciler,
3123 };
3124 if (enableCache) {
3125 + (HooksDispatcherOnRerenderInDEV: Dispatcher).getCacheSignal = getCacheSignal;
3126 (HooksDispatcherOnRerenderInDEV: Dispatcher).getCacheForType = getCacheForType;
3127 (HooksDispatcherOnRerenderInDEV: Dispatcher).useCacheRefresh = function useCacheRefresh() {
3128 currentHookNameInDev = 'useCacheRefresh';
@@ -3259,6 +3281,7 @@ if (__DEV__) {
3281 unstable_isNewReconciler: enableNewReconciler,
3282 };
3283 if (enableCache) {
3284 + (InvalidNestedHooksDispatcherOnMountInDEV: Dispatcher).getCacheSignal = getCacheSignal;
3285 (InvalidNestedHooksDispatcherOnMountInDEV: Dispatcher).getCacheForType = getCacheForType;
3286 (InvalidNestedHooksDispatcherOnMountInDEV: Dispatcher).useCacheRefresh = function useCacheRefresh() {
3287 currentHookNameInDev = 'useCacheRefresh';
@@ -3417,6 +3440,7 @@ if (__DEV__) {
3440 unstable_isNewReconciler: enableNewReconciler,
3441 };
3442 if (enableCache) {
3443 + (InvalidNestedHooksDispatcherOnUpdateInDEV: Dispatcher).getCacheSignal = getCacheSignal;
3444 (InvalidNestedHooksDispatcherOnUpdateInDEV: Dispatcher).getCacheForType = getCacheForType;
3445 (InvalidNestedHooksDispatcherOnUpdateInDEV: Dispatcher).useCacheRefresh = function useCacheRefresh() {
3446 currentHookNameInDev = 'useCacheRefresh';
@@ -3576,6 +3600,7 @@ if (__DEV__) {
3600 unstable_isNewReconciler: enableNewReconciler,
3601 };
3602 if (enableCache) {
3603 + (InvalidNestedHooksDispatcherOnRerenderInDEV: Dispatcher).getCacheSignal = getCacheSignal;
3604 (InvalidNestedHooksDispatcherOnRerenderInDEV: Dispatcher).getCacheForType = getCacheForType;
3605 (InvalidNestedHooksDispatcherOnRerenderInDEV: Dispatcher).useCacheRefresh = function useCacheRefresh() {
3606 currentHookNameInDev = 'useCacheRefresh';
packages/react-reconciler/src/ReactFiberLane.new.js
-10
@@ -17,7 +17,6 @@ export type Lane = number;
17 export type LaneMap<T> = Array<T>;
18
19 import {
20 - enableCache,
20 enableSchedulingProfiler,
21 enableUpdaterTracking,
22 allowConcurrentByDefault,
@@ -635,15 +634,6 @@ export function markRootFinished(root: FiberRoot, remainingLanes: Lanes) {
634
635 root.entangledLanes &= remainingLanes;
636
638 - if (enableCache) {
639 - const pooledCacheLanes = (root.pooledCacheLanes &= remainingLanes);
640 - if (pooledCacheLanes === NoLanes) {
641 - // None of the remaining work relies on the cache pool. Clear it so
642 - // subsequent requests get a new cache.
643 - root.pooledCache = null;
644 - }
645 - }
646 -
637 const entanglements = root.entanglements;
638 const eventTimes = root.eventTimes;
639 const expirationTimes = root.expirationTimes;
packages/react-reconciler/src/ReactFiberLane.old.js
-10
@@ -17,7 +17,6 @@ export type Lane = number;
17 export type LaneMap<T> = Array<T>;
18
19 import {
20 - enableCache,
20 enableSchedulingProfiler,
21 enableUpdaterTracking,
22 allowConcurrentByDefault,
@@ -635,15 +634,6 @@ export function markRootFinished(root: FiberRoot, remainingLanes: Lanes) {
634
635 root.entangledLanes &= remainingLanes;
636
638 - if (enableCache) {
639 - const pooledCacheLanes = (root.pooledCacheLanes &= remainingLanes);
640 - if (pooledCacheLanes === NoLanes) {
641 - // None of the remaining work relies on the cache pool. Clear it so
642 - // subsequent requests get a new cache.
643 - root.pooledCache = null;
644 - }
645 - }
646 -
637 const entanglements = root.entanglements;
638 const eventTimes = root.eventTimes;
639 const expirationTimes = root.expirationTimes;
packages/react-reconciler/src/ReactFiberRoot.new.js
+12 -1
@@ -28,6 +28,7 @@ import {
28 } from 'shared/ReactFeatureFlags';
29 import {initializeUpdateQueue} from './ReactUpdateQueue.new';
30 import {LegacyRoot, ConcurrentRoot} from './ReactRootTags';
31 +import {createCache, retainCache} from './ReactFiberCacheComponent.new';
32
33 function FiberRootNode(containerInfo, tag, hydrate) {
34 this.tag = tag;
@@ -117,8 +118,18 @@ export function createFiberRoot(
118 uninitializedFiber.stateNode = root;
119
120 if (enableCache) {
120 - const initialCache = new Map();
121 + const initialCache = createCache();
122 + retainCache(initialCache);
123 +
124 + // The pooledCache is a fresh cache instance that is used temporarily
125 + // for newly mounted boundaries during a render. In general, the
126 + // pooledCache is always cleared from the root at the end of a render:
127 + // it is either released when render commits, or moved to an Offscreen
128 + // component if rendering suspends. Because the lifetime of the pooled
129 + // cache is distinct from the main memoizedState.cache, it must be
130 + // retained separately.
131 root.pooledCache = initialCache;
132 + retainCache(initialCache);
133 const initialState = {
134 element: null,
135 cache: initialCache,
packages/react-reconciler/src/ReactFiberRoot.old.js
+12 -1
@@ -28,6 +28,7 @@ import {
28 } from 'shared/ReactFeatureFlags';
29 import {initializeUpdateQueue} from './ReactUpdateQueue.old';
30 import {LegacyRoot, ConcurrentRoot} from './ReactRootTags';
31 +import {createCache, retainCache} from './ReactFiberCacheComponent.old';
32
33 function FiberRootNode(containerInfo, tag, hydrate) {
34 this.tag = tag;
@@ -117,8 +118,18 @@ export function createFiberRoot(
118 uninitializedFiber.stateNode = root;
119
120 if (enableCache) {
120 - const initialCache = new Map();
121 + const initialCache = createCache();
122 + retainCache(initialCache);
123 +
124 + // The pooledCache is a fresh cache instance that is used temporarily
125 + // for newly mounted boundaries during a render. In general, the
126 + // pooledCache is always cleared from the root at the end of a render:
127 + // it is either released when render commits, or moved to an Offscreen
128 + // component if rendering suspends. Because the lifetime of the pooled
129 + // cache is distinct from the main memoizedState.cache, it must be
130 + // retained separately.
131 root.pooledCache = initialCache;
132 + retainCache(initialCache);
133 const initialState = {
134 element: null,
135 cache: initialCache,
packages/react-reconciler/src/ReactFiberWorkLoop.new.js
+40
@@ -32,6 +32,7 @@ import {
32 skipUnmountedBoundaries,
33 enableUpdaterTracking,
34 warnOnSubscriptionInsideStartTransition,
35 + enableCache,
36 } from 'shared/ReactFeatureFlags';
37 import ReactSharedInternals from 'shared/ReactSharedInternals';
38 import is from 'shared/objectIs';
@@ -234,6 +235,7 @@ import {
235 isDevToolsPresent,
236 } from './ReactFiberDevToolsHook.new';
237 import {onCommitRoot as onCommitRootTestSelector} from './ReactTestSelectors';
238 +import {releaseCache} from './ReactFiberCacheComponent.new';
239
240 const ceil = Math.ceil;
241
@@ -331,6 +333,7 @@ let rootDoesHavePassiveEffects: boolean = false;
333 let rootWithPendingPassiveEffects: FiberRoot | null = null;
334 let pendingPassiveEffectsLanes: Lanes = NoLanes;
335 let pendingPassiveProfilerEffects: Array<Fiber> = [];
336 +let pendingPassiveEffectsRemainingLanes: Lanes = NoLanes;
337
338 // Use these to prevent an infinite loop of nested updates
339 const NESTED_UPDATE_LIMIT = 50;
@@ -1900,8 +1903,12 @@ function commitRootImpl(root, renderPriorityLevel) {
1903 ) {
1904 if (!rootDoesHavePassiveEffects) {
1905 rootDoesHavePassiveEffects = true;
1906 + pendingPassiveEffectsRemainingLanes = remainingLanes;
1907 scheduleCallback(NormalSchedulerPriority, () => {
1908 flushPassiveEffects();
1909 + // This render triggered passive effects: release the root cache pool
1910 + // *after* passive effects fire to avoid freeing a cache pool that may
1911 + // be referenced by a node in the tree (HostRoot, Cache boundary etc)
1912 return null;
1913 });
1914 }
@@ -2027,6 +2034,10 @@ function commitRootImpl(root, renderPriorityLevel) {
2034 rootDoesHavePassiveEffects = false;
2035 rootWithPendingPassiveEffects = root;
2036 pendingPassiveEffectsLanes = lanes;
2037 + } else {
2038 + // There were no passive effects, so we can immediately release the cache
2039 + // pool for this render.
2040 + releaseRootPooledCache(root, remainingLanes);
2041 }
2042
2043 // Read this again, since an effect might have updated it
@@ -2127,6 +2138,21 @@ function commitRootImpl(root, renderPriorityLevel) {
2138 return null;
2139 }
2140
2141 +function releaseRootPooledCache(root: FiberRoot, remainingLanes: Lanes) {
2142 + if (enableCache) {
2143 + const pooledCacheLanes = (root.pooledCacheLanes &= remainingLanes);
2144 + if (pooledCacheLanes === NoLanes) {
2145 + // None of the remaining work relies on the cache pool. Clear it so
2146 + // subsequent requests get a new cache
2147 + const pooledCache = root.pooledCache;
2148 + if (pooledCache != null) {
2149 + root.pooledCache = null;
2150 + releaseCache(pooledCache);
2151 + }
2152 + }
2153 + }
2154 +}
2155 +
2156 export function flushPassiveEffects(): boolean {
2157 // Returns whether passive effects were flushed.
2158 // TODO: Combine this check with the one in flushPassiveEFfectsImpl. We should
@@ -2135,6 +2161,15 @@ export function flushPassiveEffects(): boolean {
2161 // `Scheduler.runWithPriority`, which accepts a function. But now we track the
2162 // priority within React itself, so we can mutate the variable directly.
2163 if (rootWithPendingPassiveEffects !== null) {
2164 + // Cache the root since rootWithPendingPassiveEffects is cleared in
2165 + // flushPassiveEffectsImpl
2166 + const root = rootWithPendingPassiveEffects;
2167 + // Cache and clear the remaining lanes flag; it must be reset since this
2168 + // method can be called from various places, not always from commitRoot
2169 + // where the remaining lanes are known
2170 + const remainingLanes = pendingPassiveEffectsRemainingLanes;
2171 + pendingPassiveEffectsRemainingLanes = NoLanes;
2172 +
2173 const renderPriority = lanesToEventPriority(pendingPassiveEffectsLanes);
2174 const priority = lowerEventPriority(DefaultEventPriority, renderPriority);
2175 const prevTransition = ReactCurrentBatchConfig.transition;
@@ -2146,6 +2181,11 @@ export function flushPassiveEffects(): boolean {
2181 } finally {
2182 setCurrentUpdatePriority(previousPriority);
2183 ReactCurrentBatchConfig.transition = prevTransition;
2184 +
2185 + // Once passive effects have run for the tree - giving components a
2186 + // chance to retain cache instances they use - release the pooled
2187 + // cache at the root (if there is one)
2188 + releaseRootPooledCache(root, remainingLanes);
2189 }
2190 }
2191 return false;
packages/react-reconciler/src/ReactFiberWorkLoop.old.js
+40
@@ -32,6 +32,7 @@ import {
32 skipUnmountedBoundaries,
33 enableUpdaterTracking,
34 warnOnSubscriptionInsideStartTransition,
35 + enableCache,
36 } from 'shared/ReactFeatureFlags';
37 import ReactSharedInternals from 'shared/ReactSharedInternals';
38 import is from 'shared/objectIs';
@@ -234,6 +235,7 @@ import {
235 isDevToolsPresent,
236 } from './ReactFiberDevToolsHook.old';
237 import {onCommitRoot as onCommitRootTestSelector} from './ReactTestSelectors';
238 +import {releaseCache} from './ReactFiberCacheComponent.old';
239
240 const ceil = Math.ceil;
241
@@ -331,6 +333,7 @@ let rootDoesHavePassiveEffects: boolean = false;
333 let rootWithPendingPassiveEffects: FiberRoot | null = null;
334 let pendingPassiveEffectsLanes: Lanes = NoLanes;
335 let pendingPassiveProfilerEffects: Array<Fiber> = [];
336 +let pendingPassiveEffectsRemainingLanes: Lanes = NoLanes;
337
338 // Use these to prevent an infinite loop of nested updates
339 const NESTED_UPDATE_LIMIT = 50;
@@ -1900,8 +1903,12 @@ function commitRootImpl(root, renderPriorityLevel) {
1903 ) {
1904 if (!rootDoesHavePassiveEffects) {
1905 rootDoesHavePassiveEffects = true;
1906 + pendingPassiveEffectsRemainingLanes = remainingLanes;
1907 scheduleCallback(NormalSchedulerPriority, () => {
1908 flushPassiveEffects();
1909 + // This render triggered passive effects: release the root cache pool
1910 + // *after* passive effects fire to avoid freeing a cache pool that may
1911 + // be referenced by a node in the tree (HostRoot, Cache boundary etc)
1912 return null;
1913 });
1914 }
@@ -2027,6 +2034,10 @@ function commitRootImpl(root, renderPriorityLevel) {
2034 rootDoesHavePassiveEffects = false;
2035 rootWithPendingPassiveEffects = root;
2036 pendingPassiveEffectsLanes = lanes;
2037 + } else {
2038 + // There were no passive effects, so we can immediately release the cache
2039 + // pool for this render.
2040 + releaseRootPooledCache(root, remainingLanes);
2041 }
2042
2043 // Read this again, since an effect might have updated it
@@ -2127,6 +2138,21 @@ function commitRootImpl(root, renderPriorityLevel) {
2138 return null;
2139 }
2140
2141 +function releaseRootPooledCache(root: FiberRoot, remainingLanes: Lanes) {
2142 + if (enableCache) {
2143 + const pooledCacheLanes = (root.pooledCacheLanes &= remainingLanes);
2144 + if (pooledCacheLanes === NoLanes) {
2145 + // None of the remaining work relies on the cache pool. Clear it so
2146 + // subsequent requests get a new cache
2147 + const pooledCache = root.pooledCache;
2148 + if (pooledCache != null) {
2149 + root.pooledCache = null;
2150 + releaseCache(pooledCache);
2151 + }
2152 + }
2153 + }
2154 +}
2155 +
2156 export function flushPassiveEffects(): boolean {
2157 // Returns whether passive effects were flushed.
2158 // TODO: Combine this check with the one in flushPassiveEFfectsImpl. We should
@@ -2135,6 +2161,15 @@ export function flushPassiveEffects(): boolean {
2161 // `Scheduler.runWithPriority`, which accepts a function. But now we track the
2162 // priority within React itself, so we can mutate the variable directly.
2163 if (rootWithPendingPassiveEffects !== null) {
2164 + // Cache the root since rootWithPendingPassiveEffects is cleared in
2165 + // flushPassiveEffectsImpl
2166 + const root = rootWithPendingPassiveEffects;
2167 + // Cache and clear the remaining lanes flag; it must be reset since this
2168 + // method can be called from various places, not always from commitRoot
2169 + // where the remaining lanes are known
2170 + const remainingLanes = pendingPassiveEffectsRemainingLanes;
2171 + pendingPassiveEffectsRemainingLanes = NoLanes;
2172 +
2173 const renderPriority = lanesToEventPriority(pendingPassiveEffectsLanes);
2174 const priority = lowerEventPriority(DefaultEventPriority, renderPriority);
2175 const prevTransition = ReactCurrentBatchConfig.transition;
@@ -2146,6 +2181,11 @@ export function flushPassiveEffects(): boolean {
2181 } finally {
2182 setCurrentUpdatePriority(previousPriority);
2183 ReactCurrentBatchConfig.transition = prevTransition;
2184 +
2185 + // Once passive effects have run for the tree - giving components a
2186 + // chance to retain cache instances they use - release the pooled
2187 + // cache at the root (if there is one)
2188 + releaseRootPooledCache(root, remainingLanes);
2189 }
2190 }
2191 return false;
packages/react-reconciler/src/ReactInternalTypes.js
+1
@@ -273,6 +273,7 @@ type BasicStateAction<S> = (S => S) | S;
273 type Dispatch<A> = A => void;
274
275 export type Dispatcher = {|
276 + getCacheSignal?: () => AbortSignal,
277 getCacheForType?: <T>(resourceType: () => T) => T,
278 readContext<T>(context: ReactContext<T>): T,
279 useState<S>(initialState: (() => S) | S): [S, Dispatch<BasicStateAction<S>>],
packages/react-reconciler/src/__tests__/ReactCache-test.js
+638 -13
@@ -1,6 +1,7 @@
1 let React;
2 let ReactNoop;
3 let Cache;
4 +let getCacheSignal;
5 let getCacheForType;
6 let Scheduler;
7 let act;
@@ -22,6 +23,7 @@ describe('ReactCache', () => {
23 Scheduler = require('scheduler');
24 act = require('jest-react').act;
25 Suspense = React.Suspense;
26 + getCacheSignal = React.unstable_getCacheSignal;
27 getCacheForType = React.unstable_getCacheForType;
28 useCacheRefresh = React.unstable_useCacheRefresh;
29 startTransition = React.startTransition;
@@ -52,6 +54,7 @@ describe('ReactCache', () => {
54 const newRecord = {
55 status: 'resolved',
56 value: text,
57 + cleanupScheduled: false,
58 };
59 data.set(text, newRecord);
60 } else if (record.status === 'pending') {
@@ -64,6 +67,7 @@ describe('ReactCache', () => {
67 const newRecord = {
68 status: 'rejected',
69 value: error,
70 + cleanupScheduled: false,
71 };
72 data.set(text, newRecord);
73 } else if (record.status === 'pending') {
@@ -76,9 +80,21 @@ describe('ReactCache', () => {
80 }
81
82 function readText(text) {
83 + const signal = getCacheSignal();
84 const textCache = getCacheForType(createTextCache);
85 const record = textCache.data.get(text);
86 if (record !== undefined) {
87 + if (!record.cleanupScheduled) {
88 + // This record was seeded prior to the abort signal being available:
89 + // schedule a cleanup function for it.
90 + // TODO: Add ability to cleanup entries seeded w useCacheRefresh()
91 + record.cleanupScheduled = true;
92 + signal.addEventListener('abort', () => {
93 + Scheduler.unstable_yieldValue(
94 + `Cache cleanup: ${text} [v${textCache.version}]`,
95 + );
96 + });
97 + }
98 switch (record.status) {
99 case 'pending':
100 throw record.value;
@@ -115,9 +131,15 @@ describe('ReactCache', () => {
131 const newRecord = {
132 status: 'pending',
133 value: thenable,
134 + cleanupScheduled: true,
135 };
136 textCache.data.set(text, newRecord);
137
138 + signal.addEventListener('abort', () => {
139 + Scheduler.unstable_yieldValue(
140 + `Cache cleanup: ${text} [v${textCache.version}]`,
141 + );
142 + });
143 throw thenable;
144 }
145 }
@@ -180,6 +202,13 @@ describe('ReactCache', () => {
202 });
203 expect(Scheduler).toHaveYielded(['A']);
204 expect(root).toMatchRenderedOutput('A');
205 +
206 + await act(async () => {
207 + root.render('Bye');
208 + });
209 + // no cleanup: cache is still retained at the root
210 + expect(Scheduler).toHaveYielded([]);
211 + expect(root).toMatchRenderedOutput('Bye');
212 });
213
214 // @gate experimental || www
@@ -200,12 +229,19 @@ describe('ReactCache', () => {
229 });
230 expect(Scheduler).toHaveYielded(['A']);
231 expect(root).toMatchRenderedOutput('A');
232 +
233 + await act(async () => {
234 + root.render('Bye');
235 + });
236 + // no cleanup: cache is still retained at the root
237 + expect(Scheduler).toHaveYielded([]);
238 + expect(root).toMatchRenderedOutput('Bye');
239 });
240
241 // @gate experimental || www
242 test('multiple new Cache boundaries in the same update share the same, fresh cache', async () => {
207 - function App({text}) {
208 - return (
243 + function App({showMore}) {
244 + return showMore ? (
245 <>
246 <Cache>
247 <Suspense fallback={<Text text="Loading..." />}>
@@ -218,6 +254,8 @@ describe('ReactCache', () => {
254 </Suspense>
255 </Cache>
256 </>
257 + ) : (
258 + '(empty)'
259 );
260 }
261
@@ -225,6 +263,12 @@ describe('ReactCache', () => {
263 await act(async () => {
264 root.render(<App showMore={false} />);
265 });
266 + expect(Scheduler).toHaveYielded([]);
267 + expect(root).toMatchRenderedOutput('(empty)');
268 +
269 + await act(async () => {
270 + root.render(<App showMore={true} />);
271 + });
272 // Even though there are two new <Cache /> trees, they should share the same
273 // data cache. So there should be only a single cache miss for A.
274 expect(Scheduler).toHaveYielded([
@@ -239,6 +283,15 @@ describe('ReactCache', () => {
283 });
284 expect(Scheduler).toHaveYielded(['A', 'A']);
285 expect(root).toMatchRenderedOutput('AA');
286 +
287 + await act(async () => {
288 + root.render('Bye');
289 + });
290 + // cleanup occurs for the cache shared by the inner cache boundaries (which
291 + // are not shared w the root because they were added in an update)
292 + // note that no cache is created for the root since the cache is never accessed
293 + expect(Scheduler).toHaveYielded(['Cache cleanup: A [v1]']);
294 + expect(root).toMatchRenderedOutput('Bye');
295 });
296
297 // @gate experimental || www
@@ -261,8 +314,8 @@ describe('ReactCache', () => {
314 await act(async () => {
315 root.render(<App />);
316 });
264 - // Even though there are two new <Cache /> trees, they should share the same
265 - // data cache. So there should be only a single cache miss for A.
317 + // Even though there is a nested <Cache /> boundary, it should share the same
318 + // data cache as the root. So there should be only a single cache miss for A.
319 expect(Scheduler).toHaveYielded(['Cache miss! [A]', 'Loading...']);
320 expect(root).toMatchRenderedOutput('Loading...');
321
@@ -271,6 +324,13 @@ describe('ReactCache', () => {
324 });
325 expect(Scheduler).toHaveYielded(['A', 'A']);
326 expect(root).toMatchRenderedOutput('AA');
327 +
328 + await act(async () => {
329 + root.render('Bye');
330 + });
331 + // no cleanup: cache is still retained at the root
332 + expect(Scheduler).toHaveYielded([]);
333 + expect(root).toMatchRenderedOutput('Bye');
334 },
335 );
336
@@ -309,6 +369,13 @@ describe('ReactCache', () => {
369 'A [v1]',
370 ]);
371 expect(root).toMatchRenderedOutput('A [v1]A [v1]');
372 +
373 + await act(async () => {
374 + root.render('Bye');
375 + });
376 + // no cleanup: cache is still retained at the root
377 + expect(Scheduler).toHaveYielded([]);
378 + expect(root).toMatchRenderedOutput('Bye');
379 });
380
381 // @gate experimental || www
@@ -356,10 +423,21 @@ describe('ReactCache', () => {
423 });
424 expect(Scheduler).toHaveYielded(['A [v2]']);
425 expect(root).toMatchRenderedOutput('A [v1]A [v2]');
426 +
427 + // Replace all the children: this should retain the root Cache instance,
428 + // but cleanup the separate cache instance created for the fresh cache
429 + // boundary
430 + await act(async () => {
431 + root.render('Bye!');
432 + });
433 + // Cleanup occurs for the *second* cache instance: the first is still
434 + // referenced by the root
435 + expect(Scheduler).toHaveYielded(['Cache cleanup: A [v2]']);
436 + expect(root).toMatchRenderedOutput('Bye!');
437 });
438
439 // @gate experimental || www
362 - test('inner content uses same cache as shell if spawned by the same transition', async () => {
440 + test('inner/outer cache boundaries uses the same cache instance on initial render', async () => {
441 const root = ReactNoop.createRoot();
442
443 function App() {
@@ -431,10 +509,109 @@ describe('ReactCache', () => {
509 <div>Content</div>
510 </>,
511 );
512 +
513 + await act(async () => {
514 + root.render('Bye');
515 + });
516 + // no cleanup: cache is still retained at the root
517 + expect(Scheduler).toHaveYielded([]);
518 + expect(root).toMatchRenderedOutput('Bye');
519 + });
520 +
521 + // @gate experimental || www
522 + test('inner/ outer cache boundaries added in the same update use the same cache instance', async () => {
523 + const root = ReactNoop.createRoot();
524 +
525 + function App({showMore}) {
526 + return showMore ? (
527 + <Cache>
528 + <Suspense fallback={<Text text="Loading shell..." />}>
529 + {/* The shell reads A */}
530 + <Shell>
531 + {/* The inner content reads both A and B */}
532 + <Suspense fallback={<Text text="Loading content..." />}>
533 + <Cache>
534 + <Content />
535 + </Cache>
536 + </Suspense>
537 + </Shell>
538 + </Suspense>
539 + </Cache>
540 + ) : (
541 + '(empty)'
542 + );
543 + }
544 +
545 + function Shell({children}) {
546 + readText('A');
547 + return (
548 + <>
549 + <div>
550 + <Text text="Shell" />
551 + </div>
552 + <div>{children}</div>
553 + </>
554 + );
555 + }
556 +
557 + function Content() {
558 + readText('A');
559 + readText('B');
560 + return <Text text="Content" />;
561 + }
562 +
563 + await act(async () => {
564 + root.render(<App showMore={false} />);
565 + });
566 + expect(Scheduler).toHaveYielded([]);
567 + expect(root).toMatchRenderedOutput('(empty)');
568 +
569 + await act(async () => {
570 + root.render(<App showMore={true} />);
571 + });
572 + expect(Scheduler).toHaveYielded(['Cache miss! [A]', 'Loading shell...']);
573 + expect(root).toMatchRenderedOutput('Loading shell...');
574 +
575 + await act(async () => {
576 + resolveMostRecentTextCache('A');
577 + });
578 + expect(Scheduler).toHaveYielded([
579 + 'Shell',
580 + // There's a cache miss for B, because it hasn't been read yet. But not
581 + // A, because it was cached when we rendered the shell.
582 + 'Cache miss! [B]',
583 + 'Loading content...',
584 + ]);
585 + expect(root).toMatchRenderedOutput(
586 + <>
587 + <div>Shell</div>
588 + <div>Loading content...</div>
589 + </>,
590 + );
591 +
592 + await act(async () => {
593 + resolveMostRecentTextCache('B');
594 + });
595 + expect(Scheduler).toHaveYielded(['Content']);
596 + expect(root).toMatchRenderedOutput(
597 + <>
598 + <div>Shell</div>
599 + <div>Content</div>
600 + </>,
601 + );
602 +
603 + await act(async () => {
604 + root.render('Bye');
605 + });
606 + expect(Scheduler).toHaveYielded([
607 + 'Cache cleanup: A [v1]',
608 + 'Cache cleanup: B [v1]',
609 + ]);
610 + expect(root).toMatchRenderedOutput('Bye');
611 });
612
613 // @gate experimental || www
437 - test('refresh a cache', async () => {
614 + test('refresh a cache boundary', async () => {
615 let refresh;
616 function App() {
617 refresh = useCacheRefresh();
@@ -474,6 +651,14 @@ describe('ReactCache', () => {
651 // Note that the version has updated
652 expect(Scheduler).toHaveYielded(['A [v2]']);
653 expect(root).toMatchRenderedOutput('A [v2]');
654 +
655 + await act(async () => {
656 + root.render('Bye');
657 + });
658 + // the original cache instance does not cleanup since it is still referenced
659 + // by the root, but the refreshed inner cache does cleanup
660 + expect(Scheduler).toHaveYielded(['Cache cleanup: A [v2]']);
661 + expect(root).toMatchRenderedOutput('Bye');
662 });
663
664 // @gate experimental || www
@@ -512,9 +697,64 @@ describe('ReactCache', () => {
697 await act(async () => {
698 resolveMostRecentTextCache('A');
699 });
515 - // Note that the version has updated
516 - expect(Scheduler).toHaveYielded(['A [v2]']);
700 + // Note that the version has updated, and the previous cache is cleared
701 + expect(Scheduler).toHaveYielded(['A [v2]', 'Cache cleanup: A [v1]']);
702 expect(root).toMatchRenderedOutput('A [v2]');
703 +
704 + await act(async () => {
705 + root.render('Bye');
706 + });
707 + // the original root cache already cleaned up when the refresh completed
708 + expect(Scheduler).toHaveYielded([]);
709 + expect(root).toMatchRenderedOutput('Bye');
710 + });
711 +
712 + // @gate experimental || www
713 + test('refresh the root cache without a transition', async () => {
714 + let refresh;
715 + function App() {
716 + refresh = useCacheRefresh();
717 + return <AsyncText showVersion={true} text="A" />;
718 + }
719 +
720 + // Mount initial data
721 + const root = ReactNoop.createRoot();
722 + await act(async () => {
723 + root.render(
724 + <Suspense fallback={<Text text="Loading..." />}>
725 + <App />
726 + </Suspense>,
727 + );
728 + });
729 + expect(Scheduler).toHaveYielded(['Cache miss! [A]', 'Loading...']);
730 + expect(root).toMatchRenderedOutput('Loading...');
731 +
732 + await act(async () => {
733 + resolveMostRecentTextCache('A');
734 + });
735 + expect(Scheduler).toHaveYielded(['A [v1]']);
736 + expect(root).toMatchRenderedOutput('A [v1]');
737 +
738 + // Refresh for new data.
739 + await act(async () => {
740 + refresh();
741 + });
742 + expect(Scheduler).toHaveYielded(['Cache miss! [A]', 'Loading...']);
743 + expect(root).toMatchRenderedOutput('Loading...');
744 +
745 + await act(async () => {
746 + resolveMostRecentTextCache('A');
747 + });
748 + // Note that the version has updated, and the previous cache is cleared
749 + expect(Scheduler).toHaveYielded(['A [v2]', 'Cache cleanup: A [v1]']);
750 + expect(root).toMatchRenderedOutput('A [v2]');
751 +
752 + await act(async () => {
753 + root.render('Bye');
754 + });
755 + // the original root cache already cleaned up when the refresh completed
756 + expect(Scheduler).toHaveYielded([]);
757 + expect(root).toMatchRenderedOutput('Bye');
758 });
759
760 // @gate experimental || www
@@ -556,8 +796,16 @@ describe('ReactCache', () => {
796 startTransition(() => refresh(createTextCache, cache));
797 });
798 // The root should re-render without a cache miss.
799 + // The cache is not cleared up yet, since it's still reference by the root
800 expect(Scheduler).toHaveYielded(['A [v2]']);
801 expect(root).toMatchRenderedOutput('A [v2]');
802 +
803 + await act(async () => {
804 + root.render('Bye');
805 + });
806 + // the refreshed cache boundary is unmounted and cleans up
807 + expect(Scheduler).toHaveYielded(['Cache cleanup: A [v2]']);
808 + expect(root).toMatchRenderedOutput('Bye');
809 });
810
811 // @gate experimental || www
@@ -621,8 +869,22 @@ describe('ReactCache', () => {
869 await act(async () => {
870 resolveMostRecentTextCache('A');
871 });
624 - expect(Scheduler).toHaveYielded(['A [v3]', 'A [v3]']);
872 + expect(Scheduler).toHaveYielded([
873 + 'A [v3]',
874 + 'A [v3]',
875 + // once the refresh completes the inner showMore boundary frees its previous
876 + // cache instance, since it is now using the refreshed parent instance.
877 + 'Cache cleanup: A [v2]',
878 + ]);
879 expect(root).toMatchRenderedOutput('A [v3]A [v3]');
880 +
881 + await act(async () => {
882 + root.render('Bye!');
883 + });
884 + // Unmounting children releases the refreshed cache instance only; the root
885 + // still retains the original cache instance used for the first render
886 + expect(Scheduler).toHaveYielded(['Cache cleanup: A [v3]']);
887 + expect(root).toMatchRenderedOutput('Bye!');
888 });
889
890 // @gate experimental || www
@@ -695,6 +957,21 @@ describe('ReactCache', () => {
957 });
958 expect(Scheduler).toHaveYielded(['A [v2]']);
959 expect(root).toMatchRenderedOutput('A [v2]A [v1]');
960 +
961 + // Unmount children: this should clear *both* cache instances:
962 + // the root doesn't have a cache instance (since it wasn't accessed
963 + // during the initial render, and all subsequent cache accesses were within
964 + // a fresh boundary). Therefore this causes cleanup for both the fresh cache
965 + // instance in the refreshed first boundary and cleanup for the non-refreshed
966 + // sibling boundary.
967 + await act(async () => {
968 + root.render('Bye!');
969 + });
970 + expect(Scheduler).toHaveYielded([
971 + 'Cache cleanup: A [v2]',
972 + 'Cache cleanup: A [v1]',
973 + ]);
974 + expect(root).toMatchRenderedOutput('Bye!');
975 },
976 );
977
@@ -733,6 +1010,7 @@ describe('ReactCache', () => {
1010 'Cache miss! [B]',
1011 'Loading...',
1012 ]);
1013 + expect(root).toMatchRenderedOutput('Loading...');
1014
1015 await act(async () => {
1016 // This will resolve the content in the first cache
@@ -750,6 +1028,7 @@ describe('ReactCache', () => {
1028 'A [v1]',
1029 'B [v1]',
1030 ]);
1031 + expect(root).toMatchRenderedOutput('Loading... A [v1] B [v1]');
1032
1033 // Now resolve the second tree
1034 await act(async () => {
@@ -757,6 +1036,15 @@ describe('ReactCache', () => {
1036 });
1037 expect(Scheduler).toHaveYielded(['A [v2]']);
1038 expect(root).toMatchRenderedOutput('A [v2] A [v1] B [v1]');
1039 +
1040 + await act(async () => {
1041 + root.render('Bye!');
1042 + });
1043 + // Unmounting children releases both cache boundaries, but the original
1044 + // cache instance (used by second boundary) is still referenced by the root.
1045 + // only the second cache instance is freed.
1046 + expect(Scheduler).toHaveYielded(['Cache cleanup: A [v2]']);
1047 + expect(root).toMatchRenderedOutput('Bye!');
1048 },
1049 );
1050
@@ -841,6 +1129,19 @@ describe('ReactCache', () => {
1129 });
1130 expect(Scheduler).toHaveYielded(['A [v1]', 'A [v1]', 'A [v2]']);
1131 expect(root).toMatchRenderedOutput('A [v1]A [v1]A [v2]');
1132 +
1133 + // Unmount children: the first text cache instance is created only after the root
1134 + // commits, so both fresh cache instances are released by their cache boundaries,
1135 + // cleaning up v1 (used for the first two children which render togeether) and
1136 + // v2 (used for the third boundary added later).
1137 + await act(async () => {
1138 + root.render('Bye!');
1139 + });
1140 + expect(Scheduler).toHaveYielded([
1141 + 'Cache cleanup: A [v1]',
1142 + 'Cache cleanup: A [v2]',
1143 + ]);
1144 + expect(root).toMatchRenderedOutput('Bye!');
1145 });
1146
1147 // @gate experimental || www
@@ -863,7 +1164,7 @@ describe('ReactCache', () => {
1164 <Suspense fallback={<Text text="Loading..." />}>
1165 {shouldShow ? (
1166 <Cache>
866 - <AsyncText text="A" />
1167 + <AsyncText showVersion={true} text="A" />
1168 </Cache>
1169 ) : null}
1170 </Suspense>
@@ -880,7 +1181,7 @@ describe('ReactCache', () => {
1181
1182 const root = ReactNoop.createRoot();
1183 await act(async () => {
883 - root.render(<App showMore={false} />);
1184 + root.render(<App />);
1185 });
1186 expect(Scheduler).toHaveYielded(['0']);
1187 expect(root).toMatchRenderedOutput('0');
@@ -908,7 +1209,331 @@ describe('ReactCache', () => {
1209 await act(async () => {
1210 resolveMostRecentTextCache('A');
1211 });
911 - expect(Scheduler).toHaveYielded(['A']);
912 - expect(root).toMatchRenderedOutput('A1');
1212 + expect(Scheduler).toHaveYielded(['A [v1]']);
1213 + expect(root).toMatchRenderedOutput('A [v1]1');
1214 +
1215 + // Unmount children: the first text cache instance is created only after initial
1216 + // render after calling showMore(). This instance is cleaned up when that boundary
1217 + // is unmounted. Bc root cache instance is never accessed, the inner cache
1218 + // boundary ends up at v1.
1219 + await act(async () => {
1220 + root.render('Bye!');
1221 + });
1222 + expect(Scheduler).toHaveYielded(['Cache cleanup: A [v1]']);
1223 + expect(root).toMatchRenderedOutput('Bye!');
1224 + });
1225 +
1226 + // @gate experimental || www
1227 + test('cache boundary uses a fresh cache when its key changes', async () => {
1228 + const root = ReactNoop.createRoot();
1229 + seedNextTextCache('A');
1230 + await act(async () => {
1231 + root.render(
1232 + <Suspense fallback="Loading...">
1233 + <Cache key="A">
1234 + <AsyncText showVersion={true} text="A" />
1235 + </Cache>
1236 + </Suspense>,
1237 + );
1238 + });
1239 + expect(Scheduler).toHaveYielded(['A [v1]']);
1240 + expect(root).toMatchRenderedOutput('A [v1]');
1241 +
1242 + seedNextTextCache('B');
1243 + await act(async () => {
1244 + root.render(
1245 + <Suspense fallback="Loading...">
1246 + <Cache key="B">
1247 + <AsyncText showVersion={true} text="B" />
1248 + </Cache>
1249 + </Suspense>,
1250 + );
1251 + });
1252 + expect(Scheduler).toHaveYielded(['B [v2]']);
1253 + expect(root).toMatchRenderedOutput('B [v2]');
1254 +
1255 + // Unmount children: the fresh cache instance for B cleans up since the cache boundary
1256 + // is the only owner, while the original cache instance (for A) is still retained by
1257 + // the root.
1258 + await act(async () => {
1259 + root.render('Bye!');
1260 + });
1261 + expect(Scheduler).toHaveYielded(['Cache cleanup: B [v2]']);
1262 + expect(root).toMatchRenderedOutput('Bye!');
1263 + });
1264 +
1265 + // @gate experimental || www
1266 + test('overlapping transitions after an initial mount use the same fresh cache', async () => {
1267 + const root = ReactNoop.createRoot();
1268 + await act(async () => {
1269 + root.render(
1270 + <Suspense fallback="Loading...">
1271 + <Cache key="A">
1272 + <AsyncText showVersion={true} text="A" />
1273 + </Cache>
1274 + </Suspense>,
1275 + );
1276 + });
1277 + expect(Scheduler).toHaveYielded(['Cache miss! [A]']);
1278 + expect(root).toMatchRenderedOutput('Loading...');
1279 +
1280 + await act(async () => {
1281 + resolveMostRecentTextCache('A');
1282 + });
1283 + expect(Scheduler).toHaveYielded(['A [v1]']);
1284 + expect(root).toMatchRenderedOutput('A [v1]');
1285 +
1286 + // After a mount, subsequent transitions use a fresh cache
1287 + await act(async () => {
1288 + startTransition(() => {
1289 + root.render(
1290 + <Suspense fallback="Loading...">
1291 + <Cache key="B">
1292 + <AsyncText showVersion={true} text="B" />
1293 + </Cache>
1294 + </Suspense>,
1295 + );
1296 + });
1297 + });
1298 + expect(Scheduler).toHaveYielded(['Cache miss! [B]']);
1299 + expect(root).toMatchRenderedOutput('A [v1]');
1300 +
1301 + // Update to a different text and with a different key for the cache
1302 + // boundary: this should still use the fresh cache instance created
1303 + // for the earlier transition
1304 + await act(async () => {
1305 + startTransition(() => {
1306 + root.render(
1307 + <Suspense fallback="Loading...">
1308 + <Cache key="C">
1309 + <AsyncText showVersion={true} text="C" />
1310 + </Cache>
1311 + </Suspense>,
1312 + );
1313 + });
1314 + });
1315 + expect(Scheduler).toHaveYielded(['Cache miss! [C]']);
1316 + expect(root).toMatchRenderedOutput('A [v1]');
1317 +
1318 + await act(async () => {
1319 + resolveMostRecentTextCache('C');
1320 + });
1321 + expect(Scheduler).toHaveYielded(['C [v2]']);
1322 + expect(root).toMatchRenderedOutput('C [v2]');
1323 +
1324 + // Unmount children: the fresh cache used for the updates is freed, while the
1325 + // original cache (with A) is still retained at the root.
1326 + await act(async () => {
1327 + root.render('Bye!');
1328 + });
1329 + expect(Scheduler).toHaveYielded([
1330 + 'Cache cleanup: B [v2]',
1331 + 'Cache cleanup: C [v2]',
1332 + ]);
1333 + expect(root).toMatchRenderedOutput('Bye!');
1334 + });
1335 +
1336 + // @gate experimental || www
1337 + test('overlapping updates after an initial mount use the same fresh cache', async () => {
1338 + const root = ReactNoop.createRoot();
1339 + await act(async () => {
1340 + root.render(
1341 + <Suspense fallback="Loading...">
1342 + <Cache key="A">
1343 + <AsyncText showVersion={true} text="A" />
1344 + </Cache>
1345 + </Suspense>,
1346 + );
1347 + });
1348 + expect(Scheduler).toHaveYielded(['Cache miss! [A]']);
1349 + expect(root).toMatchRenderedOutput('Loading...');
1350 +
1351 + await act(async () => {
1352 + resolveMostRecentTextCache('A');
1353 + });
1354 + expect(Scheduler).toHaveYielded(['A [v1]']);
1355 + expect(root).toMatchRenderedOutput('A [v1]');
1356 +
1357 + // After a mount, subsequent updates use a fresh cache
1358 + await act(async () => {
1359 + root.render(
1360 + <Suspense fallback="Loading...">
1361 + <Cache key="B">
1362 + <AsyncText showVersion={true} text="B" />
1363 + </Cache>
1364 + </Suspense>,
1365 + );
1366 + });
1367 + expect(Scheduler).toHaveYielded(['Cache miss! [B]']);
1368 + expect(root).toMatchRenderedOutput('Loading...');
1369 +
1370 + // A second update uses the same fresh cache: even though this is a new
1371 + // Cache boundary, the render uses the fresh cache from the pending update.
1372 + await act(async () => {
1373 + root.render(
1374 + <Suspense fallback="Loading...">
1375 + <Cache key="C">
1376 + <AsyncText showVersion={true} text="C" />
1377 + </Cache>
1378 + </Suspense>,
1379 + );
1380 + });
1381 + expect(Scheduler).toHaveYielded(['Cache miss! [C]']);
1382 + expect(root).toMatchRenderedOutput('Loading...');
1383 +
1384 + await act(async () => {
1385 + resolveMostRecentTextCache('C');
1386 + });
1387 + expect(Scheduler).toHaveYielded(['C [v2]']);
1388 + expect(root).toMatchRenderedOutput('C [v2]');
1389 +
1390 + // Unmount children: the fresh cache used for the updates is freed, while the
1391 + // original cache (with A) is still retained at the root.
1392 + await act(async () => {
1393 + root.render('Bye!');
1394 + });
1395 + expect(Scheduler).toHaveYielded([
1396 + 'Cache cleanup: B [v2]',
1397 + 'Cache cleanup: C [v2]',
1398 + ]);
1399 + expect(root).toMatchRenderedOutput('Bye!');
1400 + });
1401 +
1402 + // @gate experimental || www
1403 + test('cleans up cache only used in an aborted transition', async () => {
1404 + const root = ReactNoop.createRoot();
1405 + seedNextTextCache('A');
1406 + await act(async () => {
1407 + root.render(
1408 + <Suspense fallback="Loading...">
1409 + <Cache key="A">
1410 + <AsyncText showVersion={true} text="A" />
1411 + </Cache>
1412 + </Suspense>,
1413 + );
1414 + });
1415 + expect(Scheduler).toHaveYielded(['A [v1]']);
1416 + expect(root).toMatchRenderedOutput('A [v1]');
1417 +
1418 + // Start a transition from A -> B..., which should create a fresh cache
1419 + // for the new cache boundary (bc of the different key)
1420 + await act(async () => {
1421 + startTransition(() => {
1422 + root.render(
1423 + <Suspense fallback="Loading...">
1424 + <Cache key="B">
1425 + <AsyncText showVersion={true} text="B" />
1426 + </Cache>
1427 + </Suspense>,
1428 + );
1429 + });
1430 + });
1431 + expect(Scheduler).toHaveYielded(['Cache miss! [B]']);
1432 + expect(root).toMatchRenderedOutput('A [v1]');
1433 +
1434 + // ...but cancel by transitioning "back" to A (which we never really left)
1435 + await act(async () => {
1436 + startTransition(() => {
1437 + root.render(
1438 + <Suspense fallback="Loading...">
1439 + <Cache key="A">
1440 + <AsyncText showVersion={true} text="A" />
1441 + </Cache>
1442 + </Suspense>,
1443 + );
1444 + });
1445 + });
1446 + expect(Scheduler).toHaveYielded(['A [v1]', 'Cache cleanup: B [v2]']);
1447 + expect(root).toMatchRenderedOutput('A [v1]');
1448 +
1449 + // Unmount children: ...
1450 + await act(async () => {
1451 + root.render('Bye!');
1452 + });
1453 + expect(Scheduler).toHaveYielded([]);
1454 + expect(root).toMatchRenderedOutput('Bye!');
1455 + });
1456 +
1457 + // @gate experimental || www
1458 + test.skip('if a root cache refresh never commits its fresh cache is released', async () => {
1459 + const root = ReactNoop.createRoot();
1460 + let refresh;
1461 + function Example({text}) {
1462 + refresh = useCacheRefresh();
1463 + return <AsyncText showVersion={true} text={text} />;
1464 + }
1465 + seedNextTextCache('A');
1466 + await act(async () => {
1467 + root.render(
1468 + <Suspense fallback="Loading...">
1469 + <Example text="A" />
1470 + </Suspense>,
1471 + );
1472 + });
1473 + expect(Scheduler).toHaveYielded(['A [v1]']);
1474 + expect(root).toMatchRenderedOutput('A [v1]');
1475 +
1476 + await act(async () => {
1477 + startTransition(() => {
1478 + refresh();
1479 + });
1480 + });
1481 + expect(Scheduler).toHaveYielded(['Cache miss! [A]']);
1482 + expect(root).toMatchRenderedOutput('A [v1]');
1483 +
1484 + await act(async () => {
1485 + root.render('Bye!');
1486 + });
1487 + expect(Scheduler).toHaveYielded([
1488 + // TODO: the v1 cache should *not* be cleaned up, it is still retained by the root
1489 + // The following line is presently yielded but should not be:
1490 + // 'Cache cleanup: A [v1]',
1491 +
1492 + // TODO: the v2 cache *should* be cleaned up, it was created for the abandoned refresh
1493 + // The following line is presently not yielded but should be:
1494 + 'Cache cleanup: A [v2]',
1495 + ]);
1496 + expect(root).toMatchRenderedOutput('Bye!');
1497 + });
1498 +
1499 + // @gate experimental || www
1500 + test.skip('if a cache boundary refresh never commits its fresh cache is released', async () => {
1501 + const root = ReactNoop.createRoot();
1502 + let refresh;
1503 + function Example({text}) {
1504 + refresh = useCacheRefresh();
1505 + return <AsyncText showVersion={true} text={text} />;
1506 + }
1507 + seedNextTextCache('A');
1508 + await act(async () => {
1509 + root.render(
1510 + <Suspense fallback="Loading...">
1511 + <Cache>
1512 + <Example text="A" />
1513 + </Cache>
1514 + </Suspense>,
1515 + );
1516 + });
1517 + expect(Scheduler).toHaveYielded(['A [v1]']);
1518 + expect(root).toMatchRenderedOutput('A [v1]');
1519 +
1520 + await act(async () => {
1521 + startTransition(() => {
1522 + refresh();
1523 + });
1524 + });
1525 + expect(Scheduler).toHaveYielded(['Cache miss! [A]']);
1526 + expect(root).toMatchRenderedOutput('A [v1]');
1527 +
1528 + // Unmount the boundary before the refresh can complete
1529 + await act(async () => {
1530 + root.render('Bye!');
1531 + });
1532 + expect(Scheduler).toHaveYielded([
1533 + // TODO: the v2 cache *should* be cleaned up, it was created for the abandoned refresh
1534 + // The following line is presently not yielded but should be:
1535 + 'Cache cleanup: A [v2]',
1536 + ]);
1537 + expect(root).toMatchRenderedOutput('Bye!');
1538 });
1539 });
packages/react-reconciler/src/__tests__/SchedulingProfiler-test.internal.js
+93 -93
@@ -409,30 +409,30 @@ describe('SchedulingProfiler', () => {
409
410 if (gate(flags => flags.enableSchedulingProfiler)) {
411 expect(getMarks()).toMatchInlineSnapshot(`
412 - Array [
413 - "--render-start-16",
414 - "--component-render-start-Example",
415 - "--component-render-stop",
416 - "--render-stop",
417 - "--commit-start-16",
418 - "--react-version-17.0.3",
419 - "--profiler-version-1",
420 - "--react-lane-labels-Sync,InputContinuousHydration,InputContinuous,DefaultHydration,Default,TransitionHydration,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Retry,Retry,Retry,Retry,Retry,SelectiveHydration,IdleHydration,Idle,Offscreen",
421 - "--layout-effects-start-16",
422 - "--schedule-state-update-1-Example",
423 - "--layout-effects-stop",
424 - "--render-start-1",
425 - "--component-render-start-Example",
426 - "--component-render-stop",
427 - "--render-stop",
428 - "--commit-start-1",
429 - "--react-version-17.0.3",
430 - "--profiler-version-1",
431 - "--react-lane-labels-Sync,InputContinuousHydration,InputContinuous,DefaultHydration,Default,TransitionHydration,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Retry,Retry,Retry,Retry,Retry,SelectiveHydration,IdleHydration,Idle,Offscreen",
432 - "--commit-stop",
433 - "--commit-stop",
434 - ]
435 - `);
412 + Array [
413 + "--render-start-16",
414 + "--component-render-start-Example",
415 + "--component-render-stop",
416 + "--render-stop",
417 + "--commit-start-16",
418 + "--react-version-17.0.3",
419 + "--profiler-version-1",
420 + "--react-lane-labels-Sync,InputContinuousHydration,InputContinuous,DefaultHydration,Default,TransitionHydration,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Retry,Retry,Retry,Retry,Retry,SelectiveHydration,IdleHydration,Idle,Offscreen",
421 + "--layout-effects-start-16",
422 + "--schedule-state-update-1-Example",
423 + "--layout-effects-stop",
424 + "--render-start-1",
425 + "--component-render-start-Example",
426 + "--component-render-stop",
427 + "--render-stop",
428 + "--commit-start-1",
429 + "--react-version-17.0.3",
430 + "--profiler-version-1",
431 + "--react-lane-labels-Sync,InputContinuousHydration,InputContinuous,DefaultHydration,Default,TransitionHydration,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Retry,Retry,Retry,Retry,Retry,SelectiveHydration,IdleHydration,Idle,Offscreen",
432 + "--commit-stop",
433 + "--commit-stop",
434 + ]
435 + `);
436 }
437 });
438
@@ -462,30 +462,30 @@ describe('SchedulingProfiler', () => {
462
463 if (gate(flags => flags.enableSchedulingProfiler)) {
464 expect(getMarks()).toMatchInlineSnapshot(`
465 - Array [
466 - "--render-start-16",
467 - "--component-render-start-Example",
468 - "--component-render-stop",
469 - "--render-stop",
470 - "--commit-start-16",
471 - "--react-version-17.0.3",
472 - "--profiler-version-1",
473 - "--react-lane-labels-Sync,InputContinuousHydration,InputContinuous,DefaultHydration,Default,TransitionHydration,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Retry,Retry,Retry,Retry,Retry,SelectiveHydration,IdleHydration,Idle,Offscreen",
474 - "--layout-effects-start-16",
475 - "--schedule-forced-update-1-Example",
476 - "--layout-effects-stop",
477 - "--render-start-1",
478 - "--component-render-start-Example",
479 - "--component-render-stop",
480 - "--render-stop",
481 - "--commit-start-1",
482 - "--react-version-17.0.3",
483 - "--profiler-version-1",
484 - "--react-lane-labels-Sync,InputContinuousHydration,InputContinuous,DefaultHydration,Default,TransitionHydration,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Retry,Retry,Retry,Retry,Retry,SelectiveHydration,IdleHydration,Idle,Offscreen",
485 - "--commit-stop",
486 - "--commit-stop",
487 - ]
488 - `);
465 + Array [
466 + "--render-start-16",
467 + "--component-render-start-Example",
468 + "--component-render-stop",
469 + "--render-stop",
470 + "--commit-start-16",
471 + "--react-version-17.0.3",
472 + "--profiler-version-1",
473 + "--react-lane-labels-Sync,InputContinuousHydration,InputContinuous,DefaultHydration,Default,TransitionHydration,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Retry,Retry,Retry,Retry,Retry,SelectiveHydration,IdleHydration,Idle,Offscreen",
474 + "--layout-effects-start-16",
475 + "--schedule-forced-update-1-Example",
476 + "--layout-effects-stop",
477 + "--render-start-1",
478 + "--component-render-start-Example",
479 + "--component-render-stop",
480 + "--render-stop",
481 + "--commit-start-1",
482 + "--react-version-17.0.3",
483 + "--profiler-version-1",
484 + "--react-lane-labels-Sync,InputContinuousHydration,InputContinuous,DefaultHydration,Default,TransitionHydration,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Retry,Retry,Retry,Retry,Retry,SelectiveHydration,IdleHydration,Idle,Offscreen",
485 + "--commit-stop",
486 + "--commit-stop",
487 + ]
488 + `);
489 }
490 });
491
@@ -701,22 +701,22 @@ describe('SchedulingProfiler', () => {
701
702 if (gate(flags => flags.enableSchedulingProfiler)) {
703 expect(getMarks()).toMatchInlineSnapshot(`
704 - Array [
705 - "--schedule-render-16",
706 - "--render-start-16",
707 - "--component-render-start-Example",
708 - "--schedule-state-update-16-Example",
709 - "--component-render-stop",
710 - "--render-stop",
711 - "--commit-start-16",
712 - "--react-version-17.0.3",
713 - "--profiler-version-1",
714 - "--react-lane-labels-Sync,InputContinuousHydration,InputContinuous,DefaultHydration,Default,TransitionHydration,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Retry,Retry,Retry,Retry,Retry,SelectiveHydration,IdleHydration,Idle,Offscreen",
715 - "--layout-effects-start-16",
716 - "--layout-effects-stop",
717 - "--commit-stop",
718 - ]
719 - `);
704 + Array [
705 + "--schedule-render-16",
706 + "--render-start-16",
707 + "--component-render-start-Example",
708 + "--schedule-state-update-16-Example",
709 + "--component-render-stop",
710 + "--render-stop",
711 + "--commit-start-16",
712 + "--react-version-17.0.3",
713 + "--profiler-version-1",
714 + "--react-lane-labels-Sync,InputContinuousHydration,InputContinuous,DefaultHydration,Default,TransitionHydration,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Retry,Retry,Retry,Retry,Retry,SelectiveHydration,IdleHydration,Idle,Offscreen",
715 + "--layout-effects-start-16",
716 + "--layout-effects-stop",
717 + "--commit-stop",
718 + ]
719 + `);
720 }
721 });
722
@@ -746,35 +746,35 @@ describe('SchedulingProfiler', () => {
746
747 if (gate(flags => flags.enableSchedulingProfiler)) {
748 expect(getMarks()).toMatchInlineSnapshot(`
749 - Array [
750 - "--schedule-render-1",
751 - "--render-start-1",
752 - "--component-render-start-ErrorBoundary",
753 - "--component-render-stop",
754 - "--component-render-start-ExampleThatThrows",
755 - "--component-render-start-ExampleThatThrows",
756 - "--component-render-stop",
757 - "--error-ExampleThatThrows-mount-Expected error",
758 - "--render-stop",
759 - "--commit-start-1",
760 - "--react-version-17.0.3",
761 - "--profiler-version-1",
762 - "--react-lane-labels-Sync,InputContinuousHydration,InputContinuous,DefaultHydration,Default,TransitionHydration,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Retry,Retry,Retry,Retry,Retry,SelectiveHydration,IdleHydration,Idle,Offscreen",
763 - "--layout-effects-start-1",
764 - "--schedule-state-update-1-ErrorBoundary",
765 - "--layout-effects-stop",
766 - "--commit-stop",
767 - "--render-start-1",
768 - "--component-render-start-ErrorBoundary",
769 - "--component-render-stop",
770 - "--render-stop",
771 - "--commit-start-1",
772 - "--react-version-17.0.3",
773 - "--profiler-version-1",
774 - "--react-lane-labels-Sync,InputContinuousHydration,InputContinuous,DefaultHydration,Default,TransitionHydration,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Retry,Retry,Retry,Retry,Retry,SelectiveHydration,IdleHydration,Idle,Offscreen",
775 - "--commit-stop",
776 - ]
777 - `);
749 + Array [
750 + "--schedule-render-1",
751 + "--render-start-1",
752 + "--component-render-start-ErrorBoundary",
753 + "--component-render-stop",
754 + "--component-render-start-ExampleThatThrows",
755 + "--component-render-start-ExampleThatThrows",
756 + "--component-render-stop",
757 + "--error-ExampleThatThrows-mount-Expected error",
758 + "--render-stop",
759 + "--commit-start-1",
760 + "--react-version-17.0.3",
761 + "--profiler-version-1",
762 + "--react-lane-labels-Sync,InputContinuousHydration,InputContinuous,DefaultHydration,Default,TransitionHydration,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Retry,Retry,Retry,Retry,Retry,SelectiveHydration,IdleHydration,Idle,Offscreen",
763 + "--layout-effects-start-1",
764 + "--schedule-state-update-1-ErrorBoundary",
765 + "--layout-effects-stop",
766 + "--commit-stop",
767 + "--render-start-1",
768 + "--component-render-start-ErrorBoundary",
769 + "--component-render-stop",
770 + "--render-stop",
771 + "--commit-start-1",
772 + "--react-version-17.0.3",
773 + "--profiler-version-1",
774 + "--react-lane-labels-Sync,InputContinuousHydration,InputContinuous,DefaultHydration,Default,TransitionHydration,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Retry,Retry,Retry,Retry,Retry,SelectiveHydration,IdleHydration,Idle,Offscreen",
775 + "--commit-stop",
776 + ]
777 + `);
778 }
779 });
780
packages/react-reconciler/src/__tests__/SchedulingProfilerLabels-test.internal.js
+49 -49
@@ -89,20 +89,20 @@ describe('SchedulingProfiler labels', () => {
89
90 if (gate(flags => flags.enableSchedulingProfiler)) {
91 expect(clearedMarks).toMatchInlineSnapshot(`
92 - Array [
93 - "__v3",
94 - "--schedule-render-1",
95 - "--render-start-1",
96 - "--render-stop",
97 - "--commit-start-1",
98 - "--react-version-17.0.3",
99 - "--profiler-version-1",
100 - "--react-lane-labels-Sync,InputContinuousHydration,InputContinuous,DefaultHydration,Default,TransitionHydration,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Retry,Retry,Retry,Retry,Retry,SelectiveHydration,IdleHydration,Idle,Offscreen",
101 - "--layout-effects-start-1",
102 - "--layout-effects-stop",
103 - "--commit-stop",
104 - ]
105 - `);
92 + Array [
93 + "__v3",
94 + "--schedule-render-1",
95 + "--render-start-1",
96 + "--render-stop",
97 + "--commit-start-1",
98 + "--react-version-17.0.3",
99 + "--profiler-version-1",
100 + "--react-lane-labels-Sync,InputContinuousHydration,InputContinuous,DefaultHydration,Default,TransitionHydration,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Retry,Retry,Retry,Retry,Retry,SelectiveHydration,IdleHydration,Idle,Offscreen",
101 + "--layout-effects-start-1",
102 + "--layout-effects-stop",
103 + "--commit-stop",
104 + ]
105 + `);
106 }
107 });
108
@@ -114,11 +114,11 @@ describe('SchedulingProfiler labels', () => {
114
115 root.render(<div />);
116 expect(clearedMarks).toMatchInlineSnapshot(`
117 - Array [
118 - "__v3",
119 - "--schedule-render-16",
120 - ]
121 - `);
117 + Array [
118 + "__v3",
119 + "--schedule-render-16",
120 + ]
121 + `);
122 });
123 }
124 });
@@ -152,21 +152,21 @@ describe('SchedulingProfiler labels', () => {
152 });
153
154 expect(clearedMarks).toMatchInlineSnapshot(`
155 - Array [
156 - "--schedule-state-update-1-App",
157 - "--render-start-1",
158 - "--component-render-start-App",
159 - "--component-render-stop",
160 - "--render-stop",
161 - "--commit-start-1",
162 - "--react-version-17.0.3",
163 - "--profiler-version-1",
164 - "--react-lane-labels-Sync,InputContinuousHydration,InputContinuous,DefaultHydration,Default,TransitionHydration,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Retry,Retry,Retry,Retry,Retry,SelectiveHydration,IdleHydration,Idle,Offscreen",
165 - "--layout-effects-start-1",
166 - "--layout-effects-stop",
167 - "--commit-stop",
168 - ]
169 - `);
155 + Array [
156 + "--schedule-state-update-1-App",
157 + "--render-start-1",
158 + "--component-render-start-App",
159 + "--component-render-stop",
160 + "--render-stop",
161 + "--commit-start-1",
162 + "--react-version-17.0.3",
163 + "--profiler-version-1",
164 + "--react-lane-labels-Sync,InputContinuousHydration,InputContinuous,DefaultHydration,Default,TransitionHydration,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Retry,Retry,Retry,Retry,Retry,SelectiveHydration,IdleHydration,Idle,Offscreen",
165 + "--layout-effects-start-1",
166 + "--layout-effects-stop",
167 + "--commit-stop",
168 + ]
169 + `);
170 }
171 });
172
@@ -196,21 +196,21 @@ describe('SchedulingProfiler labels', () => {
196 });
197
198 expect(clearedMarks).toMatchInlineSnapshot(`
199 - Array [
200 - "--schedule-state-update-4-App",
201 - "--render-start-4",
202 - "--component-render-start-App",
203 - "--component-render-stop",
204 - "--render-stop",
205 - "--commit-start-4",
206 - "--react-version-17.0.3",
207 - "--profiler-version-1",
208 - "--react-lane-labels-Sync,InputContinuousHydration,InputContinuous,DefaultHydration,Default,TransitionHydration,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Retry,Retry,Retry,Retry,Retry,SelectiveHydration,IdleHydration,Idle,Offscreen",
209 - "--layout-effects-start-4",
210 - "--layout-effects-stop",
211 - "--commit-stop",
212 - ]
213 - `);
199 + Array [
200 + "--schedule-state-update-4-App",
201 + "--render-start-4",
202 + "--component-render-start-App",
203 + "--component-render-stop",
204 + "--render-stop",
205 + "--commit-start-4",
206 + "--react-version-17.0.3",
207 + "--profiler-version-1",
208 + "--react-lane-labels-Sync,InputContinuousHydration,InputContinuous,DefaultHydration,Default,TransitionHydration,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Retry,Retry,Retry,Retry,Retry,SelectiveHydration,IdleHydration,Idle,Offscreen",
209 + "--layout-effects-start-4",
210 + "--layout-effects-stop",
211 + "--commit-stop",
212 + ]
213 + `);
214 }
215 });
216 });
packages/react-reconciler/src/__tests__/StrictEffectsModeDefaults-test.internal.js
+1
@@ -113,6 +113,7 @@ describe('StrictEffectsMode defaults', () => {
113 </>,
114 );
115
116 + expect(Scheduler).toHaveYielded([]);
117 expect(Scheduler).toFlushUntilNextPaint([
118 // Cleanup and re-run "one" (and "two") since there is no dependencies array.
119 'useLayoutEffect unmount "one"',
packages/react/index.classic.fb.js
+1
@@ -37,6 +37,7 @@ export {
37 unstable_LegacyHidden,
38 unstable_Offscreen,
39 unstable_Scope,
40 + unstable_getCacheSignal,
41 unstable_getCacheForType,
42 unstable_useCacheRefresh,
43 unstable_useOpaqueIdentifier,
packages/react/index.experimental.js
+1
@@ -33,6 +33,7 @@ export {
33 unstable_DebugTracingMode,
34 unstable_LegacyHidden,
35 unstable_Offscreen,
36 + unstable_getCacheSignal,
37 unstable_getCacheForType,
38 unstable_useCacheRefresh,
39 unstable_useOpaqueIdentifier,
packages/react/index.js
+1
@@ -58,6 +58,7 @@ export {
58 unstable_LegacyHidden,
59 unstable_Offscreen,
60 unstable_Scope,
61 + unstable_getCacheSignal,
62 unstable_getCacheForType,
63 unstable_useCacheRefresh,
64 unstable_useOpaqueIdentifier,
packages/react/index.modern.fb.js
+1
@@ -36,6 +36,7 @@ export {
36 unstable_LegacyHidden,
37 unstable_Offscreen,
38 unstable_Scope,
39 + unstable_getCacheSignal,
40 unstable_getCacheForType,
41 unstable_useCacheRefresh,
42 unstable_useOpaqueIdentifier,
packages/react/src/React.js
+2
@@ -35,6 +35,7 @@ import {lazy} from './ReactLazy';
35 import {forwardRef} from './ReactForwardRef';
36 import {memo} from './ReactMemo';
37 import {
38 + getCacheSignal,
39 getCacheForType,
40 useCallback,
41 useContext,
@@ -119,6 +120,7 @@ export {
120 REACT_SUSPENSE_LIST_TYPE as SuspenseList,
121 REACT_LEGACY_HIDDEN_TYPE as unstable_LegacyHidden,
122 REACT_OFFSCREEN_TYPE as unstable_Offscreen,
123 + getCacheSignal as unstable_getCacheSignal,
124 getCacheForType as unstable_getCacheForType,
125 useCacheRefresh as unstable_useCacheRefresh,
126 REACT_CACHE_TYPE as unstable_Cache,
packages/react/src/ReactHooks.js
+6
@@ -41,6 +41,12 @@ function resolveDispatcher() {
41 return ((dispatcher: any): Dispatcher);
42 }
43
44 +export function getCacheSignal(): AbortSignal {
45 + const dispatcher = resolveDispatcher();
46 + // $FlowFixMe This is unstable, thus optional
47 + return dispatcher.getCacheSignal();
48 +}
49 +
50 export function getCacheForType<T>(resourceType: () => T): T {
51 const dispatcher = resolveDispatcher();
52 // $FlowFixMe This is unstable, thus optional
packages/react/unstable-shared-subset.experimental.js
+1
@@ -25,6 +25,7 @@ export {
25 memo,
26 startTransition,
27 unstable_DebugTracingMode,
28 + unstable_getCacheSignal,
29 unstable_getCacheForType,
30 unstable_useOpaqueIdentifier,
31 useCallback,
scripts/jest/setupEnvironment.js
+4
@@ -1,5 +1,7 @@
1 /* eslint-disable */
2
3 +const AbortController = require('abort-controller');
4 +
5 const NODE_ENV = process.env.NODE_ENV;
6 if (NODE_ENV !== 'development' && NODE_ENV !== 'production') {
7 throw new Error('NODE_ENV must either be set to development or production.');
@@ -21,6 +23,8 @@ global.__EXPERIMENTAL__ =
23
24 global.__VARIANT__ = !!process.env.VARIANT;
25
26 +global.AbortController = AbortController;
27 +
28 if (typeof window !== 'undefined') {
29 global.requestIdleCallback = function(callback) {
30 return setTimeout(() => {