mirror of
https://github.com/facebook/react.git
synced 2025-11-01 09:12:30 +00:00
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.
This commit is contained in:
+2
-2
@@ -73,7 +73,7 @@ Object {
|
||||
},
|
||||
},
|
||||
"duration": 15,
|
||||
"effectDuration": null,
|
||||
"effectDuration": 0,
|
||||
"fiberActualDurations": Map {
|
||||
1 => 15,
|
||||
2 => 15,
|
||||
@@ -86,7 +86,7 @@ Object {
|
||||
3 => 3,
|
||||
4 => 2,
|
||||
},
|
||||
"passiveEffectDuration": null,
|
||||
"passiveEffectDuration": 0,
|
||||
"priorityLevel": "Immediate",
|
||||
"timestamp": 15,
|
||||
"updaters": Array [
|
||||
|
||||
@@ -216,6 +216,10 @@ export function resetHooksState(): void {
|
||||
workInProgressHook = null;
|
||||
}
|
||||
|
||||
function getCacheSignal() {
|
||||
throw new Error('Not implemented.');
|
||||
}
|
||||
|
||||
function getCacheForType<T>(resourceType: () => T): T {
|
||||
throw new Error('Not implemented.');
|
||||
}
|
||||
@@ -551,6 +555,7 @@ export const Dispatcher: DispatcherType = {
|
||||
};
|
||||
|
||||
if (enableCache) {
|
||||
Dispatcher.getCacheSignal = getCacheSignal;
|
||||
Dispatcher.getCacheForType = getCacheForType;
|
||||
Dispatcher.useCacheRefresh = useCacheRefresh;
|
||||
}
|
||||
|
||||
@@ -18,8 +18,13 @@ import {REACT_CONTEXT_TYPE} from 'shared/ReactSymbols';
|
||||
import {isPrimaryRenderer} from './ReactFiberHostConfig';
|
||||
import {createCursor, push, pop} from './ReactFiberStack.new';
|
||||
import {pushProvider, popProvider} from './ReactFiberNewContext.new';
|
||||
import * as Scheduler from 'scheduler';
|
||||
|
||||
export type Cache = Map<() => mixed, mixed>;
|
||||
export type Cache = {|
|
||||
controller: AbortController,
|
||||
data: Map<() => mixed, mixed>,
|
||||
refCount: number,
|
||||
|};
|
||||
|
||||
export type CacheComponentState = {|
|
||||
+parent: Cache,
|
||||
@@ -31,6 +36,13 @@ export type SpawnedCachePool = {|
|
||||
+pool: Cache,
|
||||
|};
|
||||
|
||||
// Intentionally not named imports because Rollup would
|
||||
// use dynamic dispatch for CommonJS interop named imports.
|
||||
const {
|
||||
unstable_scheduleCallback: scheduleCallback,
|
||||
unstable_NormalPriority: NormalPriority,
|
||||
} = Scheduler;
|
||||
|
||||
export const CacheContext: ReactContext<Cache> = enableCache
|
||||
? {
|
||||
$$typeof: REACT_CONTEXT_TYPE,
|
||||
@@ -57,6 +69,58 @@ let pooledCache: Cache | null = null;
|
||||
// cache from the render that suspended.
|
||||
const prevFreshCacheOnStack: StackCursor<Cache | null> = createCursor(null);
|
||||
|
||||
// Creates a new empty Cache instance with a ref-count of 0. The caller is responsible
|
||||
// for retaining the cache once it is in use (retainCache), and releasing the cache
|
||||
// once it is no longer needed (releaseCache).
|
||||
export function createCache(): Cache {
|
||||
if (!enableCache) {
|
||||
return (null: any);
|
||||
}
|
||||
const cache: Cache = {
|
||||
controller: new AbortController(),
|
||||
data: new Map(),
|
||||
refCount: 0,
|
||||
};
|
||||
|
||||
return cache;
|
||||
}
|
||||
|
||||
export function retainCache(cache: Cache) {
|
||||
if (!enableCache) {
|
||||
return;
|
||||
}
|
||||
if (__DEV__) {
|
||||
if (cache.controller.signal.aborted) {
|
||||
console.warn(
|
||||
'A cache instance was retained after it was already freed. ' +
|
||||
'This likely indicates a bug in React.',
|
||||
);
|
||||
}
|
||||
}
|
||||
cache.refCount++;
|
||||
}
|
||||
|
||||
// Cleanup a cache instance, potentially freeing it if there are no more references
|
||||
export function releaseCache(cache: Cache) {
|
||||
if (!enableCache) {
|
||||
return;
|
||||
}
|
||||
cache.refCount--;
|
||||
if (__DEV__) {
|
||||
if (cache.refCount < 0) {
|
||||
console.warn(
|
||||
'A cache instance was released after it was already freed. ' +
|
||||
'This likely indicates a bug in React.',
|
||||
);
|
||||
}
|
||||
}
|
||||
if (cache.refCount === 0) {
|
||||
scheduleCallback(NormalPriority, () => {
|
||||
cache.controller.abort();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export function pushCacheProvider(workInProgress: Fiber, cache: Cache) {
|
||||
if (!enableCache) {
|
||||
return;
|
||||
@@ -78,8 +142,14 @@ export function requestCacheFromPool(renderLanes: Lanes): Cache {
|
||||
if (pooledCache !== null) {
|
||||
return pooledCache;
|
||||
}
|
||||
// Create a fresh cache.
|
||||
pooledCache = new Map();
|
||||
// Create a fresh cache. The pooled cache must be owned - it is freed
|
||||
// in releaseRootPooledCache() - but the cache instance handed out
|
||||
// is retained/released in the commit phase of the component that
|
||||
// references is (ie the host root, cache boundary, suspense component)
|
||||
// Ie, pooledCache is conceptually an Option<Arc<Cache>> (owned),
|
||||
// whereas the return value of this function is a &Arc<Cache> (borrowed).
|
||||
pooledCache = createCache();
|
||||
retainCache(pooledCache);
|
||||
return pooledCache;
|
||||
}
|
||||
|
||||
@@ -91,7 +161,13 @@ export function pushRootCachePool(root: FiberRoot) {
|
||||
// from `root.pooledCache`. If it's currently `null`, we will lazily
|
||||
// initialize it the first type it's requested. However, we only mutate
|
||||
// the root itself during the complete/unwind phase of the HostRoot.
|
||||
pooledCache = root.pooledCache;
|
||||
const rootCache = root.pooledCache;
|
||||
if (rootCache != null) {
|
||||
pooledCache = rootCache;
|
||||
root.pooledCache = null;
|
||||
} else {
|
||||
pooledCache = null;
|
||||
}
|
||||
}
|
||||
|
||||
export function popRootCachePool(root: FiberRoot, renderLanes: Lanes) {
|
||||
@@ -157,7 +233,6 @@ export function getSuspendedCachePool(): SpawnedCachePool | null {
|
||||
if (!enableCache) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// We check the cache on the stack first, since that's the one any new Caches
|
||||
// would have accessed.
|
||||
let pool = pooledCache;
|
||||
|
||||
@@ -18,8 +18,13 @@ import {REACT_CONTEXT_TYPE} from 'shared/ReactSymbols';
|
||||
import {isPrimaryRenderer} from './ReactFiberHostConfig';
|
||||
import {createCursor, push, pop} from './ReactFiberStack.old';
|
||||
import {pushProvider, popProvider} from './ReactFiberNewContext.old';
|
||||
import * as Scheduler from 'scheduler';
|
||||
|
||||
export type Cache = Map<() => mixed, mixed>;
|
||||
export type Cache = {|
|
||||
controller: AbortController,
|
||||
data: Map<() => mixed, mixed>,
|
||||
refCount: number,
|
||||
|};
|
||||
|
||||
export type CacheComponentState = {|
|
||||
+parent: Cache,
|
||||
@@ -31,6 +36,13 @@ export type SpawnedCachePool = {|
|
||||
+pool: Cache,
|
||||
|};
|
||||
|
||||
// Intentionally not named imports because Rollup would
|
||||
// use dynamic dispatch for CommonJS interop named imports.
|
||||
const {
|
||||
unstable_scheduleCallback: scheduleCallback,
|
||||
unstable_NormalPriority: NormalPriority,
|
||||
} = Scheduler;
|
||||
|
||||
export const CacheContext: ReactContext<Cache> = enableCache
|
||||
? {
|
||||
$$typeof: REACT_CONTEXT_TYPE,
|
||||
@@ -57,6 +69,58 @@ let pooledCache: Cache | null = null;
|
||||
// cache from the render that suspended.
|
||||
const prevFreshCacheOnStack: StackCursor<Cache | null> = createCursor(null);
|
||||
|
||||
// Creates a new empty Cache instance with a ref-count of 0. The caller is responsible
|
||||
// for retaining the cache once it is in use (retainCache), and releasing the cache
|
||||
// once it is no longer needed (releaseCache).
|
||||
export function createCache(): Cache {
|
||||
if (!enableCache) {
|
||||
return (null: any);
|
||||
}
|
||||
const cache: Cache = {
|
||||
controller: new AbortController(),
|
||||
data: new Map(),
|
||||
refCount: 0,
|
||||
};
|
||||
|
||||
return cache;
|
||||
}
|
||||
|
||||
export function retainCache(cache: Cache) {
|
||||
if (!enableCache) {
|
||||
return;
|
||||
}
|
||||
if (__DEV__) {
|
||||
if (cache.controller.signal.aborted) {
|
||||
console.warn(
|
||||
'A cache instance was retained after it was already freed. ' +
|
||||
'This likely indicates a bug in React.',
|
||||
);
|
||||
}
|
||||
}
|
||||
cache.refCount++;
|
||||
}
|
||||
|
||||
// Cleanup a cache instance, potentially freeing it if there are no more references
|
||||
export function releaseCache(cache: Cache) {
|
||||
if (!enableCache) {
|
||||
return;
|
||||
}
|
||||
cache.refCount--;
|
||||
if (__DEV__) {
|
||||
if (cache.refCount < 0) {
|
||||
console.warn(
|
||||
'A cache instance was released after it was already freed. ' +
|
||||
'This likely indicates a bug in React.',
|
||||
);
|
||||
}
|
||||
}
|
||||
if (cache.refCount === 0) {
|
||||
scheduleCallback(NormalPriority, () => {
|
||||
cache.controller.abort();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export function pushCacheProvider(workInProgress: Fiber, cache: Cache) {
|
||||
if (!enableCache) {
|
||||
return;
|
||||
@@ -78,8 +142,14 @@ export function requestCacheFromPool(renderLanes: Lanes): Cache {
|
||||
if (pooledCache !== null) {
|
||||
return pooledCache;
|
||||
}
|
||||
// Create a fresh cache.
|
||||
pooledCache = new Map();
|
||||
// Create a fresh cache. The pooled cache must be owned - it is freed
|
||||
// in releaseRootPooledCache() - but the cache instance handed out
|
||||
// is retained/released in the commit phase of the component that
|
||||
// references is (ie the host root, cache boundary, suspense component)
|
||||
// Ie, pooledCache is conceptually an Option<Arc<Cache>> (owned),
|
||||
// whereas the return value of this function is a &Arc<Cache> (borrowed).
|
||||
pooledCache = createCache();
|
||||
retainCache(pooledCache);
|
||||
return pooledCache;
|
||||
}
|
||||
|
||||
@@ -91,7 +161,13 @@ export function pushRootCachePool(root: FiberRoot) {
|
||||
// from `root.pooledCache`. If it's currently `null`, we will lazily
|
||||
// initialize it the first type it's requested. However, we only mutate
|
||||
// the root itself during the complete/unwind phase of the HostRoot.
|
||||
pooledCache = root.pooledCache;
|
||||
const rootCache = root.pooledCache;
|
||||
if (rootCache != null) {
|
||||
pooledCache = rootCache;
|
||||
root.pooledCache = null;
|
||||
} else {
|
||||
pooledCache = null;
|
||||
}
|
||||
}
|
||||
|
||||
export function popRootCachePool(root: FiberRoot, renderLanes: Lanes) {
|
||||
@@ -157,7 +233,6 @@ export function getSuspendedCachePool(): SpawnedCachePool | null {
|
||||
if (!enableCache) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// We check the cache on the stack first, since that's the one any new Caches
|
||||
// would have accessed.
|
||||
let pool = pooledCache;
|
||||
|
||||
@@ -24,6 +24,7 @@ import type {FunctionComponentUpdateQueue} from './ReactFiberHooks.new';
|
||||
import type {Wakeable} from 'shared/ReactTypes';
|
||||
import type {OffscreenState} from './ReactFiberOffscreenComponent';
|
||||
import type {HookFlags} from './ReactHookEffectTags';
|
||||
import type {Cache} from './ReactFiberCacheComponent.new';
|
||||
|
||||
import {
|
||||
enableCreateEventHandleAPI,
|
||||
@@ -39,6 +40,7 @@ import {
|
||||
enableSuspenseLayoutEffectSemantics,
|
||||
enableUpdaterTracking,
|
||||
warnAboutCallbackRefReturningFunction,
|
||||
enableCache,
|
||||
} from 'shared/ReactFeatureFlags';
|
||||
import {
|
||||
FunctionComponent,
|
||||
@@ -58,6 +60,7 @@ import {
|
||||
ScopeComponent,
|
||||
OffscreenComponent,
|
||||
LegacyHiddenComponent,
|
||||
CacheComponent,
|
||||
} from './ReactWorkTags';
|
||||
import {detachDeletedInstance} from './ReactFiberHostConfig';
|
||||
import {
|
||||
@@ -153,6 +156,7 @@ import {
|
||||
markComponentLayoutEffectUnmountStarted,
|
||||
markComponentLayoutEffectUnmountStopped,
|
||||
} from './SchedulingProfiler';
|
||||
import {releaseCache, retainCache} from './ReactFiberCacheComponent.new';
|
||||
|
||||
let didWarnAboutUndefinedSnapshotBeforeUpdate: Set<mixed> | null = null;
|
||||
if (__DEV__) {
|
||||
@@ -2665,6 +2669,82 @@ function commitPassiveMountOnFiber(
|
||||
}
|
||||
break;
|
||||
}
|
||||
case HostRoot: {
|
||||
if (enableCache) {
|
||||
let previousCache: Cache | null = null;
|
||||
if (finishedWork.alternate !== null) {
|
||||
previousCache = finishedWork.alternate.memoizedState.cache;
|
||||
}
|
||||
const nextCache = finishedWork.memoizedState.cache;
|
||||
// Retain/release the root cache.
|
||||
// Note that on initial mount, previousCache and nextCache will be the same
|
||||
// and this retain won't occur. To counter this, we instead retain the HostRoot's
|
||||
// initial cache when creating the root itself (see createFiberRoot() in
|
||||
// ReactFiberRoot.js). Subsequent updates that change the cache are reflected
|
||||
// here, such that previous/next caches are retained correctly.
|
||||
if (nextCache !== previousCache) {
|
||||
retainCache(nextCache);
|
||||
if (previousCache != null) {
|
||||
releaseCache(previousCache);
|
||||
}
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
case LegacyHiddenComponent:
|
||||
case OffscreenComponent: {
|
||||
if (enableCache) {
|
||||
let previousCache: Cache | null = null;
|
||||
if (
|
||||
finishedWork.alternate !== null &&
|
||||
finishedWork.alternate.memoizedState !== null &&
|
||||
finishedWork.alternate.memoizedState.cachePool !== null
|
||||
) {
|
||||
previousCache = finishedWork.alternate.memoizedState.cachePool.pool;
|
||||
}
|
||||
let nextCache: Cache | null = null;
|
||||
if (
|
||||
finishedWork.memoizedState !== null &&
|
||||
finishedWork.memoizedState.cachePool !== null
|
||||
) {
|
||||
nextCache = finishedWork.memoizedState.cachePool.pool;
|
||||
}
|
||||
// Retain/release the cache used for pending (suspended) nodes.
|
||||
// Note that this is only reached in the non-suspended/visible case:
|
||||
// when the content is suspended/hidden, the retain/release occurs
|
||||
// via the parent Suspense component (see case above).
|
||||
if (nextCache !== previousCache) {
|
||||
if (nextCache != null) {
|
||||
retainCache(nextCache);
|
||||
}
|
||||
if (previousCache != null) {
|
||||
releaseCache(previousCache);
|
||||
}
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
case CacheComponent: {
|
||||
if (enableCache) {
|
||||
let previousCache: Cache | null = null;
|
||||
if (finishedWork.alternate !== null) {
|
||||
previousCache = finishedWork.alternate.memoizedState.cache;
|
||||
}
|
||||
const nextCache = finishedWork.memoizedState.cache;
|
||||
// Retain/release the cache. In theory the cache component
|
||||
// could be "borrowing" a cache instance owned by some parent,
|
||||
// in which case we could avoid retaining/releasing. But it
|
||||
// is non-trivial to determine when that is the case, so we
|
||||
// always retain/release.
|
||||
if (nextCache !== previousCache) {
|
||||
retainCache(nextCache);
|
||||
if (previousCache != null) {
|
||||
releaseCache(previousCache);
|
||||
}
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2871,6 +2951,43 @@ function commitPassiveUnmountInsideDeletedTreeOnFiber(
|
||||
}
|
||||
break;
|
||||
}
|
||||
// TODO: run passive unmount effects when unmounting a root.
|
||||
// Because passive unmount effects are not currently run,
|
||||
// the cache instance owned by the root will never be freed.
|
||||
// When effects are run, the cache should be freed here:
|
||||
// case HostRoot: {
|
||||
// if (enableCache) {
|
||||
// const cache = current.memoizedState.cache;
|
||||
// releaseCache(cache);
|
||||
// }
|
||||
// break;
|
||||
// }
|
||||
case LegacyHiddenComponent:
|
||||
case OffscreenComponent: {
|
||||
if (enableCache) {
|
||||
if (
|
||||
current.memoizedState !== null &&
|
||||
current.memoizedState.cachePool !== null
|
||||
) {
|
||||
const cache: Cache = current.memoizedState.cachePool.pool;
|
||||
// Retain/release the cache used for pending (suspended) nodes.
|
||||
// Note that this is only reached in the non-suspended/visible case:
|
||||
// when the content is suspended/hidden, the retain/release occurs
|
||||
// via the parent Suspense component (see case above).
|
||||
if (cache != null) {
|
||||
retainCache(cache);
|
||||
}
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
case CacheComponent: {
|
||||
if (enableCache) {
|
||||
const cache = current.memoizedState.cache;
|
||||
releaseCache(cache);
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -24,6 +24,7 @@ import type {FunctionComponentUpdateQueue} from './ReactFiberHooks.old';
|
||||
import type {Wakeable} from 'shared/ReactTypes';
|
||||
import type {OffscreenState} from './ReactFiberOffscreenComponent';
|
||||
import type {HookFlags} from './ReactHookEffectTags';
|
||||
import type {Cache} from './ReactFiberCacheComponent.old';
|
||||
|
||||
import {
|
||||
enableCreateEventHandleAPI,
|
||||
@@ -39,6 +40,7 @@ import {
|
||||
enableSuspenseLayoutEffectSemantics,
|
||||
enableUpdaterTracking,
|
||||
warnAboutCallbackRefReturningFunction,
|
||||
enableCache,
|
||||
} from 'shared/ReactFeatureFlags';
|
||||
import {
|
||||
FunctionComponent,
|
||||
@@ -58,6 +60,7 @@ import {
|
||||
ScopeComponent,
|
||||
OffscreenComponent,
|
||||
LegacyHiddenComponent,
|
||||
CacheComponent,
|
||||
} from './ReactWorkTags';
|
||||
import {detachDeletedInstance} from './ReactFiberHostConfig';
|
||||
import {
|
||||
@@ -153,6 +156,7 @@ import {
|
||||
markComponentLayoutEffectUnmountStarted,
|
||||
markComponentLayoutEffectUnmountStopped,
|
||||
} from './SchedulingProfiler';
|
||||
import {releaseCache, retainCache} from './ReactFiberCacheComponent.old';
|
||||
|
||||
let didWarnAboutUndefinedSnapshotBeforeUpdate: Set<mixed> | null = null;
|
||||
if (__DEV__) {
|
||||
@@ -2665,6 +2669,82 @@ function commitPassiveMountOnFiber(
|
||||
}
|
||||
break;
|
||||
}
|
||||
case HostRoot: {
|
||||
if (enableCache) {
|
||||
let previousCache: Cache | null = null;
|
||||
if (finishedWork.alternate !== null) {
|
||||
previousCache = finishedWork.alternate.memoizedState.cache;
|
||||
}
|
||||
const nextCache = finishedWork.memoizedState.cache;
|
||||
// Retain/release the root cache.
|
||||
// Note that on initial mount, previousCache and nextCache will be the same
|
||||
// and this retain won't occur. To counter this, we instead retain the HostRoot's
|
||||
// initial cache when creating the root itself (see createFiberRoot() in
|
||||
// ReactFiberRoot.js). Subsequent updates that change the cache are reflected
|
||||
// here, such that previous/next caches are retained correctly.
|
||||
if (nextCache !== previousCache) {
|
||||
retainCache(nextCache);
|
||||
if (previousCache != null) {
|
||||
releaseCache(previousCache);
|
||||
}
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
case LegacyHiddenComponent:
|
||||
case OffscreenComponent: {
|
||||
if (enableCache) {
|
||||
let previousCache: Cache | null = null;
|
||||
if (
|
||||
finishedWork.alternate !== null &&
|
||||
finishedWork.alternate.memoizedState !== null &&
|
||||
finishedWork.alternate.memoizedState.cachePool !== null
|
||||
) {
|
||||
previousCache = finishedWork.alternate.memoizedState.cachePool.pool;
|
||||
}
|
||||
let nextCache: Cache | null = null;
|
||||
if (
|
||||
finishedWork.memoizedState !== null &&
|
||||
finishedWork.memoizedState.cachePool !== null
|
||||
) {
|
||||
nextCache = finishedWork.memoizedState.cachePool.pool;
|
||||
}
|
||||
// Retain/release the cache used for pending (suspended) nodes.
|
||||
// Note that this is only reached in the non-suspended/visible case:
|
||||
// when the content is suspended/hidden, the retain/release occurs
|
||||
// via the parent Suspense component (see case above).
|
||||
if (nextCache !== previousCache) {
|
||||
if (nextCache != null) {
|
||||
retainCache(nextCache);
|
||||
}
|
||||
if (previousCache != null) {
|
||||
releaseCache(previousCache);
|
||||
}
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
case CacheComponent: {
|
||||
if (enableCache) {
|
||||
let previousCache: Cache | null = null;
|
||||
if (finishedWork.alternate !== null) {
|
||||
previousCache = finishedWork.alternate.memoizedState.cache;
|
||||
}
|
||||
const nextCache = finishedWork.memoizedState.cache;
|
||||
// Retain/release the cache. In theory the cache component
|
||||
// could be "borrowing" a cache instance owned by some parent,
|
||||
// in which case we could avoid retaining/releasing. But it
|
||||
// is non-trivial to determine when that is the case, so we
|
||||
// always retain/release.
|
||||
if (nextCache !== previousCache) {
|
||||
retainCache(nextCache);
|
||||
if (previousCache != null) {
|
||||
releaseCache(previousCache);
|
||||
}
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2871,6 +2951,43 @@ function commitPassiveUnmountInsideDeletedTreeOnFiber(
|
||||
}
|
||||
break;
|
||||
}
|
||||
// TODO: run passive unmount effects when unmounting a root.
|
||||
// Because passive unmount effects are not currently run,
|
||||
// the cache instance owned by the root will never be freed.
|
||||
// When effects are run, the cache should be freed here:
|
||||
// case HostRoot: {
|
||||
// if (enableCache) {
|
||||
// const cache = current.memoizedState.cache;
|
||||
// releaseCache(cache);
|
||||
// }
|
||||
// break;
|
||||
// }
|
||||
case LegacyHiddenComponent:
|
||||
case OffscreenComponent: {
|
||||
if (enableCache) {
|
||||
if (
|
||||
current.memoizedState !== null &&
|
||||
current.memoizedState.cachePool !== null
|
||||
) {
|
||||
const cache: Cache = current.memoizedState.cachePool.pool;
|
||||
// Retain/release the cache used for pending (suspended) nodes.
|
||||
// Note that this is only reached in the non-suspended/visible case:
|
||||
// when the content is suspended/hidden, the retain/release occurs
|
||||
// via the parent Suspense component (see case above).
|
||||
if (cache != null) {
|
||||
retainCache(cache);
|
||||
}
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
case CacheComponent: {
|
||||
if (enableCache) {
|
||||
const cache = current.memoizedState.cache;
|
||||
releaseCache(cache);
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -72,6 +72,7 @@ import {
|
||||
ChildDeletion,
|
||||
StaticMask,
|
||||
MutationMask,
|
||||
Passive,
|
||||
} from './ReactFiberFlags';
|
||||
|
||||
import {
|
||||
@@ -848,7 +849,15 @@ function completeWork(
|
||||
if (enableCache) {
|
||||
popRootCachePool(fiberRoot, renderLanes);
|
||||
|
||||
let previousCache: Cache | null = null;
|
||||
if (workInProgress.alternate !== null) {
|
||||
previousCache = workInProgress.alternate.memoizedState.cache;
|
||||
}
|
||||
const cache: Cache = workInProgress.memoizedState.cache;
|
||||
if (cache !== previousCache) {
|
||||
// Run passive effects to retain/release the cache.
|
||||
workInProgress.flags |= Passive;
|
||||
}
|
||||
popCacheProvider(workInProgress, cache);
|
||||
}
|
||||
popHostContainer(workInProgress);
|
||||
@@ -1089,6 +1098,29 @@ function completeWork(
|
||||
prevDidTimeout = prevState !== null;
|
||||
}
|
||||
|
||||
if (enableCache && nextDidTimeout) {
|
||||
const offscreenFiber: Fiber = (workInProgress.child: any);
|
||||
let previousCache: Cache | null = null;
|
||||
if (
|
||||
offscreenFiber.alternate !== null &&
|
||||
offscreenFiber.alternate.memoizedState !== null &&
|
||||
offscreenFiber.alternate.memoizedState.cachePool !== null
|
||||
) {
|
||||
previousCache = offscreenFiber.alternate.memoizedState.cachePool.pool;
|
||||
}
|
||||
let cache: Cache | null = null;
|
||||
if (
|
||||
offscreenFiber.memoizedState !== null &&
|
||||
offscreenFiber.memoizedState.cachePool !== null
|
||||
) {
|
||||
cache = offscreenFiber.memoizedState.cachePool.pool;
|
||||
}
|
||||
if (cache !== previousCache) {
|
||||
// Run passive effects to retain/release the cache.
|
||||
offscreenFiber.flags |= Passive;
|
||||
}
|
||||
}
|
||||
|
||||
// If the suspended state of the boundary changes, we need to schedule
|
||||
// an effect to toggle the subtree's visibility. When we switch from
|
||||
// fallback -> primary, the inner Offscreen fiber schedules this effect
|
||||
@@ -1465,6 +1497,25 @@ function completeWork(
|
||||
}
|
||||
|
||||
if (enableCache) {
|
||||
let previousCache: Cache | null = null;
|
||||
if (
|
||||
workInProgress.alternate !== null &&
|
||||
workInProgress.alternate.memoizedState !== null &&
|
||||
workInProgress.alternate.memoizedState.cachePool !== null
|
||||
) {
|
||||
previousCache = workInProgress.alternate.memoizedState.cachePool.pool;
|
||||
}
|
||||
let cache: Cache | null = null;
|
||||
if (
|
||||
workInProgress.memoizedState !== null &&
|
||||
workInProgress.memoizedState.cachePool !== null
|
||||
) {
|
||||
cache = workInProgress.memoizedState.cachePool.pool;
|
||||
}
|
||||
if (cache !== previousCache) {
|
||||
// Run passive effects to retain/release the cache.
|
||||
workInProgress.flags |= Passive;
|
||||
}
|
||||
const spawnedCachePool: SpawnedCachePool | null = (workInProgress.updateQueue: any);
|
||||
if (spawnedCachePool !== null) {
|
||||
popCachePool(workInProgress);
|
||||
@@ -1475,7 +1526,15 @@ function completeWork(
|
||||
}
|
||||
case CacheComponent: {
|
||||
if (enableCache) {
|
||||
let previousCache: Cache | null = null;
|
||||
if (workInProgress.alternate !== null) {
|
||||
previousCache = workInProgress.alternate.memoizedState.cache;
|
||||
}
|
||||
const cache: Cache = workInProgress.memoizedState.cache;
|
||||
if (cache !== previousCache) {
|
||||
// Run passive effects to retain/release the cache.
|
||||
workInProgress.flags |= Passive;
|
||||
}
|
||||
popCacheProvider(workInProgress, cache);
|
||||
bubbleProperties(workInProgress);
|
||||
return null;
|
||||
|
||||
@@ -72,6 +72,7 @@ import {
|
||||
ChildDeletion,
|
||||
StaticMask,
|
||||
MutationMask,
|
||||
Passive,
|
||||
} from './ReactFiberFlags';
|
||||
|
||||
import {
|
||||
@@ -848,7 +849,15 @@ function completeWork(
|
||||
if (enableCache) {
|
||||
popRootCachePool(fiberRoot, renderLanes);
|
||||
|
||||
let previousCache: Cache | null = null;
|
||||
if (workInProgress.alternate !== null) {
|
||||
previousCache = workInProgress.alternate.memoizedState.cache;
|
||||
}
|
||||
const cache: Cache = workInProgress.memoizedState.cache;
|
||||
if (cache !== previousCache) {
|
||||
// Run passive effects to retain/release the cache.
|
||||
workInProgress.flags |= Passive;
|
||||
}
|
||||
popCacheProvider(workInProgress, cache);
|
||||
}
|
||||
popHostContainer(workInProgress);
|
||||
@@ -1089,6 +1098,29 @@ function completeWork(
|
||||
prevDidTimeout = prevState !== null;
|
||||
}
|
||||
|
||||
if (enableCache && nextDidTimeout) {
|
||||
const offscreenFiber: Fiber = (workInProgress.child: any);
|
||||
let previousCache: Cache | null = null;
|
||||
if (
|
||||
offscreenFiber.alternate !== null &&
|
||||
offscreenFiber.alternate.memoizedState !== null &&
|
||||
offscreenFiber.alternate.memoizedState.cachePool !== null
|
||||
) {
|
||||
previousCache = offscreenFiber.alternate.memoizedState.cachePool.pool;
|
||||
}
|
||||
let cache: Cache | null = null;
|
||||
if (
|
||||
offscreenFiber.memoizedState !== null &&
|
||||
offscreenFiber.memoizedState.cachePool !== null
|
||||
) {
|
||||
cache = offscreenFiber.memoizedState.cachePool.pool;
|
||||
}
|
||||
if (cache !== previousCache) {
|
||||
// Run passive effects to retain/release the cache.
|
||||
offscreenFiber.flags |= Passive;
|
||||
}
|
||||
}
|
||||
|
||||
// If the suspended state of the boundary changes, we need to schedule
|
||||
// an effect to toggle the subtree's visibility. When we switch from
|
||||
// fallback -> primary, the inner Offscreen fiber schedules this effect
|
||||
@@ -1465,6 +1497,25 @@ function completeWork(
|
||||
}
|
||||
|
||||
if (enableCache) {
|
||||
let previousCache: Cache | null = null;
|
||||
if (
|
||||
workInProgress.alternate !== null &&
|
||||
workInProgress.alternate.memoizedState !== null &&
|
||||
workInProgress.alternate.memoizedState.cachePool !== null
|
||||
) {
|
||||
previousCache = workInProgress.alternate.memoizedState.cachePool.pool;
|
||||
}
|
||||
let cache: Cache | null = null;
|
||||
if (
|
||||
workInProgress.memoizedState !== null &&
|
||||
workInProgress.memoizedState.cachePool !== null
|
||||
) {
|
||||
cache = workInProgress.memoizedState.cachePool.pool;
|
||||
}
|
||||
if (cache !== previousCache) {
|
||||
// Run passive effects to retain/release the cache.
|
||||
workInProgress.flags |= Passive;
|
||||
}
|
||||
const spawnedCachePool: SpawnedCachePool | null = (workInProgress.updateQueue: any);
|
||||
if (spawnedCachePool !== null) {
|
||||
popCachePool(workInProgress);
|
||||
@@ -1475,7 +1526,15 @@ function completeWork(
|
||||
}
|
||||
case CacheComponent: {
|
||||
if (enableCache) {
|
||||
let previousCache: Cache | null = null;
|
||||
if (workInProgress.alternate !== null) {
|
||||
previousCache = workInProgress.alternate.memoizedState.cache;
|
||||
}
|
||||
const cache: Cache = workInProgress.memoizedState.cache;
|
||||
if (cache !== previousCache) {
|
||||
// Run passive effects to retain/release the cache.
|
||||
workInProgress.flags |= Passive;
|
||||
}
|
||||
popCacheProvider(workInProgress, cache);
|
||||
bubbleProperties(workInProgress);
|
||||
return null;
|
||||
|
||||
@@ -110,7 +110,7 @@ import {
|
||||
import {getIsRendering} from './ReactCurrentFiber';
|
||||
import {logStateUpdateScheduled} from './DebugTracing';
|
||||
import {markStateUpdateScheduled} from './SchedulingProfiler';
|
||||
import {CacheContext} from './ReactFiberCacheComponent.new';
|
||||
import {createCache, CacheContext} from './ReactFiberCacheComponent.new';
|
||||
import {
|
||||
createUpdate as createLegacyQueueUpdate,
|
||||
enqueueUpdate as enqueueLegacyQueueUpdate,
|
||||
@@ -2124,6 +2124,9 @@ function updateRefresh() {
|
||||
}
|
||||
|
||||
function refreshCache<T>(fiber: Fiber, seedKey: ?() => T, seedValue: T) {
|
||||
if (!enableCache) {
|
||||
return;
|
||||
}
|
||||
// TODO: Does Cache work in legacy mode? Should decide and write a test.
|
||||
// TODO: Consider warning if the refresh is at discrete priority, or if we
|
||||
// otherwise suspect that it wasn't batched properly.
|
||||
@@ -2139,11 +2142,14 @@ function refreshCache<T>(fiber: Fiber, seedKey: ?() => T, seedValue: T) {
|
||||
entangleLegacyQueueTransitions(root, provider, lane);
|
||||
}
|
||||
|
||||
const seededCache = new Map();
|
||||
// TODO: If a refresh never commits, the new cache created here must be
|
||||
// released. A simple case is start refreshing a cache boundary, but then
|
||||
// unmount that boundary before the refresh completes.
|
||||
const seededCache = createCache();
|
||||
if (seedKey !== null && seedKey !== undefined && root !== null) {
|
||||
// Seed the cache with the value passed by the caller. This could be
|
||||
// from a server mutation, or it could be a streaming response.
|
||||
seededCache.set(seedKey, seedValue);
|
||||
seededCache.data.set(seedKey, seedValue);
|
||||
}
|
||||
|
||||
// Schedule an update on the cache boundary to trigger a refresh.
|
||||
@@ -2390,15 +2396,23 @@ function markUpdateInDevTools(fiber, lane, action) {
|
||||
}
|
||||
}
|
||||
|
||||
function getCacheSignal(): AbortSignal {
|
||||
if (!enableCache) {
|
||||
throw new Error('Not implemented.');
|
||||
}
|
||||
const cache: Cache = readContext(CacheContext);
|
||||
return cache.controller.signal;
|
||||
}
|
||||
|
||||
function getCacheForType<T>(resourceType: () => T): T {
|
||||
if (!enableCache) {
|
||||
throw new Error('Not implemented.');
|
||||
}
|
||||
const cache: Cache = readContext(CacheContext);
|
||||
let cacheForType: T | void = (cache.get(resourceType): any);
|
||||
let cacheForType: T | void = (cache.data.get(resourceType): any);
|
||||
if (cacheForType === undefined) {
|
||||
cacheForType = resourceType();
|
||||
cache.set(resourceType, cacheForType);
|
||||
cache.data.set(resourceType, cacheForType);
|
||||
}
|
||||
return cacheForType;
|
||||
}
|
||||
@@ -2426,6 +2440,7 @@ export const ContextOnlyDispatcher: Dispatcher = {
|
||||
unstable_isNewReconciler: enableNewReconciler,
|
||||
};
|
||||
if (enableCache) {
|
||||
(ContextOnlyDispatcher: Dispatcher).getCacheSignal = getCacheSignal;
|
||||
(ContextOnlyDispatcher: Dispatcher).getCacheForType = getCacheForType;
|
||||
(ContextOnlyDispatcher: Dispatcher).useCacheRefresh = throwInvalidHookError;
|
||||
}
|
||||
@@ -2453,6 +2468,7 @@ const HooksDispatcherOnMount: Dispatcher = {
|
||||
unstable_isNewReconciler: enableNewReconciler,
|
||||
};
|
||||
if (enableCache) {
|
||||
(HooksDispatcherOnMount: Dispatcher).getCacheSignal = getCacheSignal;
|
||||
(HooksDispatcherOnMount: Dispatcher).getCacheForType = getCacheForType;
|
||||
(HooksDispatcherOnMount: Dispatcher).useCacheRefresh = mountRefresh;
|
||||
}
|
||||
@@ -2480,6 +2496,7 @@ const HooksDispatcherOnUpdate: Dispatcher = {
|
||||
unstable_isNewReconciler: enableNewReconciler,
|
||||
};
|
||||
if (enableCache) {
|
||||
(HooksDispatcherOnUpdate: Dispatcher).getCacheSignal = getCacheSignal;
|
||||
(HooksDispatcherOnUpdate: Dispatcher).getCacheForType = getCacheForType;
|
||||
(HooksDispatcherOnUpdate: Dispatcher).useCacheRefresh = updateRefresh;
|
||||
}
|
||||
@@ -2507,6 +2524,7 @@ const HooksDispatcherOnRerender: Dispatcher = {
|
||||
unstable_isNewReconciler: enableNewReconciler,
|
||||
};
|
||||
if (enableCache) {
|
||||
(HooksDispatcherOnRerender: Dispatcher).getCacheSignal = getCacheSignal;
|
||||
(HooksDispatcherOnRerender: Dispatcher).getCacheForType = getCacheForType;
|
||||
(HooksDispatcherOnRerender: Dispatcher).useCacheRefresh = updateRefresh;
|
||||
}
|
||||
@@ -2677,6 +2695,7 @@ if (__DEV__) {
|
||||
unstable_isNewReconciler: enableNewReconciler,
|
||||
};
|
||||
if (enableCache) {
|
||||
(HooksDispatcherOnMountInDEV: Dispatcher).getCacheSignal = getCacheSignal;
|
||||
(HooksDispatcherOnMountInDEV: Dispatcher).getCacheForType = getCacheForType;
|
||||
(HooksDispatcherOnMountInDEV: Dispatcher).useCacheRefresh = function useCacheRefresh() {
|
||||
currentHookNameInDev = 'useCacheRefresh';
|
||||
@@ -2818,6 +2837,7 @@ if (__DEV__) {
|
||||
unstable_isNewReconciler: enableNewReconciler,
|
||||
};
|
||||
if (enableCache) {
|
||||
(HooksDispatcherOnMountWithHookTypesInDEV: Dispatcher).getCacheSignal = getCacheSignal;
|
||||
(HooksDispatcherOnMountWithHookTypesInDEV: Dispatcher).getCacheForType = getCacheForType;
|
||||
(HooksDispatcherOnMountWithHookTypesInDEV: Dispatcher).useCacheRefresh = function useCacheRefresh() {
|
||||
currentHookNameInDev = 'useCacheRefresh';
|
||||
@@ -2959,6 +2979,7 @@ if (__DEV__) {
|
||||
unstable_isNewReconciler: enableNewReconciler,
|
||||
};
|
||||
if (enableCache) {
|
||||
(HooksDispatcherOnUpdateInDEV: Dispatcher).getCacheSignal = getCacheSignal;
|
||||
(HooksDispatcherOnUpdateInDEV: Dispatcher).getCacheForType = getCacheForType;
|
||||
(HooksDispatcherOnUpdateInDEV: Dispatcher).useCacheRefresh = function useCacheRefresh() {
|
||||
currentHookNameInDev = 'useCacheRefresh';
|
||||
@@ -3101,6 +3122,7 @@ if (__DEV__) {
|
||||
unstable_isNewReconciler: enableNewReconciler,
|
||||
};
|
||||
if (enableCache) {
|
||||
(HooksDispatcherOnRerenderInDEV: Dispatcher).getCacheSignal = getCacheSignal;
|
||||
(HooksDispatcherOnRerenderInDEV: Dispatcher).getCacheForType = getCacheForType;
|
||||
(HooksDispatcherOnRerenderInDEV: Dispatcher).useCacheRefresh = function useCacheRefresh() {
|
||||
currentHookNameInDev = 'useCacheRefresh';
|
||||
@@ -3259,6 +3281,7 @@ if (__DEV__) {
|
||||
unstable_isNewReconciler: enableNewReconciler,
|
||||
};
|
||||
if (enableCache) {
|
||||
(InvalidNestedHooksDispatcherOnMountInDEV: Dispatcher).getCacheSignal = getCacheSignal;
|
||||
(InvalidNestedHooksDispatcherOnMountInDEV: Dispatcher).getCacheForType = getCacheForType;
|
||||
(InvalidNestedHooksDispatcherOnMountInDEV: Dispatcher).useCacheRefresh = function useCacheRefresh() {
|
||||
currentHookNameInDev = 'useCacheRefresh';
|
||||
@@ -3417,6 +3440,7 @@ if (__DEV__) {
|
||||
unstable_isNewReconciler: enableNewReconciler,
|
||||
};
|
||||
if (enableCache) {
|
||||
(InvalidNestedHooksDispatcherOnUpdateInDEV: Dispatcher).getCacheSignal = getCacheSignal;
|
||||
(InvalidNestedHooksDispatcherOnUpdateInDEV: Dispatcher).getCacheForType = getCacheForType;
|
||||
(InvalidNestedHooksDispatcherOnUpdateInDEV: Dispatcher).useCacheRefresh = function useCacheRefresh() {
|
||||
currentHookNameInDev = 'useCacheRefresh';
|
||||
@@ -3576,6 +3600,7 @@ if (__DEV__) {
|
||||
unstable_isNewReconciler: enableNewReconciler,
|
||||
};
|
||||
if (enableCache) {
|
||||
(InvalidNestedHooksDispatcherOnRerenderInDEV: Dispatcher).getCacheSignal = getCacheSignal;
|
||||
(InvalidNestedHooksDispatcherOnRerenderInDEV: Dispatcher).getCacheForType = getCacheForType;
|
||||
(InvalidNestedHooksDispatcherOnRerenderInDEV: Dispatcher).useCacheRefresh = function useCacheRefresh() {
|
||||
currentHookNameInDev = 'useCacheRefresh';
|
||||
|
||||
@@ -110,7 +110,7 @@ import {
|
||||
import {getIsRendering} from './ReactCurrentFiber';
|
||||
import {logStateUpdateScheduled} from './DebugTracing';
|
||||
import {markStateUpdateScheduled} from './SchedulingProfiler';
|
||||
import {CacheContext} from './ReactFiberCacheComponent.old';
|
||||
import {createCache, CacheContext} from './ReactFiberCacheComponent.old';
|
||||
import {
|
||||
createUpdate as createLegacyQueueUpdate,
|
||||
enqueueUpdate as enqueueLegacyQueueUpdate,
|
||||
@@ -2124,6 +2124,9 @@ function updateRefresh() {
|
||||
}
|
||||
|
||||
function refreshCache<T>(fiber: Fiber, seedKey: ?() => T, seedValue: T) {
|
||||
if (!enableCache) {
|
||||
return;
|
||||
}
|
||||
// TODO: Does Cache work in legacy mode? Should decide and write a test.
|
||||
// TODO: Consider warning if the refresh is at discrete priority, or if we
|
||||
// otherwise suspect that it wasn't batched properly.
|
||||
@@ -2139,11 +2142,14 @@ function refreshCache<T>(fiber: Fiber, seedKey: ?() => T, seedValue: T) {
|
||||
entangleLegacyQueueTransitions(root, provider, lane);
|
||||
}
|
||||
|
||||
const seededCache = new Map();
|
||||
// TODO: If a refresh never commits, the new cache created here must be
|
||||
// released. A simple case is start refreshing a cache boundary, but then
|
||||
// unmount that boundary before the refresh completes.
|
||||
const seededCache = createCache();
|
||||
if (seedKey !== null && seedKey !== undefined && root !== null) {
|
||||
// Seed the cache with the value passed by the caller. This could be
|
||||
// from a server mutation, or it could be a streaming response.
|
||||
seededCache.set(seedKey, seedValue);
|
||||
seededCache.data.set(seedKey, seedValue);
|
||||
}
|
||||
|
||||
// Schedule an update on the cache boundary to trigger a refresh.
|
||||
@@ -2390,15 +2396,23 @@ function markUpdateInDevTools(fiber, lane, action) {
|
||||
}
|
||||
}
|
||||
|
||||
function getCacheSignal(): AbortSignal {
|
||||
if (!enableCache) {
|
||||
throw new Error('Not implemented.');
|
||||
}
|
||||
const cache: Cache = readContext(CacheContext);
|
||||
return cache.controller.signal;
|
||||
}
|
||||
|
||||
function getCacheForType<T>(resourceType: () => T): T {
|
||||
if (!enableCache) {
|
||||
throw new Error('Not implemented.');
|
||||
}
|
||||
const cache: Cache = readContext(CacheContext);
|
||||
let cacheForType: T | void = (cache.get(resourceType): any);
|
||||
let cacheForType: T | void = (cache.data.get(resourceType): any);
|
||||
if (cacheForType === undefined) {
|
||||
cacheForType = resourceType();
|
||||
cache.set(resourceType, cacheForType);
|
||||
cache.data.set(resourceType, cacheForType);
|
||||
}
|
||||
return cacheForType;
|
||||
}
|
||||
@@ -2426,6 +2440,7 @@ export const ContextOnlyDispatcher: Dispatcher = {
|
||||
unstable_isNewReconciler: enableNewReconciler,
|
||||
};
|
||||
if (enableCache) {
|
||||
(ContextOnlyDispatcher: Dispatcher).getCacheSignal = getCacheSignal;
|
||||
(ContextOnlyDispatcher: Dispatcher).getCacheForType = getCacheForType;
|
||||
(ContextOnlyDispatcher: Dispatcher).useCacheRefresh = throwInvalidHookError;
|
||||
}
|
||||
@@ -2453,6 +2468,7 @@ const HooksDispatcherOnMount: Dispatcher = {
|
||||
unstable_isNewReconciler: enableNewReconciler,
|
||||
};
|
||||
if (enableCache) {
|
||||
(HooksDispatcherOnMount: Dispatcher).getCacheSignal = getCacheSignal;
|
||||
(HooksDispatcherOnMount: Dispatcher).getCacheForType = getCacheForType;
|
||||
(HooksDispatcherOnMount: Dispatcher).useCacheRefresh = mountRefresh;
|
||||
}
|
||||
@@ -2480,6 +2496,7 @@ const HooksDispatcherOnUpdate: Dispatcher = {
|
||||
unstable_isNewReconciler: enableNewReconciler,
|
||||
};
|
||||
if (enableCache) {
|
||||
(HooksDispatcherOnUpdate: Dispatcher).getCacheSignal = getCacheSignal;
|
||||
(HooksDispatcherOnUpdate: Dispatcher).getCacheForType = getCacheForType;
|
||||
(HooksDispatcherOnUpdate: Dispatcher).useCacheRefresh = updateRefresh;
|
||||
}
|
||||
@@ -2507,6 +2524,7 @@ const HooksDispatcherOnRerender: Dispatcher = {
|
||||
unstable_isNewReconciler: enableNewReconciler,
|
||||
};
|
||||
if (enableCache) {
|
||||
(HooksDispatcherOnRerender: Dispatcher).getCacheSignal = getCacheSignal;
|
||||
(HooksDispatcherOnRerender: Dispatcher).getCacheForType = getCacheForType;
|
||||
(HooksDispatcherOnRerender: Dispatcher).useCacheRefresh = updateRefresh;
|
||||
}
|
||||
@@ -2677,6 +2695,7 @@ if (__DEV__) {
|
||||
unstable_isNewReconciler: enableNewReconciler,
|
||||
};
|
||||
if (enableCache) {
|
||||
(HooksDispatcherOnMountInDEV: Dispatcher).getCacheSignal = getCacheSignal;
|
||||
(HooksDispatcherOnMountInDEV: Dispatcher).getCacheForType = getCacheForType;
|
||||
(HooksDispatcherOnMountInDEV: Dispatcher).useCacheRefresh = function useCacheRefresh() {
|
||||
currentHookNameInDev = 'useCacheRefresh';
|
||||
@@ -2818,6 +2837,7 @@ if (__DEV__) {
|
||||
unstable_isNewReconciler: enableNewReconciler,
|
||||
};
|
||||
if (enableCache) {
|
||||
(HooksDispatcherOnMountWithHookTypesInDEV: Dispatcher).getCacheSignal = getCacheSignal;
|
||||
(HooksDispatcherOnMountWithHookTypesInDEV: Dispatcher).getCacheForType = getCacheForType;
|
||||
(HooksDispatcherOnMountWithHookTypesInDEV: Dispatcher).useCacheRefresh = function useCacheRefresh() {
|
||||
currentHookNameInDev = 'useCacheRefresh';
|
||||
@@ -2959,6 +2979,7 @@ if (__DEV__) {
|
||||
unstable_isNewReconciler: enableNewReconciler,
|
||||
};
|
||||
if (enableCache) {
|
||||
(HooksDispatcherOnUpdateInDEV: Dispatcher).getCacheSignal = getCacheSignal;
|
||||
(HooksDispatcherOnUpdateInDEV: Dispatcher).getCacheForType = getCacheForType;
|
||||
(HooksDispatcherOnUpdateInDEV: Dispatcher).useCacheRefresh = function useCacheRefresh() {
|
||||
currentHookNameInDev = 'useCacheRefresh';
|
||||
@@ -3101,6 +3122,7 @@ if (__DEV__) {
|
||||
unstable_isNewReconciler: enableNewReconciler,
|
||||
};
|
||||
if (enableCache) {
|
||||
(HooksDispatcherOnRerenderInDEV: Dispatcher).getCacheSignal = getCacheSignal;
|
||||
(HooksDispatcherOnRerenderInDEV: Dispatcher).getCacheForType = getCacheForType;
|
||||
(HooksDispatcherOnRerenderInDEV: Dispatcher).useCacheRefresh = function useCacheRefresh() {
|
||||
currentHookNameInDev = 'useCacheRefresh';
|
||||
@@ -3259,6 +3281,7 @@ if (__DEV__) {
|
||||
unstable_isNewReconciler: enableNewReconciler,
|
||||
};
|
||||
if (enableCache) {
|
||||
(InvalidNestedHooksDispatcherOnMountInDEV: Dispatcher).getCacheSignal = getCacheSignal;
|
||||
(InvalidNestedHooksDispatcherOnMountInDEV: Dispatcher).getCacheForType = getCacheForType;
|
||||
(InvalidNestedHooksDispatcherOnMountInDEV: Dispatcher).useCacheRefresh = function useCacheRefresh() {
|
||||
currentHookNameInDev = 'useCacheRefresh';
|
||||
@@ -3417,6 +3440,7 @@ if (__DEV__) {
|
||||
unstable_isNewReconciler: enableNewReconciler,
|
||||
};
|
||||
if (enableCache) {
|
||||
(InvalidNestedHooksDispatcherOnUpdateInDEV: Dispatcher).getCacheSignal = getCacheSignal;
|
||||
(InvalidNestedHooksDispatcherOnUpdateInDEV: Dispatcher).getCacheForType = getCacheForType;
|
||||
(InvalidNestedHooksDispatcherOnUpdateInDEV: Dispatcher).useCacheRefresh = function useCacheRefresh() {
|
||||
currentHookNameInDev = 'useCacheRefresh';
|
||||
@@ -3576,6 +3600,7 @@ if (__DEV__) {
|
||||
unstable_isNewReconciler: enableNewReconciler,
|
||||
};
|
||||
if (enableCache) {
|
||||
(InvalidNestedHooksDispatcherOnRerenderInDEV: Dispatcher).getCacheSignal = getCacheSignal;
|
||||
(InvalidNestedHooksDispatcherOnRerenderInDEV: Dispatcher).getCacheForType = getCacheForType;
|
||||
(InvalidNestedHooksDispatcherOnRerenderInDEV: Dispatcher).useCacheRefresh = function useCacheRefresh() {
|
||||
currentHookNameInDev = 'useCacheRefresh';
|
||||
|
||||
@@ -17,7 +17,6 @@ export type Lane = number;
|
||||
export type LaneMap<T> = Array<T>;
|
||||
|
||||
import {
|
||||
enableCache,
|
||||
enableSchedulingProfiler,
|
||||
enableUpdaterTracking,
|
||||
allowConcurrentByDefault,
|
||||
@@ -635,15 +634,6 @@ export function markRootFinished(root: FiberRoot, remainingLanes: Lanes) {
|
||||
|
||||
root.entangledLanes &= remainingLanes;
|
||||
|
||||
if (enableCache) {
|
||||
const pooledCacheLanes = (root.pooledCacheLanes &= remainingLanes);
|
||||
if (pooledCacheLanes === NoLanes) {
|
||||
// None of the remaining work relies on the cache pool. Clear it so
|
||||
// subsequent requests get a new cache.
|
||||
root.pooledCache = null;
|
||||
}
|
||||
}
|
||||
|
||||
const entanglements = root.entanglements;
|
||||
const eventTimes = root.eventTimes;
|
||||
const expirationTimes = root.expirationTimes;
|
||||
|
||||
@@ -17,7 +17,6 @@ export type Lane = number;
|
||||
export type LaneMap<T> = Array<T>;
|
||||
|
||||
import {
|
||||
enableCache,
|
||||
enableSchedulingProfiler,
|
||||
enableUpdaterTracking,
|
||||
allowConcurrentByDefault,
|
||||
@@ -635,15 +634,6 @@ export function markRootFinished(root: FiberRoot, remainingLanes: Lanes) {
|
||||
|
||||
root.entangledLanes &= remainingLanes;
|
||||
|
||||
if (enableCache) {
|
||||
const pooledCacheLanes = (root.pooledCacheLanes &= remainingLanes);
|
||||
if (pooledCacheLanes === NoLanes) {
|
||||
// None of the remaining work relies on the cache pool. Clear it so
|
||||
// subsequent requests get a new cache.
|
||||
root.pooledCache = null;
|
||||
}
|
||||
}
|
||||
|
||||
const entanglements = root.entanglements;
|
||||
const eventTimes = root.eventTimes;
|
||||
const expirationTimes = root.expirationTimes;
|
||||
|
||||
@@ -28,6 +28,7 @@ import {
|
||||
} from 'shared/ReactFeatureFlags';
|
||||
import {initializeUpdateQueue} from './ReactUpdateQueue.new';
|
||||
import {LegacyRoot, ConcurrentRoot} from './ReactRootTags';
|
||||
import {createCache, retainCache} from './ReactFiberCacheComponent.new';
|
||||
|
||||
function FiberRootNode(containerInfo, tag, hydrate) {
|
||||
this.tag = tag;
|
||||
@@ -117,8 +118,18 @@ export function createFiberRoot(
|
||||
uninitializedFiber.stateNode = root;
|
||||
|
||||
if (enableCache) {
|
||||
const initialCache = new Map();
|
||||
const initialCache = createCache();
|
||||
retainCache(initialCache);
|
||||
|
||||
// The pooledCache is a fresh cache instance that is used temporarily
|
||||
// for newly mounted boundaries during a render. In general, the
|
||||
// pooledCache is always cleared from the root at the end of a render:
|
||||
// it is either released when render commits, or moved to an Offscreen
|
||||
// component if rendering suspends. Because the lifetime of the pooled
|
||||
// cache is distinct from the main memoizedState.cache, it must be
|
||||
// retained separately.
|
||||
root.pooledCache = initialCache;
|
||||
retainCache(initialCache);
|
||||
const initialState = {
|
||||
element: null,
|
||||
cache: initialCache,
|
||||
|
||||
@@ -28,6 +28,7 @@ import {
|
||||
} from 'shared/ReactFeatureFlags';
|
||||
import {initializeUpdateQueue} from './ReactUpdateQueue.old';
|
||||
import {LegacyRoot, ConcurrentRoot} from './ReactRootTags';
|
||||
import {createCache, retainCache} from './ReactFiberCacheComponent.old';
|
||||
|
||||
function FiberRootNode(containerInfo, tag, hydrate) {
|
||||
this.tag = tag;
|
||||
@@ -117,8 +118,18 @@ export function createFiberRoot(
|
||||
uninitializedFiber.stateNode = root;
|
||||
|
||||
if (enableCache) {
|
||||
const initialCache = new Map();
|
||||
const initialCache = createCache();
|
||||
retainCache(initialCache);
|
||||
|
||||
// The pooledCache is a fresh cache instance that is used temporarily
|
||||
// for newly mounted boundaries during a render. In general, the
|
||||
// pooledCache is always cleared from the root at the end of a render:
|
||||
// it is either released when render commits, or moved to an Offscreen
|
||||
// component if rendering suspends. Because the lifetime of the pooled
|
||||
// cache is distinct from the main memoizedState.cache, it must be
|
||||
// retained separately.
|
||||
root.pooledCache = initialCache;
|
||||
retainCache(initialCache);
|
||||
const initialState = {
|
||||
element: null,
|
||||
cache: initialCache,
|
||||
|
||||
@@ -32,6 +32,7 @@ import {
|
||||
skipUnmountedBoundaries,
|
||||
enableUpdaterTracking,
|
||||
warnOnSubscriptionInsideStartTransition,
|
||||
enableCache,
|
||||
} from 'shared/ReactFeatureFlags';
|
||||
import ReactSharedInternals from 'shared/ReactSharedInternals';
|
||||
import is from 'shared/objectIs';
|
||||
@@ -234,6 +235,7 @@ import {
|
||||
isDevToolsPresent,
|
||||
} from './ReactFiberDevToolsHook.new';
|
||||
import {onCommitRoot as onCommitRootTestSelector} from './ReactTestSelectors';
|
||||
import {releaseCache} from './ReactFiberCacheComponent.new';
|
||||
|
||||
const ceil = Math.ceil;
|
||||
|
||||
@@ -331,6 +333,7 @@ let rootDoesHavePassiveEffects: boolean = false;
|
||||
let rootWithPendingPassiveEffects: FiberRoot | null = null;
|
||||
let pendingPassiveEffectsLanes: Lanes = NoLanes;
|
||||
let pendingPassiveProfilerEffects: Array<Fiber> = [];
|
||||
let pendingPassiveEffectsRemainingLanes: Lanes = NoLanes;
|
||||
|
||||
// Use these to prevent an infinite loop of nested updates
|
||||
const NESTED_UPDATE_LIMIT = 50;
|
||||
@@ -1900,8 +1903,12 @@ function commitRootImpl(root, renderPriorityLevel) {
|
||||
) {
|
||||
if (!rootDoesHavePassiveEffects) {
|
||||
rootDoesHavePassiveEffects = true;
|
||||
pendingPassiveEffectsRemainingLanes = remainingLanes;
|
||||
scheduleCallback(NormalSchedulerPriority, () => {
|
||||
flushPassiveEffects();
|
||||
// This render triggered passive effects: release the root cache pool
|
||||
// *after* passive effects fire to avoid freeing a cache pool that may
|
||||
// be referenced by a node in the tree (HostRoot, Cache boundary etc)
|
||||
return null;
|
||||
});
|
||||
}
|
||||
@@ -2027,6 +2034,10 @@ function commitRootImpl(root, renderPriorityLevel) {
|
||||
rootDoesHavePassiveEffects = false;
|
||||
rootWithPendingPassiveEffects = root;
|
||||
pendingPassiveEffectsLanes = lanes;
|
||||
} else {
|
||||
// There were no passive effects, so we can immediately release the cache
|
||||
// pool for this render.
|
||||
releaseRootPooledCache(root, remainingLanes);
|
||||
}
|
||||
|
||||
// Read this again, since an effect might have updated it
|
||||
@@ -2127,6 +2138,21 @@ function commitRootImpl(root, renderPriorityLevel) {
|
||||
return null;
|
||||
}
|
||||
|
||||
function releaseRootPooledCache(root: FiberRoot, remainingLanes: Lanes) {
|
||||
if (enableCache) {
|
||||
const pooledCacheLanes = (root.pooledCacheLanes &= remainingLanes);
|
||||
if (pooledCacheLanes === NoLanes) {
|
||||
// None of the remaining work relies on the cache pool. Clear it so
|
||||
// subsequent requests get a new cache
|
||||
const pooledCache = root.pooledCache;
|
||||
if (pooledCache != null) {
|
||||
root.pooledCache = null;
|
||||
releaseCache(pooledCache);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export function flushPassiveEffects(): boolean {
|
||||
// Returns whether passive effects were flushed.
|
||||
// TODO: Combine this check with the one in flushPassiveEFfectsImpl. We should
|
||||
@@ -2135,6 +2161,15 @@ export function flushPassiveEffects(): boolean {
|
||||
// `Scheduler.runWithPriority`, which accepts a function. But now we track the
|
||||
// priority within React itself, so we can mutate the variable directly.
|
||||
if (rootWithPendingPassiveEffects !== null) {
|
||||
// Cache the root since rootWithPendingPassiveEffects is cleared in
|
||||
// flushPassiveEffectsImpl
|
||||
const root = rootWithPendingPassiveEffects;
|
||||
// Cache and clear the remaining lanes flag; it must be reset since this
|
||||
// method can be called from various places, not always from commitRoot
|
||||
// where the remaining lanes are known
|
||||
const remainingLanes = pendingPassiveEffectsRemainingLanes;
|
||||
pendingPassiveEffectsRemainingLanes = NoLanes;
|
||||
|
||||
const renderPriority = lanesToEventPriority(pendingPassiveEffectsLanes);
|
||||
const priority = lowerEventPriority(DefaultEventPriority, renderPriority);
|
||||
const prevTransition = ReactCurrentBatchConfig.transition;
|
||||
@@ -2146,6 +2181,11 @@ export function flushPassiveEffects(): boolean {
|
||||
} finally {
|
||||
setCurrentUpdatePriority(previousPriority);
|
||||
ReactCurrentBatchConfig.transition = prevTransition;
|
||||
|
||||
// Once passive effects have run for the tree - giving components a
|
||||
// chance to retain cache instances they use - release the pooled
|
||||
// cache at the root (if there is one)
|
||||
releaseRootPooledCache(root, remainingLanes);
|
||||
}
|
||||
}
|
||||
return false;
|
||||
|
||||
@@ -32,6 +32,7 @@ import {
|
||||
skipUnmountedBoundaries,
|
||||
enableUpdaterTracking,
|
||||
warnOnSubscriptionInsideStartTransition,
|
||||
enableCache,
|
||||
} from 'shared/ReactFeatureFlags';
|
||||
import ReactSharedInternals from 'shared/ReactSharedInternals';
|
||||
import is from 'shared/objectIs';
|
||||
@@ -234,6 +235,7 @@ import {
|
||||
isDevToolsPresent,
|
||||
} from './ReactFiberDevToolsHook.old';
|
||||
import {onCommitRoot as onCommitRootTestSelector} from './ReactTestSelectors';
|
||||
import {releaseCache} from './ReactFiberCacheComponent.old';
|
||||
|
||||
const ceil = Math.ceil;
|
||||
|
||||
@@ -331,6 +333,7 @@ let rootDoesHavePassiveEffects: boolean = false;
|
||||
let rootWithPendingPassiveEffects: FiberRoot | null = null;
|
||||
let pendingPassiveEffectsLanes: Lanes = NoLanes;
|
||||
let pendingPassiveProfilerEffects: Array<Fiber> = [];
|
||||
let pendingPassiveEffectsRemainingLanes: Lanes = NoLanes;
|
||||
|
||||
// Use these to prevent an infinite loop of nested updates
|
||||
const NESTED_UPDATE_LIMIT = 50;
|
||||
@@ -1900,8 +1903,12 @@ function commitRootImpl(root, renderPriorityLevel) {
|
||||
) {
|
||||
if (!rootDoesHavePassiveEffects) {
|
||||
rootDoesHavePassiveEffects = true;
|
||||
pendingPassiveEffectsRemainingLanes = remainingLanes;
|
||||
scheduleCallback(NormalSchedulerPriority, () => {
|
||||
flushPassiveEffects();
|
||||
// This render triggered passive effects: release the root cache pool
|
||||
// *after* passive effects fire to avoid freeing a cache pool that may
|
||||
// be referenced by a node in the tree (HostRoot, Cache boundary etc)
|
||||
return null;
|
||||
});
|
||||
}
|
||||
@@ -2027,6 +2034,10 @@ function commitRootImpl(root, renderPriorityLevel) {
|
||||
rootDoesHavePassiveEffects = false;
|
||||
rootWithPendingPassiveEffects = root;
|
||||
pendingPassiveEffectsLanes = lanes;
|
||||
} else {
|
||||
// There were no passive effects, so we can immediately release the cache
|
||||
// pool for this render.
|
||||
releaseRootPooledCache(root, remainingLanes);
|
||||
}
|
||||
|
||||
// Read this again, since an effect might have updated it
|
||||
@@ -2127,6 +2138,21 @@ function commitRootImpl(root, renderPriorityLevel) {
|
||||
return null;
|
||||
}
|
||||
|
||||
function releaseRootPooledCache(root: FiberRoot, remainingLanes: Lanes) {
|
||||
if (enableCache) {
|
||||
const pooledCacheLanes = (root.pooledCacheLanes &= remainingLanes);
|
||||
if (pooledCacheLanes === NoLanes) {
|
||||
// None of the remaining work relies on the cache pool. Clear it so
|
||||
// subsequent requests get a new cache
|
||||
const pooledCache = root.pooledCache;
|
||||
if (pooledCache != null) {
|
||||
root.pooledCache = null;
|
||||
releaseCache(pooledCache);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export function flushPassiveEffects(): boolean {
|
||||
// Returns whether passive effects were flushed.
|
||||
// TODO: Combine this check with the one in flushPassiveEFfectsImpl. We should
|
||||
@@ -2135,6 +2161,15 @@ export function flushPassiveEffects(): boolean {
|
||||
// `Scheduler.runWithPriority`, which accepts a function. But now we track the
|
||||
// priority within React itself, so we can mutate the variable directly.
|
||||
if (rootWithPendingPassiveEffects !== null) {
|
||||
// Cache the root since rootWithPendingPassiveEffects is cleared in
|
||||
// flushPassiveEffectsImpl
|
||||
const root = rootWithPendingPassiveEffects;
|
||||
// Cache and clear the remaining lanes flag; it must be reset since this
|
||||
// method can be called from various places, not always from commitRoot
|
||||
// where the remaining lanes are known
|
||||
const remainingLanes = pendingPassiveEffectsRemainingLanes;
|
||||
pendingPassiveEffectsRemainingLanes = NoLanes;
|
||||
|
||||
const renderPriority = lanesToEventPriority(pendingPassiveEffectsLanes);
|
||||
const priority = lowerEventPriority(DefaultEventPriority, renderPriority);
|
||||
const prevTransition = ReactCurrentBatchConfig.transition;
|
||||
@@ -2146,6 +2181,11 @@ export function flushPassiveEffects(): boolean {
|
||||
} finally {
|
||||
setCurrentUpdatePriority(previousPriority);
|
||||
ReactCurrentBatchConfig.transition = prevTransition;
|
||||
|
||||
// Once passive effects have run for the tree - giving components a
|
||||
// chance to retain cache instances they use - release the pooled
|
||||
// cache at the root (if there is one)
|
||||
releaseRootPooledCache(root, remainingLanes);
|
||||
}
|
||||
}
|
||||
return false;
|
||||
|
||||
@@ -273,6 +273,7 @@ type BasicStateAction<S> = (S => S) | S;
|
||||
type Dispatch<A> = A => void;
|
||||
|
||||
export type Dispatcher = {|
|
||||
getCacheSignal?: () => AbortSignal,
|
||||
getCacheForType?: <T>(resourceType: () => T) => T,
|
||||
readContext<T>(context: ReactContext<T>): T,
|
||||
useState<S>(initialState: (() => S) | S): [S, Dispatch<BasicStateAction<S>>],
|
||||
|
||||
+638
-13
@@ -1,6 +1,7 @@
|
||||
let React;
|
||||
let ReactNoop;
|
||||
let Cache;
|
||||
let getCacheSignal;
|
||||
let getCacheForType;
|
||||
let Scheduler;
|
||||
let act;
|
||||
@@ -22,6 +23,7 @@ describe('ReactCache', () => {
|
||||
Scheduler = require('scheduler');
|
||||
act = require('jest-react').act;
|
||||
Suspense = React.Suspense;
|
||||
getCacheSignal = React.unstable_getCacheSignal;
|
||||
getCacheForType = React.unstable_getCacheForType;
|
||||
useCacheRefresh = React.unstable_useCacheRefresh;
|
||||
startTransition = React.startTransition;
|
||||
@@ -52,6 +54,7 @@ describe('ReactCache', () => {
|
||||
const newRecord = {
|
||||
status: 'resolved',
|
||||
value: text,
|
||||
cleanupScheduled: false,
|
||||
};
|
||||
data.set(text, newRecord);
|
||||
} else if (record.status === 'pending') {
|
||||
@@ -64,6 +67,7 @@ describe('ReactCache', () => {
|
||||
const newRecord = {
|
||||
status: 'rejected',
|
||||
value: error,
|
||||
cleanupScheduled: false,
|
||||
};
|
||||
data.set(text, newRecord);
|
||||
} else if (record.status === 'pending') {
|
||||
@@ -76,9 +80,21 @@ describe('ReactCache', () => {
|
||||
}
|
||||
|
||||
function readText(text) {
|
||||
const signal = getCacheSignal();
|
||||
const textCache = getCacheForType(createTextCache);
|
||||
const record = textCache.data.get(text);
|
||||
if (record !== undefined) {
|
||||
if (!record.cleanupScheduled) {
|
||||
// This record was seeded prior to the abort signal being available:
|
||||
// schedule a cleanup function for it.
|
||||
// TODO: Add ability to cleanup entries seeded w useCacheRefresh()
|
||||
record.cleanupScheduled = true;
|
||||
signal.addEventListener('abort', () => {
|
||||
Scheduler.unstable_yieldValue(
|
||||
`Cache cleanup: ${text} [v${textCache.version}]`,
|
||||
);
|
||||
});
|
||||
}
|
||||
switch (record.status) {
|
||||
case 'pending':
|
||||
throw record.value;
|
||||
@@ -115,9 +131,15 @@ describe('ReactCache', () => {
|
||||
const newRecord = {
|
||||
status: 'pending',
|
||||
value: thenable,
|
||||
cleanupScheduled: true,
|
||||
};
|
||||
textCache.data.set(text, newRecord);
|
||||
|
||||
signal.addEventListener('abort', () => {
|
||||
Scheduler.unstable_yieldValue(
|
||||
`Cache cleanup: ${text} [v${textCache.version}]`,
|
||||
);
|
||||
});
|
||||
throw thenable;
|
||||
}
|
||||
}
|
||||
@@ -180,6 +202,13 @@ describe('ReactCache', () => {
|
||||
});
|
||||
expect(Scheduler).toHaveYielded(['A']);
|
||||
expect(root).toMatchRenderedOutput('A');
|
||||
|
||||
await act(async () => {
|
||||
root.render('Bye');
|
||||
});
|
||||
// no cleanup: cache is still retained at the root
|
||||
expect(Scheduler).toHaveYielded([]);
|
||||
expect(root).toMatchRenderedOutput('Bye');
|
||||
});
|
||||
|
||||
// @gate experimental || www
|
||||
@@ -200,12 +229,19 @@ describe('ReactCache', () => {
|
||||
});
|
||||
expect(Scheduler).toHaveYielded(['A']);
|
||||
expect(root).toMatchRenderedOutput('A');
|
||||
|
||||
await act(async () => {
|
||||
root.render('Bye');
|
||||
});
|
||||
// no cleanup: cache is still retained at the root
|
||||
expect(Scheduler).toHaveYielded([]);
|
||||
expect(root).toMatchRenderedOutput('Bye');
|
||||
});
|
||||
|
||||
// @gate experimental || www
|
||||
test('multiple new Cache boundaries in the same update share the same, fresh cache', async () => {
|
||||
function App({text}) {
|
||||
return (
|
||||
function App({showMore}) {
|
||||
return showMore ? (
|
||||
<>
|
||||
<Cache>
|
||||
<Suspense fallback={<Text text="Loading..." />}>
|
||||
@@ -218,6 +254,8 @@ describe('ReactCache', () => {
|
||||
</Suspense>
|
||||
</Cache>
|
||||
</>
|
||||
) : (
|
||||
'(empty)'
|
||||
);
|
||||
}
|
||||
|
||||
@@ -225,6 +263,12 @@ describe('ReactCache', () => {
|
||||
await act(async () => {
|
||||
root.render(<App showMore={false} />);
|
||||
});
|
||||
expect(Scheduler).toHaveYielded([]);
|
||||
expect(root).toMatchRenderedOutput('(empty)');
|
||||
|
||||
await act(async () => {
|
||||
root.render(<App showMore={true} />);
|
||||
});
|
||||
// Even though there are two new <Cache /> trees, they should share the same
|
||||
// data cache. So there should be only a single cache miss for A.
|
||||
expect(Scheduler).toHaveYielded([
|
||||
@@ -239,6 +283,15 @@ describe('ReactCache', () => {
|
||||
});
|
||||
expect(Scheduler).toHaveYielded(['A', 'A']);
|
||||
expect(root).toMatchRenderedOutput('AA');
|
||||
|
||||
await act(async () => {
|
||||
root.render('Bye');
|
||||
});
|
||||
// cleanup occurs for the cache shared by the inner cache boundaries (which
|
||||
// are not shared w the root because they were added in an update)
|
||||
// note that no cache is created for the root since the cache is never accessed
|
||||
expect(Scheduler).toHaveYielded(['Cache cleanup: A [v1]']);
|
||||
expect(root).toMatchRenderedOutput('Bye');
|
||||
});
|
||||
|
||||
// @gate experimental || www
|
||||
@@ -261,8 +314,8 @@ describe('ReactCache', () => {
|
||||
await act(async () => {
|
||||
root.render(<App />);
|
||||
});
|
||||
// Even though there are two new <Cache /> trees, they should share the same
|
||||
// data cache. So there should be only a single cache miss for A.
|
||||
// Even though there is a nested <Cache /> boundary, it should share the same
|
||||
// data cache as the root. So there should be only a single cache miss for A.
|
||||
expect(Scheduler).toHaveYielded(['Cache miss! [A]', 'Loading...']);
|
||||
expect(root).toMatchRenderedOutput('Loading...');
|
||||
|
||||
@@ -271,6 +324,13 @@ describe('ReactCache', () => {
|
||||
});
|
||||
expect(Scheduler).toHaveYielded(['A', 'A']);
|
||||
expect(root).toMatchRenderedOutput('AA');
|
||||
|
||||
await act(async () => {
|
||||
root.render('Bye');
|
||||
});
|
||||
// no cleanup: cache is still retained at the root
|
||||
expect(Scheduler).toHaveYielded([]);
|
||||
expect(root).toMatchRenderedOutput('Bye');
|
||||
},
|
||||
);
|
||||
|
||||
@@ -309,6 +369,13 @@ describe('ReactCache', () => {
|
||||
'A [v1]',
|
||||
]);
|
||||
expect(root).toMatchRenderedOutput('A [v1]A [v1]');
|
||||
|
||||
await act(async () => {
|
||||
root.render('Bye');
|
||||
});
|
||||
// no cleanup: cache is still retained at the root
|
||||
expect(Scheduler).toHaveYielded([]);
|
||||
expect(root).toMatchRenderedOutput('Bye');
|
||||
});
|
||||
|
||||
// @gate experimental || www
|
||||
@@ -356,10 +423,21 @@ describe('ReactCache', () => {
|
||||
});
|
||||
expect(Scheduler).toHaveYielded(['A [v2]']);
|
||||
expect(root).toMatchRenderedOutput('A [v1]A [v2]');
|
||||
|
||||
// Replace all the children: this should retain the root Cache instance,
|
||||
// but cleanup the separate cache instance created for the fresh cache
|
||||
// boundary
|
||||
await act(async () => {
|
||||
root.render('Bye!');
|
||||
});
|
||||
// Cleanup occurs for the *second* cache instance: the first is still
|
||||
// referenced by the root
|
||||
expect(Scheduler).toHaveYielded(['Cache cleanup: A [v2]']);
|
||||
expect(root).toMatchRenderedOutput('Bye!');
|
||||
});
|
||||
|
||||
// @gate experimental || www
|
||||
test('inner content uses same cache as shell if spawned by the same transition', async () => {
|
||||
test('inner/outer cache boundaries uses the same cache instance on initial render', async () => {
|
||||
const root = ReactNoop.createRoot();
|
||||
|
||||
function App() {
|
||||
@@ -431,10 +509,109 @@ describe('ReactCache', () => {
|
||||
<div>Content</div>
|
||||
</>,
|
||||
);
|
||||
|
||||
await act(async () => {
|
||||
root.render('Bye');
|
||||
});
|
||||
// no cleanup: cache is still retained at the root
|
||||
expect(Scheduler).toHaveYielded([]);
|
||||
expect(root).toMatchRenderedOutput('Bye');
|
||||
});
|
||||
|
||||
// @gate experimental || www
|
||||
test('refresh a cache', async () => {
|
||||
test('inner/ outer cache boundaries added in the same update use the same cache instance', async () => {
|
||||
const root = ReactNoop.createRoot();
|
||||
|
||||
function App({showMore}) {
|
||||
return showMore ? (
|
||||
<Cache>
|
||||
<Suspense fallback={<Text text="Loading shell..." />}>
|
||||
{/* The shell reads A */}
|
||||
<Shell>
|
||||
{/* The inner content reads both A and B */}
|
||||
<Suspense fallback={<Text text="Loading content..." />}>
|
||||
<Cache>
|
||||
<Content />
|
||||
</Cache>
|
||||
</Suspense>
|
||||
</Shell>
|
||||
</Suspense>
|
||||
</Cache>
|
||||
) : (
|
||||
'(empty)'
|
||||
);
|
||||
}
|
||||
|
||||
function Shell({children}) {
|
||||
readText('A');
|
||||
return (
|
||||
<>
|
||||
<div>
|
||||
<Text text="Shell" />
|
||||
</div>
|
||||
<div>{children}</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
function Content() {
|
||||
readText('A');
|
||||
readText('B');
|
||||
return <Text text="Content" />;
|
||||
}
|
||||
|
||||
await act(async () => {
|
||||
root.render(<App showMore={false} />);
|
||||
});
|
||||
expect(Scheduler).toHaveYielded([]);
|
||||
expect(root).toMatchRenderedOutput('(empty)');
|
||||
|
||||
await act(async () => {
|
||||
root.render(<App showMore={true} />);
|
||||
});
|
||||
expect(Scheduler).toHaveYielded(['Cache miss! [A]', 'Loading shell...']);
|
||||
expect(root).toMatchRenderedOutput('Loading shell...');
|
||||
|
||||
await act(async () => {
|
||||
resolveMostRecentTextCache('A');
|
||||
});
|
||||
expect(Scheduler).toHaveYielded([
|
||||
'Shell',
|
||||
// There's a cache miss for B, because it hasn't been read yet. But not
|
||||
// A, because it was cached when we rendered the shell.
|
||||
'Cache miss! [B]',
|
||||
'Loading content...',
|
||||
]);
|
||||
expect(root).toMatchRenderedOutput(
|
||||
<>
|
||||
<div>Shell</div>
|
||||
<div>Loading content...</div>
|
||||
</>,
|
||||
);
|
||||
|
||||
await act(async () => {
|
||||
resolveMostRecentTextCache('B');
|
||||
});
|
||||
expect(Scheduler).toHaveYielded(['Content']);
|
||||
expect(root).toMatchRenderedOutput(
|
||||
<>
|
||||
<div>Shell</div>
|
||||
<div>Content</div>
|
||||
</>,
|
||||
);
|
||||
|
||||
await act(async () => {
|
||||
root.render('Bye');
|
||||
});
|
||||
expect(Scheduler).toHaveYielded([
|
||||
'Cache cleanup: A [v1]',
|
||||
'Cache cleanup: B [v1]',
|
||||
]);
|
||||
expect(root).toMatchRenderedOutput('Bye');
|
||||
});
|
||||
|
||||
// @gate experimental || www
|
||||
test('refresh a cache boundary', async () => {
|
||||
let refresh;
|
||||
function App() {
|
||||
refresh = useCacheRefresh();
|
||||
@@ -474,6 +651,14 @@ describe('ReactCache', () => {
|
||||
// Note that the version has updated
|
||||
expect(Scheduler).toHaveYielded(['A [v2]']);
|
||||
expect(root).toMatchRenderedOutput('A [v2]');
|
||||
|
||||
await act(async () => {
|
||||
root.render('Bye');
|
||||
});
|
||||
// the original cache instance does not cleanup since it is still referenced
|
||||
// by the root, but the refreshed inner cache does cleanup
|
||||
expect(Scheduler).toHaveYielded(['Cache cleanup: A [v2]']);
|
||||
expect(root).toMatchRenderedOutput('Bye');
|
||||
});
|
||||
|
||||
// @gate experimental || www
|
||||
@@ -512,9 +697,64 @@ describe('ReactCache', () => {
|
||||
await act(async () => {
|
||||
resolveMostRecentTextCache('A');
|
||||
});
|
||||
// Note that the version has updated
|
||||
expect(Scheduler).toHaveYielded(['A [v2]']);
|
||||
// Note that the version has updated, and the previous cache is cleared
|
||||
expect(Scheduler).toHaveYielded(['A [v2]', 'Cache cleanup: A [v1]']);
|
||||
expect(root).toMatchRenderedOutput('A [v2]');
|
||||
|
||||
await act(async () => {
|
||||
root.render('Bye');
|
||||
});
|
||||
// the original root cache already cleaned up when the refresh completed
|
||||
expect(Scheduler).toHaveYielded([]);
|
||||
expect(root).toMatchRenderedOutput('Bye');
|
||||
});
|
||||
|
||||
// @gate experimental || www
|
||||
test('refresh the root cache without a transition', async () => {
|
||||
let refresh;
|
||||
function App() {
|
||||
refresh = useCacheRefresh();
|
||||
return <AsyncText showVersion={true} text="A" />;
|
||||
}
|
||||
|
||||
// Mount initial data
|
||||
const root = ReactNoop.createRoot();
|
||||
await act(async () => {
|
||||
root.render(
|
||||
<Suspense fallback={<Text text="Loading..." />}>
|
||||
<App />
|
||||
</Suspense>,
|
||||
);
|
||||
});
|
||||
expect(Scheduler).toHaveYielded(['Cache miss! [A]', 'Loading...']);
|
||||
expect(root).toMatchRenderedOutput('Loading...');
|
||||
|
||||
await act(async () => {
|
||||
resolveMostRecentTextCache('A');
|
||||
});
|
||||
expect(Scheduler).toHaveYielded(['A [v1]']);
|
||||
expect(root).toMatchRenderedOutput('A [v1]');
|
||||
|
||||
// Refresh for new data.
|
||||
await act(async () => {
|
||||
refresh();
|
||||
});
|
||||
expect(Scheduler).toHaveYielded(['Cache miss! [A]', 'Loading...']);
|
||||
expect(root).toMatchRenderedOutput('Loading...');
|
||||
|
||||
await act(async () => {
|
||||
resolveMostRecentTextCache('A');
|
||||
});
|
||||
// Note that the version has updated, and the previous cache is cleared
|
||||
expect(Scheduler).toHaveYielded(['A [v2]', 'Cache cleanup: A [v1]']);
|
||||
expect(root).toMatchRenderedOutput('A [v2]');
|
||||
|
||||
await act(async () => {
|
||||
root.render('Bye');
|
||||
});
|
||||
// the original root cache already cleaned up when the refresh completed
|
||||
expect(Scheduler).toHaveYielded([]);
|
||||
expect(root).toMatchRenderedOutput('Bye');
|
||||
});
|
||||
|
||||
// @gate experimental || www
|
||||
@@ -556,8 +796,16 @@ describe('ReactCache', () => {
|
||||
startTransition(() => refresh(createTextCache, cache));
|
||||
});
|
||||
// The root should re-render without a cache miss.
|
||||
// The cache is not cleared up yet, since it's still reference by the root
|
||||
expect(Scheduler).toHaveYielded(['A [v2]']);
|
||||
expect(root).toMatchRenderedOutput('A [v2]');
|
||||
|
||||
await act(async () => {
|
||||
root.render('Bye');
|
||||
});
|
||||
// the refreshed cache boundary is unmounted and cleans up
|
||||
expect(Scheduler).toHaveYielded(['Cache cleanup: A [v2]']);
|
||||
expect(root).toMatchRenderedOutput('Bye');
|
||||
});
|
||||
|
||||
// @gate experimental || www
|
||||
@@ -621,8 +869,22 @@ describe('ReactCache', () => {
|
||||
await act(async () => {
|
||||
resolveMostRecentTextCache('A');
|
||||
});
|
||||
expect(Scheduler).toHaveYielded(['A [v3]', 'A [v3]']);
|
||||
expect(Scheduler).toHaveYielded([
|
||||
'A [v3]',
|
||||
'A [v3]',
|
||||
// once the refresh completes the inner showMore boundary frees its previous
|
||||
// cache instance, since it is now using the refreshed parent instance.
|
||||
'Cache cleanup: A [v2]',
|
||||
]);
|
||||
expect(root).toMatchRenderedOutput('A [v3]A [v3]');
|
||||
|
||||
await act(async () => {
|
||||
root.render('Bye!');
|
||||
});
|
||||
// Unmounting children releases the refreshed cache instance only; the root
|
||||
// still retains the original cache instance used for the first render
|
||||
expect(Scheduler).toHaveYielded(['Cache cleanup: A [v3]']);
|
||||
expect(root).toMatchRenderedOutput('Bye!');
|
||||
});
|
||||
|
||||
// @gate experimental || www
|
||||
@@ -695,6 +957,21 @@ describe('ReactCache', () => {
|
||||
});
|
||||
expect(Scheduler).toHaveYielded(['A [v2]']);
|
||||
expect(root).toMatchRenderedOutput('A [v2]A [v1]');
|
||||
|
||||
// Unmount children: this should clear *both* cache instances:
|
||||
// the root doesn't have a cache instance (since it wasn't accessed
|
||||
// during the initial render, and all subsequent cache accesses were within
|
||||
// a fresh boundary). Therefore this causes cleanup for both the fresh cache
|
||||
// instance in the refreshed first boundary and cleanup for the non-refreshed
|
||||
// sibling boundary.
|
||||
await act(async () => {
|
||||
root.render('Bye!');
|
||||
});
|
||||
expect(Scheduler).toHaveYielded([
|
||||
'Cache cleanup: A [v2]',
|
||||
'Cache cleanup: A [v1]',
|
||||
]);
|
||||
expect(root).toMatchRenderedOutput('Bye!');
|
||||
},
|
||||
);
|
||||
|
||||
@@ -733,6 +1010,7 @@ describe('ReactCache', () => {
|
||||
'Cache miss! [B]',
|
||||
'Loading...',
|
||||
]);
|
||||
expect(root).toMatchRenderedOutput('Loading...');
|
||||
|
||||
await act(async () => {
|
||||
// This will resolve the content in the first cache
|
||||
@@ -750,6 +1028,7 @@ describe('ReactCache', () => {
|
||||
'A [v1]',
|
||||
'B [v1]',
|
||||
]);
|
||||
expect(root).toMatchRenderedOutput('Loading... A [v1] B [v1]');
|
||||
|
||||
// Now resolve the second tree
|
||||
await act(async () => {
|
||||
@@ -757,6 +1036,15 @@ describe('ReactCache', () => {
|
||||
});
|
||||
expect(Scheduler).toHaveYielded(['A [v2]']);
|
||||
expect(root).toMatchRenderedOutput('A [v2] A [v1] B [v1]');
|
||||
|
||||
await act(async () => {
|
||||
root.render('Bye!');
|
||||
});
|
||||
// Unmounting children releases both cache boundaries, but the original
|
||||
// cache instance (used by second boundary) is still referenced by the root.
|
||||
// only the second cache instance is freed.
|
||||
expect(Scheduler).toHaveYielded(['Cache cleanup: A [v2]']);
|
||||
expect(root).toMatchRenderedOutput('Bye!');
|
||||
},
|
||||
);
|
||||
|
||||
@@ -841,6 +1129,19 @@ describe('ReactCache', () => {
|
||||
});
|
||||
expect(Scheduler).toHaveYielded(['A [v1]', 'A [v1]', 'A [v2]']);
|
||||
expect(root).toMatchRenderedOutput('A [v1]A [v1]A [v2]');
|
||||
|
||||
// Unmount children: the first text cache instance is created only after the root
|
||||
// commits, so both fresh cache instances are released by their cache boundaries,
|
||||
// cleaning up v1 (used for the first two children which render togeether) and
|
||||
// v2 (used for the third boundary added later).
|
||||
await act(async () => {
|
||||
root.render('Bye!');
|
||||
});
|
||||
expect(Scheduler).toHaveYielded([
|
||||
'Cache cleanup: A [v1]',
|
||||
'Cache cleanup: A [v2]',
|
||||
]);
|
||||
expect(root).toMatchRenderedOutput('Bye!');
|
||||
});
|
||||
|
||||
// @gate experimental || www
|
||||
@@ -863,7 +1164,7 @@ describe('ReactCache', () => {
|
||||
<Suspense fallback={<Text text="Loading..." />}>
|
||||
{shouldShow ? (
|
||||
<Cache>
|
||||
<AsyncText text="A" />
|
||||
<AsyncText showVersion={true} text="A" />
|
||||
</Cache>
|
||||
) : null}
|
||||
</Suspense>
|
||||
@@ -880,7 +1181,7 @@ describe('ReactCache', () => {
|
||||
|
||||
const root = ReactNoop.createRoot();
|
||||
await act(async () => {
|
||||
root.render(<App showMore={false} />);
|
||||
root.render(<App />);
|
||||
});
|
||||
expect(Scheduler).toHaveYielded(['0']);
|
||||
expect(root).toMatchRenderedOutput('0');
|
||||
@@ -908,7 +1209,331 @@ describe('ReactCache', () => {
|
||||
await act(async () => {
|
||||
resolveMostRecentTextCache('A');
|
||||
});
|
||||
expect(Scheduler).toHaveYielded(['A']);
|
||||
expect(root).toMatchRenderedOutput('A1');
|
||||
expect(Scheduler).toHaveYielded(['A [v1]']);
|
||||
expect(root).toMatchRenderedOutput('A [v1]1');
|
||||
|
||||
// Unmount children: the first text cache instance is created only after initial
|
||||
// render after calling showMore(). This instance is cleaned up when that boundary
|
||||
// is unmounted. Bc root cache instance is never accessed, the inner cache
|
||||
// boundary ends up at v1.
|
||||
await act(async () => {
|
||||
root.render('Bye!');
|
||||
});
|
||||
expect(Scheduler).toHaveYielded(['Cache cleanup: A [v1]']);
|
||||
expect(root).toMatchRenderedOutput('Bye!');
|
||||
});
|
||||
|
||||
// @gate experimental || www
|
||||
test('cache boundary uses a fresh cache when its key changes', async () => {
|
||||
const root = ReactNoop.createRoot();
|
||||
seedNextTextCache('A');
|
||||
await act(async () => {
|
||||
root.render(
|
||||
<Suspense fallback="Loading...">
|
||||
<Cache key="A">
|
||||
<AsyncText showVersion={true} text="A" />
|
||||
</Cache>
|
||||
</Suspense>,
|
||||
);
|
||||
});
|
||||
expect(Scheduler).toHaveYielded(['A [v1]']);
|
||||
expect(root).toMatchRenderedOutput('A [v1]');
|
||||
|
||||
seedNextTextCache('B');
|
||||
await act(async () => {
|
||||
root.render(
|
||||
<Suspense fallback="Loading...">
|
||||
<Cache key="B">
|
||||
<AsyncText showVersion={true} text="B" />
|
||||
</Cache>
|
||||
</Suspense>,
|
||||
);
|
||||
});
|
||||
expect(Scheduler).toHaveYielded(['B [v2]']);
|
||||
expect(root).toMatchRenderedOutput('B [v2]');
|
||||
|
||||
// Unmount children: the fresh cache instance for B cleans up since the cache boundary
|
||||
// is the only owner, while the original cache instance (for A) is still retained by
|
||||
// the root.
|
||||
await act(async () => {
|
||||
root.render('Bye!');
|
||||
});
|
||||
expect(Scheduler).toHaveYielded(['Cache cleanup: B [v2]']);
|
||||
expect(root).toMatchRenderedOutput('Bye!');
|
||||
});
|
||||
|
||||
// @gate experimental || www
|
||||
test('overlapping transitions after an initial mount use the same fresh cache', async () => {
|
||||
const root = ReactNoop.createRoot();
|
||||
await act(async () => {
|
||||
root.render(
|
||||
<Suspense fallback="Loading...">
|
||||
<Cache key="A">
|
||||
<AsyncText showVersion={true} text="A" />
|
||||
</Cache>
|
||||
</Suspense>,
|
||||
);
|
||||
});
|
||||
expect(Scheduler).toHaveYielded(['Cache miss! [A]']);
|
||||
expect(root).toMatchRenderedOutput('Loading...');
|
||||
|
||||
await act(async () => {
|
||||
resolveMostRecentTextCache('A');
|
||||
});
|
||||
expect(Scheduler).toHaveYielded(['A [v1]']);
|
||||
expect(root).toMatchRenderedOutput('A [v1]');
|
||||
|
||||
// After a mount, subsequent transitions use a fresh cache
|
||||
await act(async () => {
|
||||
startTransition(() => {
|
||||
root.render(
|
||||
<Suspense fallback="Loading...">
|
||||
<Cache key="B">
|
||||
<AsyncText showVersion={true} text="B" />
|
||||
</Cache>
|
||||
</Suspense>,
|
||||
);
|
||||
});
|
||||
});
|
||||
expect(Scheduler).toHaveYielded(['Cache miss! [B]']);
|
||||
expect(root).toMatchRenderedOutput('A [v1]');
|
||||
|
||||
// Update to a different text and with a different key for the cache
|
||||
// boundary: this should still use the fresh cache instance created
|
||||
// for the earlier transition
|
||||
await act(async () => {
|
||||
startTransition(() => {
|
||||
root.render(
|
||||
<Suspense fallback="Loading...">
|
||||
<Cache key="C">
|
||||
<AsyncText showVersion={true} text="C" />
|
||||
</Cache>
|
||||
</Suspense>,
|
||||
);
|
||||
});
|
||||
});
|
||||
expect(Scheduler).toHaveYielded(['Cache miss! [C]']);
|
||||
expect(root).toMatchRenderedOutput('A [v1]');
|
||||
|
||||
await act(async () => {
|
||||
resolveMostRecentTextCache('C');
|
||||
});
|
||||
expect(Scheduler).toHaveYielded(['C [v2]']);
|
||||
expect(root).toMatchRenderedOutput('C [v2]');
|
||||
|
||||
// Unmount children: the fresh cache used for the updates is freed, while the
|
||||
// original cache (with A) is still retained at the root.
|
||||
await act(async () => {
|
||||
root.render('Bye!');
|
||||
});
|
||||
expect(Scheduler).toHaveYielded([
|
||||
'Cache cleanup: B [v2]',
|
||||
'Cache cleanup: C [v2]',
|
||||
]);
|
||||
expect(root).toMatchRenderedOutput('Bye!');
|
||||
});
|
||||
|
||||
// @gate experimental || www
|
||||
test('overlapping updates after an initial mount use the same fresh cache', async () => {
|
||||
const root = ReactNoop.createRoot();
|
||||
await act(async () => {
|
||||
root.render(
|
||||
<Suspense fallback="Loading...">
|
||||
<Cache key="A">
|
||||
<AsyncText showVersion={true} text="A" />
|
||||
</Cache>
|
||||
</Suspense>,
|
||||
);
|
||||
});
|
||||
expect(Scheduler).toHaveYielded(['Cache miss! [A]']);
|
||||
expect(root).toMatchRenderedOutput('Loading...');
|
||||
|
||||
await act(async () => {
|
||||
resolveMostRecentTextCache('A');
|
||||
});
|
||||
expect(Scheduler).toHaveYielded(['A [v1]']);
|
||||
expect(root).toMatchRenderedOutput('A [v1]');
|
||||
|
||||
// After a mount, subsequent updates use a fresh cache
|
||||
await act(async () => {
|
||||
root.render(
|
||||
<Suspense fallback="Loading...">
|
||||
<Cache key="B">
|
||||
<AsyncText showVersion={true} text="B" />
|
||||
</Cache>
|
||||
</Suspense>,
|
||||
);
|
||||
});
|
||||
expect(Scheduler).toHaveYielded(['Cache miss! [B]']);
|
||||
expect(root).toMatchRenderedOutput('Loading...');
|
||||
|
||||
// A second update uses the same fresh cache: even though this is a new
|
||||
// Cache boundary, the render uses the fresh cache from the pending update.
|
||||
await act(async () => {
|
||||
root.render(
|
||||
<Suspense fallback="Loading...">
|
||||
<Cache key="C">
|
||||
<AsyncText showVersion={true} text="C" />
|
||||
</Cache>
|
||||
</Suspense>,
|
||||
);
|
||||
});
|
||||
expect(Scheduler).toHaveYielded(['Cache miss! [C]']);
|
||||
expect(root).toMatchRenderedOutput('Loading...');
|
||||
|
||||
await act(async () => {
|
||||
resolveMostRecentTextCache('C');
|
||||
});
|
||||
expect(Scheduler).toHaveYielded(['C [v2]']);
|
||||
expect(root).toMatchRenderedOutput('C [v2]');
|
||||
|
||||
// Unmount children: the fresh cache used for the updates is freed, while the
|
||||
// original cache (with A) is still retained at the root.
|
||||
await act(async () => {
|
||||
root.render('Bye!');
|
||||
});
|
||||
expect(Scheduler).toHaveYielded([
|
||||
'Cache cleanup: B [v2]',
|
||||
'Cache cleanup: C [v2]',
|
||||
]);
|
||||
expect(root).toMatchRenderedOutput('Bye!');
|
||||
});
|
||||
|
||||
// @gate experimental || www
|
||||
test('cleans up cache only used in an aborted transition', async () => {
|
||||
const root = ReactNoop.createRoot();
|
||||
seedNextTextCache('A');
|
||||
await act(async () => {
|
||||
root.render(
|
||||
<Suspense fallback="Loading...">
|
||||
<Cache key="A">
|
||||
<AsyncText showVersion={true} text="A" />
|
||||
</Cache>
|
||||
</Suspense>,
|
||||
);
|
||||
});
|
||||
expect(Scheduler).toHaveYielded(['A [v1]']);
|
||||
expect(root).toMatchRenderedOutput('A [v1]');
|
||||
|
||||
// Start a transition from A -> B..., which should create a fresh cache
|
||||
// for the new cache boundary (bc of the different key)
|
||||
await act(async () => {
|
||||
startTransition(() => {
|
||||
root.render(
|
||||
<Suspense fallback="Loading...">
|
||||
<Cache key="B">
|
||||
<AsyncText showVersion={true} text="B" />
|
||||
</Cache>
|
||||
</Suspense>,
|
||||
);
|
||||
});
|
||||
});
|
||||
expect(Scheduler).toHaveYielded(['Cache miss! [B]']);
|
||||
expect(root).toMatchRenderedOutput('A [v1]');
|
||||
|
||||
// ...but cancel by transitioning "back" to A (which we never really left)
|
||||
await act(async () => {
|
||||
startTransition(() => {
|
||||
root.render(
|
||||
<Suspense fallback="Loading...">
|
||||
<Cache key="A">
|
||||
<AsyncText showVersion={true} text="A" />
|
||||
</Cache>
|
||||
</Suspense>,
|
||||
);
|
||||
});
|
||||
});
|
||||
expect(Scheduler).toHaveYielded(['A [v1]', 'Cache cleanup: B [v2]']);
|
||||
expect(root).toMatchRenderedOutput('A [v1]');
|
||||
|
||||
// Unmount children: ...
|
||||
await act(async () => {
|
||||
root.render('Bye!');
|
||||
});
|
||||
expect(Scheduler).toHaveYielded([]);
|
||||
expect(root).toMatchRenderedOutput('Bye!');
|
||||
});
|
||||
|
||||
// @gate experimental || www
|
||||
test.skip('if a root cache refresh never commits its fresh cache is released', async () => {
|
||||
const root = ReactNoop.createRoot();
|
||||
let refresh;
|
||||
function Example({text}) {
|
||||
refresh = useCacheRefresh();
|
||||
return <AsyncText showVersion={true} text={text} />;
|
||||
}
|
||||
seedNextTextCache('A');
|
||||
await act(async () => {
|
||||
root.render(
|
||||
<Suspense fallback="Loading...">
|
||||
<Example text="A" />
|
||||
</Suspense>,
|
||||
);
|
||||
});
|
||||
expect(Scheduler).toHaveYielded(['A [v1]']);
|
||||
expect(root).toMatchRenderedOutput('A [v1]');
|
||||
|
||||
await act(async () => {
|
||||
startTransition(() => {
|
||||
refresh();
|
||||
});
|
||||
});
|
||||
expect(Scheduler).toHaveYielded(['Cache miss! [A]']);
|
||||
expect(root).toMatchRenderedOutput('A [v1]');
|
||||
|
||||
await act(async () => {
|
||||
root.render('Bye!');
|
||||
});
|
||||
expect(Scheduler).toHaveYielded([
|
||||
// TODO: the v1 cache should *not* be cleaned up, it is still retained by the root
|
||||
// The following line is presently yielded but should not be:
|
||||
// 'Cache cleanup: A [v1]',
|
||||
|
||||
// TODO: the v2 cache *should* be cleaned up, it was created for the abandoned refresh
|
||||
// The following line is presently not yielded but should be:
|
||||
'Cache cleanup: A [v2]',
|
||||
]);
|
||||
expect(root).toMatchRenderedOutput('Bye!');
|
||||
});
|
||||
|
||||
// @gate experimental || www
|
||||
test.skip('if a cache boundary refresh never commits its fresh cache is released', async () => {
|
||||
const root = ReactNoop.createRoot();
|
||||
let refresh;
|
||||
function Example({text}) {
|
||||
refresh = useCacheRefresh();
|
||||
return <AsyncText showVersion={true} text={text} />;
|
||||
}
|
||||
seedNextTextCache('A');
|
||||
await act(async () => {
|
||||
root.render(
|
||||
<Suspense fallback="Loading...">
|
||||
<Cache>
|
||||
<Example text="A" />
|
||||
</Cache>
|
||||
</Suspense>,
|
||||
);
|
||||
});
|
||||
expect(Scheduler).toHaveYielded(['A [v1]']);
|
||||
expect(root).toMatchRenderedOutput('A [v1]');
|
||||
|
||||
await act(async () => {
|
||||
startTransition(() => {
|
||||
refresh();
|
||||
});
|
||||
});
|
||||
expect(Scheduler).toHaveYielded(['Cache miss! [A]']);
|
||||
expect(root).toMatchRenderedOutput('A [v1]');
|
||||
|
||||
// Unmount the boundary before the refresh can complete
|
||||
await act(async () => {
|
||||
root.render('Bye!');
|
||||
});
|
||||
expect(Scheduler).toHaveYielded([
|
||||
// TODO: the v2 cache *should* be cleaned up, it was created for the abandoned refresh
|
||||
// The following line is presently not yielded but should be:
|
||||
'Cache cleanup: A [v2]',
|
||||
]);
|
||||
expect(root).toMatchRenderedOutput('Bye!');
|
||||
});
|
||||
});
|
||||
|
||||
@@ -409,30 +409,30 @@ describe('SchedulingProfiler', () => {
|
||||
|
||||
if (gate(flags => flags.enableSchedulingProfiler)) {
|
||||
expect(getMarks()).toMatchInlineSnapshot(`
|
||||
Array [
|
||||
"--render-start-16",
|
||||
"--component-render-start-Example",
|
||||
"--component-render-stop",
|
||||
"--render-stop",
|
||||
"--commit-start-16",
|
||||
"--react-version-17.0.3",
|
||||
"--profiler-version-1",
|
||||
"--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",
|
||||
"--layout-effects-start-16",
|
||||
"--schedule-state-update-1-Example",
|
||||
"--layout-effects-stop",
|
||||
"--render-start-1",
|
||||
"--component-render-start-Example",
|
||||
"--component-render-stop",
|
||||
"--render-stop",
|
||||
"--commit-start-1",
|
||||
"--react-version-17.0.3",
|
||||
"--profiler-version-1",
|
||||
"--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",
|
||||
"--commit-stop",
|
||||
"--commit-stop",
|
||||
]
|
||||
`);
|
||||
Array [
|
||||
"--render-start-16",
|
||||
"--component-render-start-Example",
|
||||
"--component-render-stop",
|
||||
"--render-stop",
|
||||
"--commit-start-16",
|
||||
"--react-version-17.0.3",
|
||||
"--profiler-version-1",
|
||||
"--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",
|
||||
"--layout-effects-start-16",
|
||||
"--schedule-state-update-1-Example",
|
||||
"--layout-effects-stop",
|
||||
"--render-start-1",
|
||||
"--component-render-start-Example",
|
||||
"--component-render-stop",
|
||||
"--render-stop",
|
||||
"--commit-start-1",
|
||||
"--react-version-17.0.3",
|
||||
"--profiler-version-1",
|
||||
"--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",
|
||||
"--commit-stop",
|
||||
"--commit-stop",
|
||||
]
|
||||
`);
|
||||
}
|
||||
});
|
||||
|
||||
@@ -462,30 +462,30 @@ describe('SchedulingProfiler', () => {
|
||||
|
||||
if (gate(flags => flags.enableSchedulingProfiler)) {
|
||||
expect(getMarks()).toMatchInlineSnapshot(`
|
||||
Array [
|
||||
"--render-start-16",
|
||||
"--component-render-start-Example",
|
||||
"--component-render-stop",
|
||||
"--render-stop",
|
||||
"--commit-start-16",
|
||||
"--react-version-17.0.3",
|
||||
"--profiler-version-1",
|
||||
"--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",
|
||||
"--layout-effects-start-16",
|
||||
"--schedule-forced-update-1-Example",
|
||||
"--layout-effects-stop",
|
||||
"--render-start-1",
|
||||
"--component-render-start-Example",
|
||||
"--component-render-stop",
|
||||
"--render-stop",
|
||||
"--commit-start-1",
|
||||
"--react-version-17.0.3",
|
||||
"--profiler-version-1",
|
||||
"--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",
|
||||
"--commit-stop",
|
||||
"--commit-stop",
|
||||
]
|
||||
`);
|
||||
Array [
|
||||
"--render-start-16",
|
||||
"--component-render-start-Example",
|
||||
"--component-render-stop",
|
||||
"--render-stop",
|
||||
"--commit-start-16",
|
||||
"--react-version-17.0.3",
|
||||
"--profiler-version-1",
|
||||
"--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",
|
||||
"--layout-effects-start-16",
|
||||
"--schedule-forced-update-1-Example",
|
||||
"--layout-effects-stop",
|
||||
"--render-start-1",
|
||||
"--component-render-start-Example",
|
||||
"--component-render-stop",
|
||||
"--render-stop",
|
||||
"--commit-start-1",
|
||||
"--react-version-17.0.3",
|
||||
"--profiler-version-1",
|
||||
"--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",
|
||||
"--commit-stop",
|
||||
"--commit-stop",
|
||||
]
|
||||
`);
|
||||
}
|
||||
});
|
||||
|
||||
@@ -701,22 +701,22 @@ describe('SchedulingProfiler', () => {
|
||||
|
||||
if (gate(flags => flags.enableSchedulingProfiler)) {
|
||||
expect(getMarks()).toMatchInlineSnapshot(`
|
||||
Array [
|
||||
"--schedule-render-16",
|
||||
"--render-start-16",
|
||||
"--component-render-start-Example",
|
||||
"--schedule-state-update-16-Example",
|
||||
"--component-render-stop",
|
||||
"--render-stop",
|
||||
"--commit-start-16",
|
||||
"--react-version-17.0.3",
|
||||
"--profiler-version-1",
|
||||
"--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",
|
||||
"--layout-effects-start-16",
|
||||
"--layout-effects-stop",
|
||||
"--commit-stop",
|
||||
]
|
||||
`);
|
||||
Array [
|
||||
"--schedule-render-16",
|
||||
"--render-start-16",
|
||||
"--component-render-start-Example",
|
||||
"--schedule-state-update-16-Example",
|
||||
"--component-render-stop",
|
||||
"--render-stop",
|
||||
"--commit-start-16",
|
||||
"--react-version-17.0.3",
|
||||
"--profiler-version-1",
|
||||
"--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",
|
||||
"--layout-effects-start-16",
|
||||
"--layout-effects-stop",
|
||||
"--commit-stop",
|
||||
]
|
||||
`);
|
||||
}
|
||||
});
|
||||
|
||||
@@ -746,35 +746,35 @@ describe('SchedulingProfiler', () => {
|
||||
|
||||
if (gate(flags => flags.enableSchedulingProfiler)) {
|
||||
expect(getMarks()).toMatchInlineSnapshot(`
|
||||
Array [
|
||||
"--schedule-render-1",
|
||||
"--render-start-1",
|
||||
"--component-render-start-ErrorBoundary",
|
||||
"--component-render-stop",
|
||||
"--component-render-start-ExampleThatThrows",
|
||||
"--component-render-start-ExampleThatThrows",
|
||||
"--component-render-stop",
|
||||
"--error-ExampleThatThrows-mount-Expected error",
|
||||
"--render-stop",
|
||||
"--commit-start-1",
|
||||
"--react-version-17.0.3",
|
||||
"--profiler-version-1",
|
||||
"--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",
|
||||
"--layout-effects-start-1",
|
||||
"--schedule-state-update-1-ErrorBoundary",
|
||||
"--layout-effects-stop",
|
||||
"--commit-stop",
|
||||
"--render-start-1",
|
||||
"--component-render-start-ErrorBoundary",
|
||||
"--component-render-stop",
|
||||
"--render-stop",
|
||||
"--commit-start-1",
|
||||
"--react-version-17.0.3",
|
||||
"--profiler-version-1",
|
||||
"--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",
|
||||
"--commit-stop",
|
||||
]
|
||||
`);
|
||||
Array [
|
||||
"--schedule-render-1",
|
||||
"--render-start-1",
|
||||
"--component-render-start-ErrorBoundary",
|
||||
"--component-render-stop",
|
||||
"--component-render-start-ExampleThatThrows",
|
||||
"--component-render-start-ExampleThatThrows",
|
||||
"--component-render-stop",
|
||||
"--error-ExampleThatThrows-mount-Expected error",
|
||||
"--render-stop",
|
||||
"--commit-start-1",
|
||||
"--react-version-17.0.3",
|
||||
"--profiler-version-1",
|
||||
"--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",
|
||||
"--layout-effects-start-1",
|
||||
"--schedule-state-update-1-ErrorBoundary",
|
||||
"--layout-effects-stop",
|
||||
"--commit-stop",
|
||||
"--render-start-1",
|
||||
"--component-render-start-ErrorBoundary",
|
||||
"--component-render-stop",
|
||||
"--render-stop",
|
||||
"--commit-start-1",
|
||||
"--react-version-17.0.3",
|
||||
"--profiler-version-1",
|
||||
"--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",
|
||||
"--commit-stop",
|
||||
]
|
||||
`);
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
@@ -89,20 +89,20 @@ describe('SchedulingProfiler labels', () => {
|
||||
|
||||
if (gate(flags => flags.enableSchedulingProfiler)) {
|
||||
expect(clearedMarks).toMatchInlineSnapshot(`
|
||||
Array [
|
||||
"__v3",
|
||||
"--schedule-render-1",
|
||||
"--render-start-1",
|
||||
"--render-stop",
|
||||
"--commit-start-1",
|
||||
"--react-version-17.0.3",
|
||||
"--profiler-version-1",
|
||||
"--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",
|
||||
"--layout-effects-start-1",
|
||||
"--layout-effects-stop",
|
||||
"--commit-stop",
|
||||
]
|
||||
`);
|
||||
Array [
|
||||
"__v3",
|
||||
"--schedule-render-1",
|
||||
"--render-start-1",
|
||||
"--render-stop",
|
||||
"--commit-start-1",
|
||||
"--react-version-17.0.3",
|
||||
"--profiler-version-1",
|
||||
"--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",
|
||||
"--layout-effects-start-1",
|
||||
"--layout-effects-stop",
|
||||
"--commit-stop",
|
||||
]
|
||||
`);
|
||||
}
|
||||
});
|
||||
|
||||
@@ -114,11 +114,11 @@ describe('SchedulingProfiler labels', () => {
|
||||
|
||||
root.render(<div />);
|
||||
expect(clearedMarks).toMatchInlineSnapshot(`
|
||||
Array [
|
||||
"__v3",
|
||||
"--schedule-render-16",
|
||||
]
|
||||
`);
|
||||
Array [
|
||||
"__v3",
|
||||
"--schedule-render-16",
|
||||
]
|
||||
`);
|
||||
});
|
||||
}
|
||||
});
|
||||
@@ -152,21 +152,21 @@ describe('SchedulingProfiler labels', () => {
|
||||
});
|
||||
|
||||
expect(clearedMarks).toMatchInlineSnapshot(`
|
||||
Array [
|
||||
"--schedule-state-update-1-App",
|
||||
"--render-start-1",
|
||||
"--component-render-start-App",
|
||||
"--component-render-stop",
|
||||
"--render-stop",
|
||||
"--commit-start-1",
|
||||
"--react-version-17.0.3",
|
||||
"--profiler-version-1",
|
||||
"--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",
|
||||
"--layout-effects-start-1",
|
||||
"--layout-effects-stop",
|
||||
"--commit-stop",
|
||||
]
|
||||
`);
|
||||
Array [
|
||||
"--schedule-state-update-1-App",
|
||||
"--render-start-1",
|
||||
"--component-render-start-App",
|
||||
"--component-render-stop",
|
||||
"--render-stop",
|
||||
"--commit-start-1",
|
||||
"--react-version-17.0.3",
|
||||
"--profiler-version-1",
|
||||
"--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",
|
||||
"--layout-effects-start-1",
|
||||
"--layout-effects-stop",
|
||||
"--commit-stop",
|
||||
]
|
||||
`);
|
||||
}
|
||||
});
|
||||
|
||||
@@ -196,21 +196,21 @@ describe('SchedulingProfiler labels', () => {
|
||||
});
|
||||
|
||||
expect(clearedMarks).toMatchInlineSnapshot(`
|
||||
Array [
|
||||
"--schedule-state-update-4-App",
|
||||
"--render-start-4",
|
||||
"--component-render-start-App",
|
||||
"--component-render-stop",
|
||||
"--render-stop",
|
||||
"--commit-start-4",
|
||||
"--react-version-17.0.3",
|
||||
"--profiler-version-1",
|
||||
"--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",
|
||||
"--layout-effects-start-4",
|
||||
"--layout-effects-stop",
|
||||
"--commit-stop",
|
||||
]
|
||||
`);
|
||||
Array [
|
||||
"--schedule-state-update-4-App",
|
||||
"--render-start-4",
|
||||
"--component-render-start-App",
|
||||
"--component-render-stop",
|
||||
"--render-stop",
|
||||
"--commit-start-4",
|
||||
"--react-version-17.0.3",
|
||||
"--profiler-version-1",
|
||||
"--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",
|
||||
"--layout-effects-start-4",
|
||||
"--layout-effects-stop",
|
||||
"--commit-stop",
|
||||
]
|
||||
`);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
@@ -113,6 +113,7 @@ describe('StrictEffectsMode defaults', () => {
|
||||
</>,
|
||||
);
|
||||
|
||||
expect(Scheduler).toHaveYielded([]);
|
||||
expect(Scheduler).toFlushUntilNextPaint([
|
||||
// Cleanup and re-run "one" (and "two") since there is no dependencies array.
|
||||
'useLayoutEffect unmount "one"',
|
||||
|
||||
@@ -37,6 +37,7 @@ export {
|
||||
unstable_LegacyHidden,
|
||||
unstable_Offscreen,
|
||||
unstable_Scope,
|
||||
unstable_getCacheSignal,
|
||||
unstable_getCacheForType,
|
||||
unstable_useCacheRefresh,
|
||||
unstable_useOpaqueIdentifier,
|
||||
|
||||
@@ -33,6 +33,7 @@ export {
|
||||
unstable_DebugTracingMode,
|
||||
unstable_LegacyHidden,
|
||||
unstable_Offscreen,
|
||||
unstable_getCacheSignal,
|
||||
unstable_getCacheForType,
|
||||
unstable_useCacheRefresh,
|
||||
unstable_useOpaqueIdentifier,
|
||||
|
||||
@@ -58,6 +58,7 @@ export {
|
||||
unstable_LegacyHidden,
|
||||
unstable_Offscreen,
|
||||
unstable_Scope,
|
||||
unstable_getCacheSignal,
|
||||
unstable_getCacheForType,
|
||||
unstable_useCacheRefresh,
|
||||
unstable_useOpaqueIdentifier,
|
||||
|
||||
@@ -36,6 +36,7 @@ export {
|
||||
unstable_LegacyHidden,
|
||||
unstable_Offscreen,
|
||||
unstable_Scope,
|
||||
unstable_getCacheSignal,
|
||||
unstable_getCacheForType,
|
||||
unstable_useCacheRefresh,
|
||||
unstable_useOpaqueIdentifier,
|
||||
|
||||
@@ -35,6 +35,7 @@ import {lazy} from './ReactLazy';
|
||||
import {forwardRef} from './ReactForwardRef';
|
||||
import {memo} from './ReactMemo';
|
||||
import {
|
||||
getCacheSignal,
|
||||
getCacheForType,
|
||||
useCallback,
|
||||
useContext,
|
||||
@@ -119,6 +120,7 @@ export {
|
||||
REACT_SUSPENSE_LIST_TYPE as SuspenseList,
|
||||
REACT_LEGACY_HIDDEN_TYPE as unstable_LegacyHidden,
|
||||
REACT_OFFSCREEN_TYPE as unstable_Offscreen,
|
||||
getCacheSignal as unstable_getCacheSignal,
|
||||
getCacheForType as unstable_getCacheForType,
|
||||
useCacheRefresh as unstable_useCacheRefresh,
|
||||
REACT_CACHE_TYPE as unstable_Cache,
|
||||
|
||||
@@ -41,6 +41,12 @@ function resolveDispatcher() {
|
||||
return ((dispatcher: any): Dispatcher);
|
||||
}
|
||||
|
||||
export function getCacheSignal(): AbortSignal {
|
||||
const dispatcher = resolveDispatcher();
|
||||
// $FlowFixMe This is unstable, thus optional
|
||||
return dispatcher.getCacheSignal();
|
||||
}
|
||||
|
||||
export function getCacheForType<T>(resourceType: () => T): T {
|
||||
const dispatcher = resolveDispatcher();
|
||||
// $FlowFixMe This is unstable, thus optional
|
||||
|
||||
@@ -25,6 +25,7 @@ export {
|
||||
memo,
|
||||
startTransition,
|
||||
unstable_DebugTracingMode,
|
||||
unstable_getCacheSignal,
|
||||
unstable_getCacheForType,
|
||||
unstable_useOpaqueIdentifier,
|
||||
useCallback,
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
/* eslint-disable */
|
||||
|
||||
const AbortController = require('abort-controller');
|
||||
|
||||
const NODE_ENV = process.env.NODE_ENV;
|
||||
if (NODE_ENV !== 'development' && NODE_ENV !== 'production') {
|
||||
throw new Error('NODE_ENV must either be set to development or production.');
|
||||
@@ -21,6 +23,8 @@ global.__EXPERIMENTAL__ =
|
||||
|
||||
global.__VARIANT__ = !!process.env.VARIANT;
|
||||
|
||||
global.AbortController = AbortController;
|
||||
|
||||
if (typeof window !== 'undefined') {
|
||||
global.requestIdleCallback = function(callback) {
|
||||
return setTimeout(() => {
|
||||
|
||||
Reference in New Issue
Block a user