Add built-in Suspense cache with support for invalidation (refreshing) (#20456)

This commit is contained in:
Andrew Clark
2020-12-18 10:57:24 -08:00
committed by GitHub
parent 00a5b08e24
commit efc57e5cbb
39 changed files with 2387 additions and 59 deletions
@@ -489,6 +489,10 @@ function useOpaqueIdentifier(): OpaqueIDType {
);
}
function useCacheRefresh(): <T>(?() => T, ?T) => void {
invariant(false, 'Not implemented.');
}
function noop(): void {}
export let currentPartialRenderer: PartialRenderer = (null: any);
@@ -520,4 +524,5 @@ export const Dispatcher: DispatcherType = {
if (enableCache) {
Dispatcher.getCacheForType = getCacheForType;
Dispatcher.useCacheRefresh = useCacheRefresh;
}
@@ -27,6 +27,7 @@ import {
enableProfilerTimer,
enableFundamentalAPI,
enableScopeAPI,
enableCache,
} from 'shared/ReactFeatureFlags';
import {NoFlags, Placement, StaticMask} from './ReactFiberFlags';
import {ConcurrentRoot, BlockingRoot} from './ReactRootTags';
@@ -54,6 +55,7 @@ import {
ScopeComponent,
OffscreenComponent,
LegacyHiddenComponent,
CacheComponent,
} from './ReactWorkTags';
import getComponentName from 'shared/getComponentName';
@@ -88,6 +90,7 @@ import {
REACT_SCOPE_TYPE,
REACT_OFFSCREEN_TYPE,
REACT_LEGACY_HIDDEN_TYPE,
REACT_CACHE_TYPE,
} from 'shared/ReactSymbols';
export type {Fiber};
@@ -501,6 +504,11 @@ export function createFiberFromTypeAndProps(
return createFiberFromScope(type, pendingProps, mode, lanes, key);
}
// eslint-disable-next-line no-fallthrough
case REACT_CACHE_TYPE:
if (enableCache) {
return createFiberFromCache(pendingProps, mode, lanes, key);
}
// eslint-disable-next-line no-fallthrough
default: {
if (typeof type === 'object' && type !== null) {
switch (type.$$typeof) {
@@ -745,6 +753,24 @@ export function createFiberFromLegacyHidden(
return fiber;
}
export function createFiberFromCache(
pendingProps: any,
mode: TypeOfMode,
lanes: Lanes,
key: null | string,
) {
const fiber = createFiber(CacheComponent, pendingProps, key, mode);
// TODO: The Cache fiber shouldn't have a type. It has a tag.
// This needs to be fixed in getComponentName so that it relies on the tag
// instead.
if (__DEV__) {
fiber.type = REACT_CACHE_TYPE;
}
fiber.elementType = REACT_CACHE_TYPE;
fiber.lanes = lanes;
return fiber;
}
export function createFiberFromText(
content: string,
mode: TypeOfMode,
@@ -27,6 +27,7 @@ import {
enableProfilerTimer,
enableFundamentalAPI,
enableScopeAPI,
enableCache,
} from 'shared/ReactFeatureFlags';
import {NoFlags, Placement, StaticMask} from './ReactFiberFlags';
import {ConcurrentRoot, BlockingRoot} from './ReactRootTags';
@@ -54,6 +55,7 @@ import {
ScopeComponent,
OffscreenComponent,
LegacyHiddenComponent,
CacheComponent,
} from './ReactWorkTags';
import getComponentName from 'shared/getComponentName';
@@ -88,6 +90,7 @@ import {
REACT_SCOPE_TYPE,
REACT_OFFSCREEN_TYPE,
REACT_LEGACY_HIDDEN_TYPE,
REACT_CACHE_TYPE,
} from 'shared/ReactSymbols';
export type {Fiber};
@@ -501,6 +504,11 @@ export function createFiberFromTypeAndProps(
return createFiberFromScope(type, pendingProps, mode, lanes, key);
}
// eslint-disable-next-line no-fallthrough
case REACT_CACHE_TYPE:
if (enableCache) {
return createFiberFromCache(pendingProps, mode, lanes, key);
}
// eslint-disable-next-line no-fallthrough
default: {
if (typeof type === 'object' && type !== null) {
switch (type.$$typeof) {
@@ -745,6 +753,24 @@ export function createFiberFromLegacyHidden(
return fiber;
}
export function createFiberFromCache(
pendingProps: any,
mode: TypeOfMode,
lanes: Lanes,
key: null | string,
) {
const fiber = createFiber(CacheComponent, pendingProps, key, mode);
// TODO: The Cache fiber shouldn't have a type. It has a tag.
// This needs to be fixed in getComponentName so that it relies on the tag
// instead.
if (__DEV__) {
fiber.type = REACT_CACHE_TYPE;
}
fiber.elementType = REACT_CACHE_TYPE;
fiber.lanes = lanes;
return fiber;
}
export function createFiberFromText(
content: string,
mode: TypeOfMode,
@@ -23,6 +23,12 @@ import type {
OffscreenProps,
OffscreenState,
} from './ReactFiberOffscreenComponent';
import type {
Cache,
CacheComponentState,
SpawnedCachePool,
} from './ReactFiberCacheComponent.new';
import type {UpdateQueue} from './ReactUpdateQueue.new';
import checkPropTypes from 'shared/checkPropTypes';
@@ -50,6 +56,7 @@ import {
ScopeComponent,
OffscreenComponent,
LegacyHiddenComponent,
CacheComponent,
} from './ReactWorkTags';
import {
NoFlags,
@@ -76,6 +83,7 @@ import {
enableFundamentalAPI,
warnAboutDefaultPropsOnFunctionComponents,
enableScopeAPI,
enableCache,
} from 'shared/ReactFeatureFlags';
import invariant from 'shared/invariant';
import shallowEqual from 'shared/shallowEqual';
@@ -129,6 +137,7 @@ import {
isSuspenseInstanceFallback,
registerSuspenseInstanceRetry,
supportsHydration,
isPrimaryRenderer,
} from './ReactFiberHostConfig';
import type {SuspenseInstance} from './ReactFiberHostConfig';
import {shouldSuspend} from './ReactFiberReconciler';
@@ -201,6 +210,16 @@ import {
} from './ReactFiberWorkLoop.new';
import {unstable_wrap as Schedule_tracing_wrap} from 'scheduler/tracing';
import {setWorkInProgressVersion} from './ReactMutableSource.new';
import {
requestCacheFromPool,
pushCacheProvider,
pushRootCachePool,
CacheContext,
getSuspendedCachePool,
restoreSpawnedCachePool,
getOffscreenDeferredCachePool,
} from './ReactFiberCacheComponent.new';
import {MAX_SIGNED_31_BIT_INT} from './MaxInts';
import {disableLogs, reenableLogs} from 'shared/ConsolePatchingDev';
@@ -576,23 +595,39 @@ function updateOffscreenComponent(
const prevState: OffscreenState | null =
current !== null ? current.memoizedState : null;
// If this is not null, this is a cache pool that was carried over from the
// previous render. We will push this to the cache pool context so that we can
// resume in-flight requests.
let spawnedCachePool: SpawnedCachePool | null = null;
if (
nextProps.mode === 'hidden' ||
nextProps.mode === 'unstable-defer-without-hiding'
) {
// Rendering a hidden tree.
if ((workInProgress.mode & ConcurrentMode) === NoMode) {
// In legacy sync mode, don't defer the subtree. Render it now.
// TODO: Figure out what we should do in Blocking mode.
const nextState: OffscreenState = {
baseLanes: NoLanes,
cachePool: null,
};
workInProgress.memoizedState = nextState;
pushRenderLanes(workInProgress, renderLanes);
} else if (!includesSomeLane(renderLanes, (OffscreenLane: Lane))) {
// We're hidden, and we're not rendering at Offscreen. We will bail out
// and resume this tree later.
let nextBaseLanes;
if (prevState !== null) {
const prevBaseLanes = prevState.baseLanes;
nextBaseLanes = mergeLanes(prevBaseLanes, renderLanes);
if (enableCache) {
// Save the cache pool so we can resume later.
spawnedCachePool = getOffscreenDeferredCachePool();
// We don't need to push to the cache pool because we're about to
// bail out. There won't be a context mismatch because we only pop
// the cache pool if `updateQueue` is non-null.
}
} else {
nextBaseLanes = renderLanes;
}
@@ -606,16 +641,35 @@ function updateOffscreenComponent(
);
const nextState: OffscreenState = {
baseLanes: nextBaseLanes,
cachePool: spawnedCachePool,
};
workInProgress.memoizedState = nextState;
workInProgress.updateQueue = null;
// We're about to bail out, but we need to push this to the stack anyway
// to avoid a push/pop misalignment.
pushRenderLanes(workInProgress, nextBaseLanes);
return null;
} else {
// This is the second render. The surrounding visible content has already
// committed. Now we resume rendering the hidden tree.
if (enableCache && prevState !== null) {
// If the render that spawned this one accessed the cache pool, resume
// using the same cache. Unless the parent changed, since that means
// there was a refresh.
const prevCachePool = prevState.cachePool;
if (prevCachePool !== null) {
spawnedCachePool = restoreSpawnedCachePool(
workInProgress,
prevCachePool,
);
}
}
// Rendering at offscreen, so we can clear the base lanes.
const nextState: OffscreenState = {
baseLanes: NoLanes,
cachePool: null,
};
workInProgress.memoizedState = nextState;
// Push the lanes that were skipped when we bailed out.
@@ -624,9 +678,26 @@ function updateOffscreenComponent(
pushRenderLanes(workInProgress, subtreeRenderLanes);
}
} else {
// Rendering a visible tree.
let subtreeRenderLanes;
if (prevState !== null) {
// We're going from hidden -> visible.
subtreeRenderLanes = mergeLanes(prevState.baseLanes, renderLanes);
if (enableCache) {
// If the render that spawned this one accessed the cache pool, resume
// using the same cache. Unless the parent changed, since that means
// there was a refresh.
const prevCachePool = prevState.cachePool;
if (prevCachePool !== null) {
spawnedCachePool = restoreSpawnedCachePool(
workInProgress,
prevCachePool,
);
}
}
// Since we're not hidden anymore, reset the state
workInProgress.memoizedState = null;
} else {
@@ -638,6 +709,12 @@ function updateOffscreenComponent(
pushRenderLanes(workInProgress, subtreeRenderLanes);
}
if (enableCache) {
// If we have a cache pool from a previous render attempt, then this will be
// non-null. We use this to infer whether to push/pop the cache context.
workInProgress.updateQueue = spawnedCachePool;
}
reconcileChildren(current, workInProgress, nextChildren, renderLanes);
return workInProgress.child;
}
@@ -647,6 +724,79 @@ function updateOffscreenComponent(
// fork the function.
const updateLegacyHiddenComponent = updateOffscreenComponent;
function updateCacheComponent(
current: Fiber | null,
workInProgress: Fiber,
updateLanes: Lanes,
renderLanes: Lanes,
) {
if (!enableCache) {
return null;
}
prepareToReadContext(workInProgress, renderLanes);
const parentCache = readContext(CacheContext);
if (current === null) {
// Initial mount. Request a fresh cache from the pool.
const freshCache = requestCacheFromPool(renderLanes);
const initialState: CacheComponentState = {
parent: parentCache,
cache: freshCache,
};
workInProgress.memoizedState = initialState;
initializeUpdateQueue(workInProgress);
pushCacheProvider(workInProgress, freshCache);
} else {
// Check for updates
if (includesSomeLane(renderLanes, updateLanes)) {
cloneUpdateQueue(current, workInProgress);
processUpdateQueue(workInProgress, null, null, renderLanes);
}
const prevState: CacheComponentState = current.memoizedState;
const nextState: CacheComponentState = workInProgress.memoizedState;
// Compare the new parent cache to the previous to see detect there was
// a refresh.
if (prevState.parent !== parentCache) {
// Refresh in parent. Update the parent.
const derivedState: CacheComponentState = {
parent: parentCache,
cache: parentCache,
};
// Copied from getDerivedStateFromProps implementation. Once the update
// queue is empty, persist the derived state onto the base state.
workInProgress.memoizedState = derivedState;
if (workInProgress.lanes === NoLanes) {
const updateQueue: UpdateQueue<any> = (workInProgress.updateQueue: any);
workInProgress.memoizedState = updateQueue.baseState = derivedState;
}
pushCacheProvider(workInProgress, parentCache);
// No need to propagate a context change because the refreshed parent
// already did.
} else {
// The parent didn't refresh. Now check if this cache did.
const nextCache = nextState.cache;
pushCacheProvider(workInProgress, nextCache);
if (nextCache !== prevState.cache) {
// This cache refreshed. Propagate a context change.
propagateContextChange(
workInProgress,
CacheContext,
MAX_SIGNED_31_BIT_INT,
renderLanes,
);
}
}
}
const nextChildren = workInProgress.pendingProps.children;
reconcileChildren(current, workInProgress, nextChildren, renderLanes);
return workInProgress.child;
}
function updateFragment(
current: Fiber | null,
workInProgress: Fiber,
@@ -991,10 +1141,28 @@ function updateHostRoot(current, workInProgress, renderLanes) {
);
const nextProps = workInProgress.pendingProps;
const prevState = workInProgress.memoizedState;
const prevChildren = prevState !== null ? prevState.element : null;
const prevChildren = prevState.element;
cloneUpdateQueue(current, workInProgress);
processUpdateQueue(workInProgress, nextProps, null, renderLanes);
const nextState = workInProgress.memoizedState;
const root: FiberRoot = workInProgress.stateNode;
if (enableCache) {
const nextCache: Cache = nextState.cache;
pushRootCachePool(root);
pushCacheProvider(workInProgress, nextCache);
if (nextCache !== prevState.cache) {
// The root cache refreshed.
propagateContextChange(
workInProgress,
CacheContext,
MAX_SIGNED_31_BIT_INT,
renderLanes,
);
}
}
// Caution: React DevTools currently depends on this property
// being called "element".
const nextChildren = nextState.element;
@@ -1002,7 +1170,6 @@ function updateHostRoot(current, workInProgress, renderLanes) {
resetHydrationState();
return bailoutOnAlreadyFinishedWork(current, workInProgress, renderLanes);
}
const root: FiberRoot = workInProgress.stateNode;
if (root.hydrate && enterHydrationState(workInProgress)) {
// If we don't have any current children this might be the first pass.
// We always try to hydrate. If this isn't a hydration pass there won't
@@ -1562,6 +1729,7 @@ const SUSPENDED_MARKER: SuspenseState = {
function mountSuspenseOffscreenState(renderLanes: Lanes): OffscreenState {
return {
baseLanes: renderLanes,
cachePool: getSuspendedCachePool(),
};
}
@@ -1569,8 +1737,33 @@ function updateSuspenseOffscreenState(
prevOffscreenState: OffscreenState,
renderLanes: Lanes,
): OffscreenState {
let cachePool: SpawnedCachePool | null = null;
if (enableCache) {
const prevCachePool: SpawnedCachePool | null = prevOffscreenState.cachePool;
if (prevCachePool !== null) {
const parentCache = isPrimaryRenderer
? CacheContext._currentValue
: CacheContext._currentValue2;
if (prevCachePool.parent !== parentCache) {
// Detected a refresh in the parent. This overrides any previously
// suspended cache.
cachePool = {
parent: parentCache,
pool: parentCache,
};
} else {
// We can reuse the cache from last time. The only thing that would have
// overridden it is a parent refresh, which we checked for above.
cachePool = prevCachePool;
}
} else {
// If there's no previous cache pool, grab the current one.
cachePool = getSuspendedCachePool();
}
}
return {
baseLanes: mergeLanes(prevOffscreenState.baseLanes, renderLanes),
cachePool,
};
}
@@ -2801,7 +2994,7 @@ function updateContextProvider(
}
}
pushProvider(workInProgress, newValue);
pushProvider(workInProgress, context, newValue);
if (oldProps !== null) {
const oldValue = oldProps.value;
@@ -3068,6 +3261,12 @@ function beginWork(
switch (workInProgress.tag) {
case HostRoot:
pushHostRootContext(workInProgress);
if (enableCache) {
const root: FiberRoot = workInProgress.stateNode;
const cache: Cache = current.memoizedState.cache;
pushCacheProvider(workInProgress, cache);
pushRootCachePool(root);
}
resetHydrationState();
break;
case HostComponent:
@@ -3088,7 +3287,8 @@ function beginWork(
break;
case ContextProvider: {
const newValue = workInProgress.memoizedProps.value;
pushProvider(workInProgress, newValue);
const context: ReactContext<any> = workInProgress.type._context;
pushProvider(workInProgress, context, newValue);
break;
}
case Profiler:
@@ -3234,6 +3434,13 @@ function beginWork(
workInProgress.lanes = NoLanes;
return updateOffscreenComponent(current, workInProgress, renderLanes);
}
case CacheComponent: {
if (enableCache) {
const cache: Cache = current.memoizedState.cache;
pushCacheProvider(workInProgress, cache);
}
break;
}
}
return bailoutOnAlreadyFinishedWork(current, workInProgress, renderLanes);
} else {
@@ -3418,6 +3625,17 @@ function beginWork(
case LegacyHiddenComponent: {
return updateLegacyHiddenComponent(current, workInProgress, renderLanes);
}
case CacheComponent: {
if (enableCache) {
return updateCacheComponent(
current,
workInProgress,
updateLanes,
renderLanes,
);
}
break;
}
}
invariant(
false,
@@ -23,6 +23,12 @@ import type {
OffscreenProps,
OffscreenState,
} from './ReactFiberOffscreenComponent';
import type {
Cache,
CacheComponentState,
SpawnedCachePool,
} from './ReactFiberCacheComponent.old';
import type {UpdateQueue} from './ReactUpdateQueue.old';
import checkPropTypes from 'shared/checkPropTypes';
@@ -50,6 +56,7 @@ import {
ScopeComponent,
OffscreenComponent,
LegacyHiddenComponent,
CacheComponent,
} from './ReactWorkTags';
import {
NoFlags,
@@ -76,6 +83,7 @@ import {
enableFundamentalAPI,
warnAboutDefaultPropsOnFunctionComponents,
enableScopeAPI,
enableCache,
} from 'shared/ReactFeatureFlags';
import invariant from 'shared/invariant';
import shallowEqual from 'shared/shallowEqual';
@@ -129,6 +137,7 @@ import {
isSuspenseInstanceFallback,
registerSuspenseInstanceRetry,
supportsHydration,
isPrimaryRenderer,
} from './ReactFiberHostConfig';
import type {SuspenseInstance} from './ReactFiberHostConfig';
import {shouldSuspend} from './ReactFiberReconciler';
@@ -201,6 +210,16 @@ import {
} from './ReactFiberWorkLoop.old';
import {unstable_wrap as Schedule_tracing_wrap} from 'scheduler/tracing';
import {setWorkInProgressVersion} from './ReactMutableSource.old';
import {
requestCacheFromPool,
pushCacheProvider,
pushRootCachePool,
CacheContext,
getSuspendedCachePool,
restoreSpawnedCachePool,
getOffscreenDeferredCachePool,
} from './ReactFiberCacheComponent.old';
import {MAX_SIGNED_31_BIT_INT} from './MaxInts';
import {disableLogs, reenableLogs} from 'shared/ConsolePatchingDev';
@@ -576,23 +595,39 @@ function updateOffscreenComponent(
const prevState: OffscreenState | null =
current !== null ? current.memoizedState : null;
// If this is not null, this is a cache pool that was carried over from the
// previous render. We will push this to the cache pool context so that we can
// resume in-flight requests.
let spawnedCachePool: SpawnedCachePool | null = null;
if (
nextProps.mode === 'hidden' ||
nextProps.mode === 'unstable-defer-without-hiding'
) {
// Rendering a hidden tree.
if ((workInProgress.mode & ConcurrentMode) === NoMode) {
// In legacy sync mode, don't defer the subtree. Render it now.
// TODO: Figure out what we should do in Blocking mode.
const nextState: OffscreenState = {
baseLanes: NoLanes,
cachePool: null,
};
workInProgress.memoizedState = nextState;
pushRenderLanes(workInProgress, renderLanes);
} else if (!includesSomeLane(renderLanes, (OffscreenLane: Lane))) {
// We're hidden, and we're not rendering at Offscreen. We will bail out
// and resume this tree later.
let nextBaseLanes;
if (prevState !== null) {
const prevBaseLanes = prevState.baseLanes;
nextBaseLanes = mergeLanes(prevBaseLanes, renderLanes);
if (enableCache) {
// Save the cache pool so we can resume later.
spawnedCachePool = getOffscreenDeferredCachePool();
// We don't need to push to the cache pool because we're about to
// bail out. There won't be a context mismatch because we only pop
// the cache pool if `updateQueue` is non-null.
}
} else {
nextBaseLanes = renderLanes;
}
@@ -606,16 +641,35 @@ function updateOffscreenComponent(
);
const nextState: OffscreenState = {
baseLanes: nextBaseLanes,
cachePool: spawnedCachePool,
};
workInProgress.memoizedState = nextState;
workInProgress.updateQueue = null;
// We're about to bail out, but we need to push this to the stack anyway
// to avoid a push/pop misalignment.
pushRenderLanes(workInProgress, nextBaseLanes);
return null;
} else {
// This is the second render. The surrounding visible content has already
// committed. Now we resume rendering the hidden tree.
if (enableCache && prevState !== null) {
// If the render that spawned this one accessed the cache pool, resume
// using the same cache. Unless the parent changed, since that means
// there was a refresh.
const prevCachePool = prevState.cachePool;
if (prevCachePool !== null) {
spawnedCachePool = restoreSpawnedCachePool(
workInProgress,
prevCachePool,
);
}
}
// Rendering at offscreen, so we can clear the base lanes.
const nextState: OffscreenState = {
baseLanes: NoLanes,
cachePool: null,
};
workInProgress.memoizedState = nextState;
// Push the lanes that were skipped when we bailed out.
@@ -624,9 +678,26 @@ function updateOffscreenComponent(
pushRenderLanes(workInProgress, subtreeRenderLanes);
}
} else {
// Rendering a visible tree.
let subtreeRenderLanes;
if (prevState !== null) {
// We're going from hidden -> visible.
subtreeRenderLanes = mergeLanes(prevState.baseLanes, renderLanes);
if (enableCache) {
// If the render that spawned this one accessed the cache pool, resume
// using the same cache. Unless the parent changed, since that means
// there was a refresh.
const prevCachePool = prevState.cachePool;
if (prevCachePool !== null) {
spawnedCachePool = restoreSpawnedCachePool(
workInProgress,
prevCachePool,
);
}
}
// Since we're not hidden anymore, reset the state
workInProgress.memoizedState = null;
} else {
@@ -638,6 +709,12 @@ function updateOffscreenComponent(
pushRenderLanes(workInProgress, subtreeRenderLanes);
}
if (enableCache) {
// If we have a cache pool from a previous render attempt, then this will be
// non-null. We use this to infer whether to push/pop the cache context.
workInProgress.updateQueue = spawnedCachePool;
}
reconcileChildren(current, workInProgress, nextChildren, renderLanes);
return workInProgress.child;
}
@@ -647,6 +724,79 @@ function updateOffscreenComponent(
// fork the function.
const updateLegacyHiddenComponent = updateOffscreenComponent;
function updateCacheComponent(
current: Fiber | null,
workInProgress: Fiber,
updateLanes: Lanes,
renderLanes: Lanes,
) {
if (!enableCache) {
return null;
}
prepareToReadContext(workInProgress, renderLanes);
const parentCache = readContext(CacheContext);
if (current === null) {
// Initial mount. Request a fresh cache from the pool.
const freshCache = requestCacheFromPool(renderLanes);
const initialState: CacheComponentState = {
parent: parentCache,
cache: freshCache,
};
workInProgress.memoizedState = initialState;
initializeUpdateQueue(workInProgress);
pushCacheProvider(workInProgress, freshCache);
} else {
// Check for updates
if (includesSomeLane(renderLanes, updateLanes)) {
cloneUpdateQueue(current, workInProgress);
processUpdateQueue(workInProgress, null, null, renderLanes);
}
const prevState: CacheComponentState = current.memoizedState;
const nextState: CacheComponentState = workInProgress.memoizedState;
// Compare the new parent cache to the previous to see detect there was
// a refresh.
if (prevState.parent !== parentCache) {
// Refresh in parent. Update the parent.
const derivedState: CacheComponentState = {
parent: parentCache,
cache: parentCache,
};
// Copied from getDerivedStateFromProps implementation. Once the update
// queue is empty, persist the derived state onto the base state.
workInProgress.memoizedState = derivedState;
if (workInProgress.lanes === NoLanes) {
const updateQueue: UpdateQueue<any> = (workInProgress.updateQueue: any);
workInProgress.memoizedState = updateQueue.baseState = derivedState;
}
pushCacheProvider(workInProgress, parentCache);
// No need to propagate a context change because the refreshed parent
// already did.
} else {
// The parent didn't refresh. Now check if this cache did.
const nextCache = nextState.cache;
pushCacheProvider(workInProgress, nextCache);
if (nextCache !== prevState.cache) {
// This cache refreshed. Propagate a context change.
propagateContextChange(
workInProgress,
CacheContext,
MAX_SIGNED_31_BIT_INT,
renderLanes,
);
}
}
}
const nextChildren = workInProgress.pendingProps.children;
reconcileChildren(current, workInProgress, nextChildren, renderLanes);
return workInProgress.child;
}
function updateFragment(
current: Fiber | null,
workInProgress: Fiber,
@@ -991,10 +1141,28 @@ function updateHostRoot(current, workInProgress, renderLanes) {
);
const nextProps = workInProgress.pendingProps;
const prevState = workInProgress.memoizedState;
const prevChildren = prevState !== null ? prevState.element : null;
const prevChildren = prevState.element;
cloneUpdateQueue(current, workInProgress);
processUpdateQueue(workInProgress, nextProps, null, renderLanes);
const nextState = workInProgress.memoizedState;
const root: FiberRoot = workInProgress.stateNode;
if (enableCache) {
const nextCache: Cache = nextState.cache;
pushRootCachePool(root);
pushCacheProvider(workInProgress, nextCache);
if (nextCache !== prevState.cache) {
// The root cache refreshed.
propagateContextChange(
workInProgress,
CacheContext,
MAX_SIGNED_31_BIT_INT,
renderLanes,
);
}
}
// Caution: React DevTools currently depends on this property
// being called "element".
const nextChildren = nextState.element;
@@ -1002,7 +1170,6 @@ function updateHostRoot(current, workInProgress, renderLanes) {
resetHydrationState();
return bailoutOnAlreadyFinishedWork(current, workInProgress, renderLanes);
}
const root: FiberRoot = workInProgress.stateNode;
if (root.hydrate && enterHydrationState(workInProgress)) {
// If we don't have any current children this might be the first pass.
// We always try to hydrate. If this isn't a hydration pass there won't
@@ -1562,6 +1729,7 @@ const SUSPENDED_MARKER: SuspenseState = {
function mountSuspenseOffscreenState(renderLanes: Lanes): OffscreenState {
return {
baseLanes: renderLanes,
cachePool: getSuspendedCachePool(),
};
}
@@ -1569,8 +1737,33 @@ function updateSuspenseOffscreenState(
prevOffscreenState: OffscreenState,
renderLanes: Lanes,
): OffscreenState {
let cachePool: SpawnedCachePool | null = null;
if (enableCache) {
const prevCachePool: SpawnedCachePool | null = prevOffscreenState.cachePool;
if (prevCachePool !== null) {
const parentCache = isPrimaryRenderer
? CacheContext._currentValue
: CacheContext._currentValue2;
if (prevCachePool.parent !== parentCache) {
// Detected a refresh in the parent. This overrides any previously
// suspended cache.
cachePool = {
parent: parentCache,
pool: parentCache,
};
} else {
// We can reuse the cache from last time. The only thing that would have
// overridden it is a parent refresh, which we checked for above.
cachePool = prevCachePool;
}
} else {
// If there's no previous cache pool, grab the current one.
cachePool = getSuspendedCachePool();
}
}
return {
baseLanes: mergeLanes(prevOffscreenState.baseLanes, renderLanes),
cachePool,
};
}
@@ -2801,7 +2994,7 @@ function updateContextProvider(
}
}
pushProvider(workInProgress, newValue);
pushProvider(workInProgress, context, newValue);
if (oldProps !== null) {
const oldValue = oldProps.value;
@@ -3068,6 +3261,12 @@ function beginWork(
switch (workInProgress.tag) {
case HostRoot:
pushHostRootContext(workInProgress);
if (enableCache) {
const root: FiberRoot = workInProgress.stateNode;
const cache: Cache = current.memoizedState.cache;
pushCacheProvider(workInProgress, cache);
pushRootCachePool(root);
}
resetHydrationState();
break;
case HostComponent:
@@ -3088,7 +3287,8 @@ function beginWork(
break;
case ContextProvider: {
const newValue = workInProgress.memoizedProps.value;
pushProvider(workInProgress, newValue);
const context: ReactContext<any> = workInProgress.type._context;
pushProvider(workInProgress, context, newValue);
break;
}
case Profiler:
@@ -3234,6 +3434,13 @@ function beginWork(
workInProgress.lanes = NoLanes;
return updateOffscreenComponent(current, workInProgress, renderLanes);
}
case CacheComponent: {
if (enableCache) {
const cache: Cache = current.memoizedState.cache;
pushCacheProvider(workInProgress, cache);
}
break;
}
}
return bailoutOnAlreadyFinishedWork(current, workInProgress, renderLanes);
} else {
@@ -3418,6 +3625,17 @@ function beginWork(
case LegacyHiddenComponent: {
return updateLegacyHiddenComponent(current, workInProgress, renderLanes);
}
case CacheComponent: {
if (enableCache) {
return updateCacheComponent(
current,
workInProgress,
updateLanes,
renderLanes,
);
}
break;
}
}
invariant(
false,
@@ -0,0 +1,204 @@
/**
* Copyright (c) Facebook, Inc. and its affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
* @flow
*/
import type {ReactContext} from 'shared/ReactTypes';
import type {FiberRoot} from './ReactInternalTypes';
import type {Lanes} from './ReactFiberLane.new';
import type {StackCursor} from './ReactFiberStack.new';
import {enableCache} from 'shared/ReactFeatureFlags';
import {REACT_CONTEXT_TYPE} from 'shared/ReactSymbols';
import {isPrimaryRenderer} from './ReactFiberHostConfig';
import {createCursor, push, pop} from './ReactFiberStack.new';
import {pushProvider, popProvider} from './ReactFiberNewContext.new';
export type Cache = Map<() => mixed, mixed>;
export type CacheComponentState = {|
+parent: Cache,
+cache: Cache,
|};
export type SpawnedCachePool = {|
+parent: Cache,
+pool: Cache,
|};
export const CacheContext: ReactContext<Cache> = enableCache
? {
$$typeof: REACT_CONTEXT_TYPE,
// We don't use Consumer/Provider for Cache components. So we'll cheat.
Consumer: (null: any),
Provider: (null: any),
_calculateChangedBits: null,
// We'll initialize these at the root.
_currentValue: (null: any),
_currentValue2: (null: any),
_threadCount: 0,
}
: (null: any);
if (__DEV__ && enableCache) {
CacheContext._currentRenderer = null;
CacheContext._currentRenderer2 = null;
}
// The cache that newly mounted Cache boundaries should use. It's either
// retrieved from the cache pool, or the result of a refresh.
let pooledCache: Cache | null = null;
// When retrying a Suspense/Offscreen boundary, we override pooledCache with the
// cache from the render that suspended.
const prevFreshCacheOnStack: StackCursor<Cache | null> = createCursor(null);
export function pushCacheProvider(workInProgress: Fiber, cache: Cache) {
if (!enableCache) {
return;
}
pushProvider(workInProgress, CacheContext, cache);
}
export function popCacheProvider(workInProgress: Fiber, cache: Cache) {
if (!enableCache) {
return;
}
popProvider(CacheContext, workInProgress);
}
export function requestCacheFromPool(renderLanes: Lanes): Cache {
if (!enableCache) {
return (null: any);
}
if (pooledCache !== null) {
return pooledCache;
}
// Create a fresh cache.
pooledCache = new Map();
return pooledCache;
}
export function pushRootCachePool(root: FiberRoot) {
if (!enableCache) {
return;
}
// When we start rendering a tree, read the pooled cache for this render
// 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;
}
export function popRootCachePool(root: FiberRoot, renderLanes: Lanes) {
if (!enableCache) {
return;
}
// The `pooledCache` variable points to the cache that was used for new
// cache boundaries during this render, if any. Stash it on the root so that
// parallel transitions may share the same cache. We will clear this field
// once all the transitions that depend on it (which we track with
// `pooledCacheLanes`) have committed.
root.pooledCache = pooledCache;
if (pooledCache !== null) {
root.pooledCacheLanes |= renderLanes;
}
}
export function restoreSpawnedCachePool(
offscreenWorkInProgress: Fiber,
prevCachePool: SpawnedCachePool,
): SpawnedCachePool | null {
if (!enableCache) {
return (null: any);
}
const nextParentCache = isPrimaryRenderer
? CacheContext._currentValue
: CacheContext._currentValue2;
if (nextParentCache !== prevCachePool.parent) {
// There was a refresh. Don't bother restoring anything since the refresh
// will override it.
return null;
} else {
// No refresh. Resume with the previous cache. This will override the cache
// pool so that any new Cache boundaries in the subtree use this one instead
// of requesting a fresh one.
push(prevFreshCacheOnStack, pooledCache, offscreenWorkInProgress);
pooledCache = prevCachePool.pool;
// Return the cache pool to signal that we did in fact push it. We will
// assign this to the field on the fiber so we know to pop the context.
return prevCachePool;
}
}
// Note: Ideally, `popCachePool` would return this value, and then we would pass
// it to `getSuspendedCachePool`. But factoring reasons, those two functions are
// in different phases/files. They are always called in sequence, though, so we
// can stash the value here temporarily.
let _suspendedPooledCache: Cache | null = null;
export function popCachePool(workInProgress: Fiber) {
if (!enableCache) {
return;
}
_suspendedPooledCache = pooledCache;
pooledCache = prevFreshCacheOnStack.current;
pop(prevFreshCacheOnStack, workInProgress);
}
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;
if (pool === null) {
// There's no pooled cache above us in the stack. However, a child in the
// suspended tree may have requested a fresh cache pool. If so, we would
// have unwound it with `popCachePool`.
if (_suspendedPooledCache !== null) {
pool = _suspendedPooledCache;
_suspendedPooledCache = null;
} else {
// There's no suspended cache pool.
return null;
}
}
return {
// We must also save the parent, so that when we resume we can detect
// a refresh.
parent: isPrimaryRenderer
? CacheContext._currentValue
: CacheContext._currentValue2,
pool,
};
}
export function getOffscreenDeferredCachePool(): SpawnedCachePool | null {
if (!enableCache) {
return null;
}
if (pooledCache === null) {
// There's no deferred cache pool.
return null;
}
return {
// We must also store the parent, so that when we resume we can detect
// a refresh.
parent: isPrimaryRenderer
? CacheContext._currentValue
: CacheContext._currentValue2,
pool: pooledCache,
};
}
@@ -0,0 +1,204 @@
/**
* Copyright (c) Facebook, Inc. and its affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
* @flow
*/
import type {ReactContext} from 'shared/ReactTypes';
import type {FiberRoot} from './ReactInternalTypes';
import type {Lanes} from './ReactFiberLane.old';
import type {StackCursor} from './ReactFiberStack.old';
import {enableCache} from 'shared/ReactFeatureFlags';
import {REACT_CONTEXT_TYPE} from 'shared/ReactSymbols';
import {isPrimaryRenderer} from './ReactFiberHostConfig';
import {createCursor, push, pop} from './ReactFiberStack.old';
import {pushProvider, popProvider} from './ReactFiberNewContext.old';
export type Cache = Map<() => mixed, mixed>;
export type CacheComponentState = {|
+parent: Cache,
+cache: Cache,
|};
export type SpawnedCachePool = {|
+parent: Cache,
+pool: Cache,
|};
export const CacheContext: ReactContext<Cache> = enableCache
? {
$$typeof: REACT_CONTEXT_TYPE,
// We don't use Consumer/Provider for Cache components. So we'll cheat.
Consumer: (null: any),
Provider: (null: any),
_calculateChangedBits: null,
// We'll initialize these at the root.
_currentValue: (null: any),
_currentValue2: (null: any),
_threadCount: 0,
}
: (null: any);
if (__DEV__ && enableCache) {
CacheContext._currentRenderer = null;
CacheContext._currentRenderer2 = null;
}
// The cache that newly mounted Cache boundaries should use. It's either
// retrieved from the cache pool, or the result of a refresh.
let pooledCache: Cache | null = null;
// When retrying a Suspense/Offscreen boundary, we override pooledCache with the
// cache from the render that suspended.
const prevFreshCacheOnStack: StackCursor<Cache | null> = createCursor(null);
export function pushCacheProvider(workInProgress: Fiber, cache: Cache) {
if (!enableCache) {
return;
}
pushProvider(workInProgress, CacheContext, cache);
}
export function popCacheProvider(workInProgress: Fiber, cache: Cache) {
if (!enableCache) {
return;
}
popProvider(CacheContext, workInProgress);
}
export function requestCacheFromPool(renderLanes: Lanes): Cache {
if (!enableCache) {
return (null: any);
}
if (pooledCache !== null) {
return pooledCache;
}
// Create a fresh cache.
pooledCache = new Map();
return pooledCache;
}
export function pushRootCachePool(root: FiberRoot) {
if (!enableCache) {
return;
}
// When we start rendering a tree, read the pooled cache for this render
// 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;
}
export function popRootCachePool(root: FiberRoot, renderLanes: Lanes) {
if (!enableCache) {
return;
}
// The `pooledCache` variable points to the cache that was used for new
// cache boundaries during this render, if any. Stash it on the root so that
// parallel transitions may share the same cache. We will clear this field
// once all the transitions that depend on it (which we track with
// `pooledCacheLanes`) have committed.
root.pooledCache = pooledCache;
if (pooledCache !== null) {
root.pooledCacheLanes |= renderLanes;
}
}
export function restoreSpawnedCachePool(
offscreenWorkInProgress: Fiber,
prevCachePool: SpawnedCachePool,
): SpawnedCachePool | null {
if (!enableCache) {
return (null: any);
}
const nextParentCache = isPrimaryRenderer
? CacheContext._currentValue
: CacheContext._currentValue2;
if (nextParentCache !== prevCachePool.parent) {
// There was a refresh. Don't bother restoring anything since the refresh
// will override it.
return null;
} else {
// No refresh. Resume with the previous cache. This will override the cache
// pool so that any new Cache boundaries in the subtree use this one instead
// of requesting a fresh one.
push(prevFreshCacheOnStack, pooledCache, offscreenWorkInProgress);
pooledCache = prevCachePool.pool;
// Return the cache pool to signal that we did in fact push it. We will
// assign this to the field on the fiber so we know to pop the context.
return prevCachePool;
}
}
// Note: Ideally, `popCachePool` would return this value, and then we would pass
// it to `getSuspendedCachePool`. But factoring reasons, those two functions are
// in different phases/files. They are always called in sequence, though, so we
// can stash the value here temporarily.
let _suspendedPooledCache: Cache | null = null;
export function popCachePool(workInProgress: Fiber) {
if (!enableCache) {
return;
}
_suspendedPooledCache = pooledCache;
pooledCache = prevFreshCacheOnStack.current;
pop(prevFreshCacheOnStack, workInProgress);
}
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;
if (pool === null) {
// There's no pooled cache above us in the stack. However, a child in the
// suspended tree may have requested a fresh cache pool. If so, we would
// have unwound it with `popCachePool`.
if (_suspendedPooledCache !== null) {
pool = _suspendedPooledCache;
_suspendedPooledCache = null;
} else {
// There's no suspended cache pool.
return null;
}
}
return {
// We must also save the parent, so that when we resume we can detect
// a refresh.
parent: isPrimaryRenderer
? CacheContext._currentValue
: CacheContext._currentValue2,
pool,
};
}
export function getOffscreenDeferredCachePool(): SpawnedCachePool | null {
if (!enableCache) {
return null;
}
if (pooledCache === null) {
// There's no deferred cache pool.
return null;
}
return {
// We must also store the parent, so that when we resume we can detect
// a refresh.
parent: isPrimaryRenderer
? CacheContext._currentValue
: CacheContext._currentValue2,
pool: pooledCache,
};
}
@@ -12,6 +12,7 @@ import type {Lanes, Lane} from './ReactFiberLane.new';
import type {
ReactFundamentalComponentInstance,
ReactScopeInstance,
ReactContext,
} from 'shared/ReactTypes';
import type {FiberRoot} from './ReactInternalTypes';
import type {
@@ -27,6 +28,7 @@ import type {
} from './ReactFiberSuspenseComponent.new';
import type {SuspenseContext} from './ReactFiberSuspenseContext.new';
import type {OffscreenState} from './ReactFiberOffscreenComponent';
import type {Cache, SpawnedCachePool} from './ReactFiberCacheComponent.new';
import {resetWorkInProgressVersions as resetMutableSourceWorkInProgressVersions} from './ReactMutableSource.new';
@@ -56,6 +58,7 @@ import {
ScopeComponent,
OffscreenComponent,
LegacyHiddenComponent,
CacheComponent,
} from './ReactWorkTags';
import {
NoMode,
@@ -132,6 +135,7 @@ import {
enableFundamentalAPI,
enableScopeAPI,
enableProfilerTimer,
enableCache,
} from 'shared/ReactFeatureFlags';
import {
markSpawnedWork,
@@ -153,6 +157,11 @@ import {
import {resetChildFibers} from './ReactChildFiber.new';
import {createScopeInstance} from './ReactFiberScope.new';
import {transferActualDuration} from './ReactProfilerTimer.new';
import {
popCacheProvider,
popRootCachePool,
popCachePool,
} from './ReactFiberCacheComponent.new';
function markUpdate(workInProgress: Fiber) {
// Tag the fiber with an update effect. This turns a Placement into
@@ -808,10 +817,16 @@ function completeWork(
return null;
}
case HostRoot: {
const fiberRoot = (workInProgress.stateNode: FiberRoot);
if (enableCache) {
popRootCachePool(fiberRoot, renderLanes);
const cache: Cache = workInProgress.memoizedState.cache;
popCacheProvider(workInProgress, cache);
}
popHostContainer(workInProgress);
popTopLevelLegacyContextObject(workInProgress);
resetMutableSourceWorkInProgressVersions();
const fiberRoot = (workInProgress.stateNode: FiberRoot);
if (fiberRoot.pendingContext) {
fiberRoot.context = fiberRoot.pendingContext;
fiberRoot.pendingContext = null;
@@ -1135,7 +1150,8 @@ function completeWork(
return null;
case ContextProvider:
// Pop provider fiber
popProvider(workInProgress);
const context: ReactContext<any> = workInProgress.type._context;
popProvider(context, workInProgress);
bubbleProperties(workInProgress);
return null;
case IncompleteClassComponent: {
@@ -1479,8 +1495,23 @@ function completeWork(
bubbleProperties(workInProgress);
}
if (enableCache) {
const spawnedCachePool: SpawnedCachePool | null = (workInProgress.updateQueue: any);
if (spawnedCachePool !== null) {
popCachePool(workInProgress);
}
}
return null;
}
case CacheComponent: {
if (enableCache) {
const cache: Cache = workInProgress.memoizedState.cache;
popCacheProvider(workInProgress, cache);
bubbleProperties(workInProgress);
return null;
}
}
}
invariant(
false,
@@ -12,6 +12,7 @@ import type {Lanes, Lane} from './ReactFiberLane.old';
import type {
ReactFundamentalComponentInstance,
ReactScopeInstance,
ReactContext,
} from 'shared/ReactTypes';
import type {FiberRoot} from './ReactInternalTypes';
import type {
@@ -27,6 +28,7 @@ import type {
} from './ReactFiberSuspenseComponent.old';
import type {SuspenseContext} from './ReactFiberSuspenseContext.old';
import type {OffscreenState} from './ReactFiberOffscreenComponent';
import type {Cache, SpawnedCachePool} from './ReactFiberCacheComponent.old';
import {resetWorkInProgressVersions as resetMutableSourceWorkInProgressVersions} from './ReactMutableSource.old';
@@ -56,6 +58,7 @@ import {
ScopeComponent,
OffscreenComponent,
LegacyHiddenComponent,
CacheComponent,
} from './ReactWorkTags';
import {
NoMode,
@@ -132,6 +135,7 @@ import {
enableFundamentalAPI,
enableScopeAPI,
enableProfilerTimer,
enableCache,
} from 'shared/ReactFeatureFlags';
import {
markSpawnedWork,
@@ -153,6 +157,11 @@ import {
import {resetChildFibers} from './ReactChildFiber.old';
import {createScopeInstance} from './ReactFiberScope.old';
import {transferActualDuration} from './ReactProfilerTimer.old';
import {
popCacheProvider,
popRootCachePool,
popCachePool,
} from './ReactFiberCacheComponent.old';
function markUpdate(workInProgress: Fiber) {
// Tag the fiber with an update effect. This turns a Placement into
@@ -808,10 +817,16 @@ function completeWork(
return null;
}
case HostRoot: {
const fiberRoot = (workInProgress.stateNode: FiberRoot);
if (enableCache) {
popRootCachePool(fiberRoot, renderLanes);
const cache: Cache = workInProgress.memoizedState.cache;
popCacheProvider(workInProgress, cache);
}
popHostContainer(workInProgress);
popTopLevelLegacyContextObject(workInProgress);
resetMutableSourceWorkInProgressVersions();
const fiberRoot = (workInProgress.stateNode: FiberRoot);
if (fiberRoot.pendingContext) {
fiberRoot.context = fiberRoot.pendingContext;
fiberRoot.pendingContext = null;
@@ -1135,7 +1150,8 @@ function completeWork(
return null;
case ContextProvider:
// Pop provider fiber
popProvider(workInProgress);
const context: ReactContext<any> = workInProgress.type._context;
popProvider(context, workInProgress);
bubbleProperties(workInProgress);
return null;
case IncompleteClassComponent: {
@@ -1479,8 +1495,23 @@ function completeWork(
bubbleProperties(workInProgress);
}
if (enableCache) {
const spawnedCachePool: SpawnedCachePool | null = (workInProgress.updateQueue: any);
if (spawnedCachePool !== null) {
popCachePool(workInProgress);
}
}
return null;
}
case CacheComponent: {
if (enableCache) {
const cache: Cache = workInProgress.memoizedState.cache;
popCacheProvider(workInProgress, cache);
bubbleProperties(workInProgress);
return null;
}
}
}
invariant(
false,
@@ -19,6 +19,7 @@ import type {HookFlags} from './ReactHookEffectTags';
import type {ReactPriorityLevel} from './ReactInternalTypes';
import type {FiberRoot} from './ReactInternalTypes';
import type {OpaqueIDType} from './ReactFiberHostConfig';
import type {Cache} from './ReactFiberCacheComponent.new';
import ReactSharedInternals from 'shared/ReactSharedInternals';
import {
@@ -46,6 +47,7 @@ import {
DefaultLanePriority,
} from './ReactFiberLane.new';
import {readContext} from './ReactFiberNewContext.new';
import {HostRoot, CacheComponent} from './ReactWorkTags';
import {
Update as UpdateEffect,
Passive as PassiveEffect,
@@ -92,6 +94,8 @@ import {
import {getIsRendering} from './ReactCurrentFiber';
import {logStateUpdateScheduled} from './DebugTracing';
import {markStateUpdateScheduled} from './SchedulingProfiler';
import {CacheContext} from './ReactFiberCacheComponent.new';
import {createUpdate, enqueueUpdate} from './ReactUpdateQueue.new';
const {ReactCurrentDispatcher, ReactCurrentBatchConfig} = ReactSharedInternals;
@@ -1707,6 +1711,55 @@ function rerenderOpaqueIdentifier(): OpaqueIDType | void {
return id;
}
function mountRefresh() {
const hook = mountWorkInProgressHook();
const refresh = (hook.memoizedState = refreshCache.bind(
null,
currentlyRenderingFiber,
));
return refresh;
}
function updateRefresh() {
const hook = updateWorkInProgressHook();
return hook.memoizedState;
}
function refreshCache<T>(fiber: Fiber, seedKey: ?() => T, seedValue: T) {
// 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.
let provider = fiber.return;
while (provider !== null) {
switch (provider.tag) {
case CacheComponent:
case HostRoot: {
const lane = requestUpdateLane(provider);
const eventTime = requestEventTime();
const root = scheduleUpdateOnFiber(provider, lane, eventTime);
const seededCache = new Map();
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);
}
// Schedule an update on the cache boundary to trigger a refresh.
const refreshUpdate = createUpdate(eventTime, lane);
const payload = {
cache: seededCache,
};
refreshUpdate.payload = payload;
enqueueUpdate(provider, refreshUpdate);
return;
}
}
provider = provider.return;
}
// TODO: Warn if unmounted?
}
function dispatchAction<S, A>(
fiber: Fiber,
queue: UpdateQueue<S, A>,
@@ -1818,7 +1871,16 @@ function dispatchAction<S, A>(
}
function getCacheForType<T>(resourceType: () => T): T {
invariant(false, 'Not implemented.');
if (!enableCache) {
invariant(false, 'Not implemented.');
}
const cache: Cache = readContext(CacheContext);
let cacheForType: T | void = (cache.get(resourceType): any);
if (cacheForType === undefined) {
cacheForType = resourceType();
cache.set(resourceType, cacheForType);
}
return cacheForType;
}
export const ContextOnlyDispatcher: Dispatcher = {
@@ -1843,6 +1905,7 @@ export const ContextOnlyDispatcher: Dispatcher = {
};
if (enableCache) {
(ContextOnlyDispatcher: Dispatcher).getCacheForType = getCacheForType;
(ContextOnlyDispatcher: Dispatcher).useCacheRefresh = throwInvalidHookError;
}
const HooksDispatcherOnMount: Dispatcher = {
@@ -1865,6 +1928,10 @@ const HooksDispatcherOnMount: Dispatcher = {
unstable_isNewReconciler: enableNewReconciler,
};
if (enableCache) {
(HooksDispatcherOnMount: Dispatcher).getCacheForType = getCacheForType;
(HooksDispatcherOnMount: Dispatcher).useCacheRefresh = mountRefresh;
}
const HooksDispatcherOnUpdate: Dispatcher = {
readContext,
@@ -1888,6 +1955,7 @@ const HooksDispatcherOnUpdate: Dispatcher = {
};
if (enableCache) {
(HooksDispatcherOnUpdate: Dispatcher).getCacheForType = getCacheForType;
(HooksDispatcherOnUpdate: Dispatcher).useCacheRefresh = updateRefresh;
}
const HooksDispatcherOnRerender: Dispatcher = {
@@ -1912,6 +1980,7 @@ const HooksDispatcherOnRerender: Dispatcher = {
};
if (enableCache) {
(HooksDispatcherOnRerender: Dispatcher).getCacheForType = getCacheForType;
(HooksDispatcherOnRerender: Dispatcher).useCacheRefresh = updateRefresh;
}
let HooksDispatcherOnMountInDEV: Dispatcher | null = null;
@@ -2069,6 +2138,11 @@ if (__DEV__) {
};
if (enableCache) {
(HooksDispatcherOnMountInDEV: Dispatcher).getCacheForType = getCacheForType;
(HooksDispatcherOnMountInDEV: Dispatcher).useCacheRefresh = function useCacheRefresh() {
currentHookNameInDev = 'useCacheRefresh';
mountHookTypesDev();
return mountRefresh();
};
}
HooksDispatcherOnMountWithHookTypesInDEV = {
@@ -2194,6 +2268,11 @@ if (__DEV__) {
};
if (enableCache) {
(HooksDispatcherOnMountWithHookTypesInDEV: Dispatcher).getCacheForType = getCacheForType;
(HooksDispatcherOnMountWithHookTypesInDEV: Dispatcher).useCacheRefresh = function useCacheRefresh() {
currentHookNameInDev = 'useCacheRefresh';
updateHookTypesDev();
return mountRefresh();
};
}
HooksDispatcherOnUpdateInDEV = {
@@ -2319,6 +2398,11 @@ if (__DEV__) {
};
if (enableCache) {
(HooksDispatcherOnUpdateInDEV: Dispatcher).getCacheForType = getCacheForType;
(HooksDispatcherOnUpdateInDEV: Dispatcher).useCacheRefresh = function useCacheRefresh() {
currentHookNameInDev = 'useCacheRefresh';
updateHookTypesDev();
return updateRefresh();
};
}
HooksDispatcherOnRerenderInDEV = {
@@ -2445,6 +2529,11 @@ if (__DEV__) {
};
if (enableCache) {
(HooksDispatcherOnRerenderInDEV: Dispatcher).getCacheForType = getCacheForType;
(HooksDispatcherOnRerenderInDEV: Dispatcher).useCacheRefresh = function useCacheRefresh() {
currentHookNameInDev = 'useCacheRefresh';
updateHookTypesDev();
return updateRefresh();
};
}
InvalidNestedHooksDispatcherOnMountInDEV = {
@@ -2585,6 +2674,11 @@ if (__DEV__) {
};
if (enableCache) {
(InvalidNestedHooksDispatcherOnMountInDEV: Dispatcher).getCacheForType = getCacheForType;
(InvalidNestedHooksDispatcherOnMountInDEV: Dispatcher).useCacheRefresh = function useCacheRefresh() {
currentHookNameInDev = 'useCacheRefresh';
updateHookTypesDev();
return mountRefresh();
};
}
InvalidNestedHooksDispatcherOnUpdateInDEV = {
@@ -2725,6 +2819,11 @@ if (__DEV__) {
};
if (enableCache) {
(InvalidNestedHooksDispatcherOnUpdateInDEV: Dispatcher).getCacheForType = getCacheForType;
(InvalidNestedHooksDispatcherOnUpdateInDEV: Dispatcher).useCacheRefresh = function useCacheRefresh() {
currentHookNameInDev = 'useCacheRefresh';
updateHookTypesDev();
return updateRefresh();
};
}
InvalidNestedHooksDispatcherOnRerenderInDEV = {
@@ -2866,5 +2965,10 @@ if (__DEV__) {
};
if (enableCache) {
(InvalidNestedHooksDispatcherOnRerenderInDEV: Dispatcher).getCacheForType = getCacheForType;
(InvalidNestedHooksDispatcherOnRerenderInDEV: Dispatcher).useCacheRefresh = function useCacheRefresh() {
currentHookNameInDev = 'useCacheRefresh';
updateHookTypesDev();
return updateRefresh();
};
}
}
@@ -19,6 +19,7 @@ import type {HookFlags} from './ReactHookEffectTags';
import type {ReactPriorityLevel} from './ReactInternalTypes';
import type {FiberRoot} from './ReactInternalTypes';
import type {OpaqueIDType} from './ReactFiberHostConfig';
import type {Cache} from './ReactFiberCacheComponent.old';
import ReactSharedInternals from 'shared/ReactSharedInternals';
import {
@@ -46,6 +47,7 @@ import {
DefaultLanePriority,
} from './ReactFiberLane.old';
import {readContext} from './ReactFiberNewContext.old';
import {HostRoot, CacheComponent} from './ReactWorkTags';
import {
Update as UpdateEffect,
Passive as PassiveEffect,
@@ -92,6 +94,8 @@ import {
import {getIsRendering} from './ReactCurrentFiber';
import {logStateUpdateScheduled} from './DebugTracing';
import {markStateUpdateScheduled} from './SchedulingProfiler';
import {CacheContext} from './ReactFiberCacheComponent.old';
import {createUpdate, enqueueUpdate} from './ReactUpdateQueue.old';
const {ReactCurrentDispatcher, ReactCurrentBatchConfig} = ReactSharedInternals;
@@ -1707,6 +1711,55 @@ function rerenderOpaqueIdentifier(): OpaqueIDType | void {
return id;
}
function mountRefresh() {
const hook = mountWorkInProgressHook();
const refresh = (hook.memoizedState = refreshCache.bind(
null,
currentlyRenderingFiber,
));
return refresh;
}
function updateRefresh() {
const hook = updateWorkInProgressHook();
return hook.memoizedState;
}
function refreshCache<T>(fiber: Fiber, seedKey: ?() => T, seedValue: T) {
// 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.
let provider = fiber.return;
while (provider !== null) {
switch (provider.tag) {
case CacheComponent:
case HostRoot: {
const lane = requestUpdateLane(provider);
const eventTime = requestEventTime();
const root = scheduleUpdateOnFiber(provider, lane, eventTime);
const seededCache = new Map();
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);
}
// Schedule an update on the cache boundary to trigger a refresh.
const refreshUpdate = createUpdate(eventTime, lane);
const payload = {
cache: seededCache,
};
refreshUpdate.payload = payload;
enqueueUpdate(provider, refreshUpdate);
return;
}
}
provider = provider.return;
}
// TODO: Warn if unmounted?
}
function dispatchAction<S, A>(
fiber: Fiber,
queue: UpdateQueue<S, A>,
@@ -1818,7 +1871,16 @@ function dispatchAction<S, A>(
}
function getCacheForType<T>(resourceType: () => T): T {
invariant(false, 'Not implemented.');
if (!enableCache) {
invariant(false, 'Not implemented.');
}
const cache: Cache = readContext(CacheContext);
let cacheForType: T | void = (cache.get(resourceType): any);
if (cacheForType === undefined) {
cacheForType = resourceType();
cache.set(resourceType, cacheForType);
}
return cacheForType;
}
export const ContextOnlyDispatcher: Dispatcher = {
@@ -1843,6 +1905,7 @@ export const ContextOnlyDispatcher: Dispatcher = {
};
if (enableCache) {
(ContextOnlyDispatcher: Dispatcher).getCacheForType = getCacheForType;
(ContextOnlyDispatcher: Dispatcher).useCacheRefresh = throwInvalidHookError;
}
const HooksDispatcherOnMount: Dispatcher = {
@@ -1865,6 +1928,10 @@ const HooksDispatcherOnMount: Dispatcher = {
unstable_isNewReconciler: enableNewReconciler,
};
if (enableCache) {
(HooksDispatcherOnMount: Dispatcher).getCacheForType = getCacheForType;
(HooksDispatcherOnMount: Dispatcher).useCacheRefresh = mountRefresh;
}
const HooksDispatcherOnUpdate: Dispatcher = {
readContext,
@@ -1888,6 +1955,7 @@ const HooksDispatcherOnUpdate: Dispatcher = {
};
if (enableCache) {
(HooksDispatcherOnUpdate: Dispatcher).getCacheForType = getCacheForType;
(HooksDispatcherOnUpdate: Dispatcher).useCacheRefresh = updateRefresh;
}
const HooksDispatcherOnRerender: Dispatcher = {
@@ -1912,6 +1980,7 @@ const HooksDispatcherOnRerender: Dispatcher = {
};
if (enableCache) {
(HooksDispatcherOnRerender: Dispatcher).getCacheForType = getCacheForType;
(HooksDispatcherOnRerender: Dispatcher).useCacheRefresh = updateRefresh;
}
let HooksDispatcherOnMountInDEV: Dispatcher | null = null;
@@ -2069,6 +2138,11 @@ if (__DEV__) {
};
if (enableCache) {
(HooksDispatcherOnMountInDEV: Dispatcher).getCacheForType = getCacheForType;
(HooksDispatcherOnMountInDEV: Dispatcher).useCacheRefresh = function useCacheRefresh() {
currentHookNameInDev = 'useCacheRefresh';
mountHookTypesDev();
return mountRefresh();
};
}
HooksDispatcherOnMountWithHookTypesInDEV = {
@@ -2194,6 +2268,11 @@ if (__DEV__) {
};
if (enableCache) {
(HooksDispatcherOnMountWithHookTypesInDEV: Dispatcher).getCacheForType = getCacheForType;
(HooksDispatcherOnMountWithHookTypesInDEV: Dispatcher).useCacheRefresh = function useCacheRefresh() {
currentHookNameInDev = 'useCacheRefresh';
updateHookTypesDev();
return mountRefresh();
};
}
HooksDispatcherOnUpdateInDEV = {
@@ -2319,6 +2398,11 @@ if (__DEV__) {
};
if (enableCache) {
(HooksDispatcherOnUpdateInDEV: Dispatcher).getCacheForType = getCacheForType;
(HooksDispatcherOnUpdateInDEV: Dispatcher).useCacheRefresh = function useCacheRefresh() {
currentHookNameInDev = 'useCacheRefresh';
updateHookTypesDev();
return updateRefresh();
};
}
HooksDispatcherOnRerenderInDEV = {
@@ -2445,6 +2529,11 @@ if (__DEV__) {
};
if (enableCache) {
(HooksDispatcherOnRerenderInDEV: Dispatcher).getCacheForType = getCacheForType;
(HooksDispatcherOnRerenderInDEV: Dispatcher).useCacheRefresh = function useCacheRefresh() {
currentHookNameInDev = 'useCacheRefresh';
updateHookTypesDev();
return updateRefresh();
};
}
InvalidNestedHooksDispatcherOnMountInDEV = {
@@ -2585,6 +2674,11 @@ if (__DEV__) {
};
if (enableCache) {
(InvalidNestedHooksDispatcherOnMountInDEV: Dispatcher).getCacheForType = getCacheForType;
(InvalidNestedHooksDispatcherOnMountInDEV: Dispatcher).useCacheRefresh = function useCacheRefresh() {
currentHookNameInDev = 'useCacheRefresh';
updateHookTypesDev();
return mountRefresh();
};
}
InvalidNestedHooksDispatcherOnUpdateInDEV = {
@@ -2725,6 +2819,11 @@ if (__DEV__) {
};
if (enableCache) {
(InvalidNestedHooksDispatcherOnUpdateInDEV: Dispatcher).getCacheForType = getCacheForType;
(InvalidNestedHooksDispatcherOnUpdateInDEV: Dispatcher).useCacheRefresh = function useCacheRefresh() {
currentHookNameInDev = 'useCacheRefresh';
updateHookTypesDev();
return updateRefresh();
};
}
InvalidNestedHooksDispatcherOnRerenderInDEV = {
@@ -2866,5 +2965,10 @@ if (__DEV__) {
};
if (enableCache) {
(InvalidNestedHooksDispatcherOnRerenderInDEV: Dispatcher).getCacheForType = getCacheForType;
(InvalidNestedHooksDispatcherOnRerenderInDEV: Dispatcher).useCacheRefresh = function useCacheRefresh() {
currentHookNameInDev = 'useCacheRefresh';
updateHookTypesDev();
return updateRefresh();
};
}
}
@@ -36,6 +36,7 @@ export type Lane = number;
export type LaneMap<T> = Array<T>;
import invariant from 'shared/invariant';
import {enableCache} from 'shared/ReactFeatureFlags';
import {
ImmediatePriority as ImmediateSchedulerPriority,
@@ -737,6 +738,15 @@ 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;
@@ -36,6 +36,7 @@ export type Lane = number;
export type LaneMap<T> = Array<T>;
import invariant from 'shared/invariant';
import {enableCache} from 'shared/ReactFeatureFlags';
import {
ImmediatePriority as ImmediateSchedulerPriority,
@@ -737,6 +738,15 @@ 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;
@@ -72,9 +72,11 @@ export function exitDisallowedContextReadInDEV(): void {
}
}
export function pushProvider<T>(providerFiber: Fiber, nextValue: T): void {
const context: ReactContext<T> = providerFiber.type._context;
export function pushProvider<T>(
providerFiber: Fiber,
context: ReactContext<T>,
nextValue: T,
): void {
if (isPrimaryRenderer) {
push(valueCursor, context._currentValue, providerFiber);
@@ -112,12 +114,12 @@ export function pushProvider<T>(providerFiber: Fiber, nextValue: T): void {
}
}
export function popProvider(providerFiber: Fiber): void {
export function popProvider(
context: ReactContext<any>,
providerFiber: Fiber,
): void {
const currentValue = valueCursor.current;
pop(valueCursor, providerFiber);
const context: ReactContext<any> = providerFiber.type._context;
if (isPrimaryRenderer) {
context._currentValue = currentValue;
} else {
@@ -179,9 +181,9 @@ export function scheduleWorkOnParentPath(
}
}
export function propagateContextChange(
export function propagateContextChange<T>(
workInProgress: Fiber,
context: ReactContext<mixed>,
context: ReactContext<T>,
changedBits: number,
renderLanes: Lanes,
): void {
@@ -72,9 +72,11 @@ export function exitDisallowedContextReadInDEV(): void {
}
}
export function pushProvider<T>(providerFiber: Fiber, nextValue: T): void {
const context: ReactContext<T> = providerFiber.type._context;
export function pushProvider<T>(
providerFiber: Fiber,
context: ReactContext<T>,
nextValue: T,
): void {
if (isPrimaryRenderer) {
push(valueCursor, context._currentValue, providerFiber);
@@ -112,12 +114,12 @@ export function pushProvider<T>(providerFiber: Fiber, nextValue: T): void {
}
}
export function popProvider(providerFiber: Fiber): void {
export function popProvider(
context: ReactContext<any>,
providerFiber: Fiber,
): void {
const currentValue = valueCursor.current;
pop(valueCursor, providerFiber);
const context: ReactContext<any> = providerFiber.type._context;
if (isPrimaryRenderer) {
context._currentValue = currentValue;
} else {
@@ -179,9 +181,9 @@ export function scheduleWorkOnParentPath(
}
}
export function propagateContextChange(
export function propagateContextChange<T>(
workInProgress: Fiber,
context: ReactContext<mixed>,
context: ReactContext<T>,
changedBits: number,
renderLanes: Lanes,
): void {
@@ -9,6 +9,7 @@
import type {ReactNodeList} from 'shared/ReactTypes';
import type {Lanes} from './ReactFiberLane.old';
import type {SpawnedCachePool} from './ReactFiberCacheComponent.new';
export type OffscreenProps = {|
// TODO: Pick an API before exposing the Offscreen type. I've chosen an enum
@@ -28,4 +29,5 @@ export type OffscreenState = {|
// will represent the pending work that must be included in the render in
// order to unhide the component.
baseLanes: Lanes,
cachePool: SpawnedCachePool | null,
|};
@@ -21,6 +21,7 @@ import {
import {
enableSchedulerTracing,
enableSuspenseCallback,
enableCache,
} from 'shared/ReactFeatureFlags';
import {unstable_getThreadID} from 'scheduler/tracing';
import {initializeUpdateQueue} from './ReactUpdateQueue.new';
@@ -52,6 +53,11 @@ function FiberRootNode(containerInfo, tag, hydrate) {
this.entangledLanes = NoLanes;
this.entanglements = createLaneMap(NoLanes);
if (enableCache) {
this.pooledCache = null;
this.pooledCacheLanes = NoLanes;
}
if (supportsHydration) {
this.mutableSourceEagerHydrationData = null;
}
@@ -97,6 +103,21 @@ export function createFiberRoot(
root.current = uninitializedFiber;
uninitializedFiber.stateNode = root;
if (enableCache) {
const initialCache = new Map();
root.pooledCache = initialCache;
const initialState = {
element: null,
cache: initialCache,
};
uninitializedFiber.memoizedState = initialState;
} else {
const initialState = {
element: null,
};
uninitializedFiber.memoizedState = initialState;
}
initializeUpdateQueue(uninitializedFiber);
return root;
@@ -21,6 +21,7 @@ import {
import {
enableSchedulerTracing,
enableSuspenseCallback,
enableCache,
} from 'shared/ReactFeatureFlags';
import {unstable_getThreadID} from 'scheduler/tracing';
import {initializeUpdateQueue} from './ReactUpdateQueue.old';
@@ -52,6 +53,11 @@ function FiberRootNode(containerInfo, tag, hydrate) {
this.entangledLanes = NoLanes;
this.entanglements = createLaneMap(NoLanes);
if (enableCache) {
this.pooledCache = null;
this.pooledCacheLanes = NoLanes;
}
if (supportsHydration) {
this.mutableSourceEagerHydrationData = null;
}
@@ -97,6 +103,21 @@ export function createFiberRoot(
root.current = uninitializedFiber;
uninitializedFiber.stateNode = root;
if (enableCache) {
const initialCache = new Map();
root.pooledCache = initialCache;
const initialState = {
element: null,
cache: initialCache,
};
uninitializedFiber.memoizedState = initialState;
} else {
const initialState = {
element: null,
};
uninitializedFiber.memoizedState = initialState;
}
initializeUpdateQueue(uninitializedFiber);
return root;
@@ -21,6 +21,9 @@ import {
HostRoot,
SuspenseComponent,
IncompleteClassComponent,
FunctionComponent,
ForwardRef,
SimpleMemoComponent,
} from './ReactWorkTags';
import {
DidCapture,
@@ -209,9 +212,15 @@ function throwException(
markComponentSuspended(sourceFiber, wakeable);
}
if ((sourceFiber.mode & BlockingMode) === NoMode) {
// Reset the memoizedState to what it was before we attempted
// to render it.
// Reset the memoizedState to what it was before we attempted to render it.
// A legacy mode Suspense quirk, only relevant to hook components.
const tag = sourceFiber.tag;
if (
(sourceFiber.mode & BlockingMode) === NoMode &&
(tag === FunctionComponent ||
tag === ForwardRef ||
tag === SimpleMemoComponent)
) {
const currentSource = sourceFiber.alternate;
if (currentSource) {
sourceFiber.updateQueue = currentSource.updateQueue;
@@ -21,6 +21,9 @@ import {
HostRoot,
SuspenseComponent,
IncompleteClassComponent,
FunctionComponent,
ForwardRef,
SimpleMemoComponent,
} from './ReactWorkTags';
import {
DidCapture,
@@ -209,9 +212,15 @@ function throwException(
markComponentSuspended(sourceFiber, wakeable);
}
if ((sourceFiber.mode & BlockingMode) === NoMode) {
// Reset the memoizedState to what it was before we attempted
// to render it.
// Reset the memoizedState to what it was before we attempted to render it.
// A legacy mode Suspense quirk, only relevant to hook components.
const tag = sourceFiber.tag;
if (
(sourceFiber.mode & BlockingMode) === NoMode &&
(tag === FunctionComponent ||
tag === ForwardRef ||
tag === SimpleMemoComponent)
) {
const currentSource = sourceFiber.alternate;
if (currentSource) {
sourceFiber.updateQueue = currentSource.updateQueue;
@@ -7,9 +7,11 @@
* @flow
*/
import type {Fiber} from './ReactInternalTypes';
import type {ReactContext} from 'shared/ReactTypes';
import type {Fiber, FiberRoot} from './ReactInternalTypes';
import type {Lanes} from './ReactFiberLane.new';
import type {SuspenseState} from './ReactFiberSuspenseComponent.new';
import type {Cache, SpawnedCachePool} from './ReactFiberCacheComponent.new';
import {resetWorkInProgressVersions as resetMutableSourceWorkInProgressVersions} from './ReactMutableSource.new';
import {
@@ -22,12 +24,14 @@ import {
SuspenseListComponent,
OffscreenComponent,
LegacyHiddenComponent,
CacheComponent,
} from './ReactWorkTags';
import {DidCapture, NoFlags, ShouldCapture} from './ReactFiberFlags';
import {NoMode, ProfileMode} from './ReactTypeOfMode';
import {
enableSuspenseServerRenderer,
enableProfilerTimer,
enableCache,
} from 'shared/ReactFeatureFlags';
import {popHostContainer, popHostContext} from './ReactFiberHostContext.new';
@@ -40,6 +44,11 @@ import {
} from './ReactFiberContext.new';
import {popProvider} from './ReactFiberNewContext.new';
import {popRenderLanes} from './ReactFiberWorkLoop.new';
import {
popCacheProvider,
popRootCachePool,
popCachePool,
} from './ReactFiberCacheComponent.new';
import {transferActualDuration} from './ReactProfilerTimer.new';
import invariant from 'shared/invariant';
@@ -65,6 +74,13 @@ function unwindWork(workInProgress: Fiber, renderLanes: Lanes) {
return null;
}
case HostRoot: {
if (enableCache) {
const root: FiberRoot = workInProgress.stateNode;
popRootCachePool(root, renderLanes);
const cache: Cache = workInProgress.memoizedState.cache;
popCacheProvider(workInProgress, cache);
}
popHostContainer(workInProgress);
popTopLevelLegacyContextObject(workInProgress);
resetMutableSourceWorkInProgressVersions();
@@ -120,18 +136,31 @@ function unwindWork(workInProgress: Fiber, renderLanes: Lanes) {
popHostContainer(workInProgress);
return null;
case ContextProvider:
popProvider(workInProgress);
const context: ReactContext<any> = workInProgress.type._context;
popProvider(context, workInProgress);
return null;
case OffscreenComponent:
case LegacyHiddenComponent:
popRenderLanes(workInProgress);
if (enableCache) {
const spawnedCachePool: SpawnedCachePool | null = (workInProgress.updateQueue: any);
if (spawnedCachePool !== null) {
popCachePool(workInProgress);
}
}
return null;
case CacheComponent:
if (enableCache) {
const cache: Cache = workInProgress.memoizedState.cache;
popCacheProvider(workInProgress, cache);
}
return null;
default:
return null;
}
}
function unwindInterruptedWork(interruptedWork: Fiber) {
function unwindInterruptedWork(interruptedWork: Fiber, renderLanes: Lanes) {
switch (interruptedWork.tag) {
case ClassComponent: {
const childContextTypes = interruptedWork.type.childContextTypes;
@@ -141,6 +170,13 @@ function unwindInterruptedWork(interruptedWork: Fiber) {
break;
}
case HostRoot: {
if (enableCache) {
const root: FiberRoot = interruptedWork.stateNode;
popRootCachePool(root, renderLanes);
const cache: Cache = interruptedWork.memoizedState.cache;
popCacheProvider(interruptedWork, cache);
}
popHostContainer(interruptedWork);
popTopLevelLegacyContextObject(interruptedWork);
resetMutableSourceWorkInProgressVersions();
@@ -160,11 +196,25 @@ function unwindInterruptedWork(interruptedWork: Fiber) {
popSuspenseContext(interruptedWork);
break;
case ContextProvider:
popProvider(interruptedWork);
const context: ReactContext<any> = interruptedWork.type._context;
popProvider(context, interruptedWork);
break;
case OffscreenComponent:
case LegacyHiddenComponent:
popRenderLanes(interruptedWork);
if (enableCache) {
const spawnedCachePool: SpawnedCachePool | null = (interruptedWork.updateQueue: any);
if (spawnedCachePool !== null) {
popCachePool(interruptedWork);
}
}
break;
case CacheComponent:
if (enableCache) {
const cache: Cache = interruptedWork.memoizedState.cache;
popCacheProvider(interruptedWork, cache);
}
break;
default:
break;
@@ -7,9 +7,11 @@
* @flow
*/
import type {Fiber} from './ReactInternalTypes';
import type {ReactContext} from 'shared/ReactTypes';
import type {Fiber, FiberRoot} from './ReactInternalTypes';
import type {Lanes} from './ReactFiberLane.old';
import type {SuspenseState} from './ReactFiberSuspenseComponent.old';
import type {Cache, SpawnedCachePool} from './ReactFiberCacheComponent.old';
import {resetWorkInProgressVersions as resetMutableSourceWorkInProgressVersions} from './ReactMutableSource.old';
import {
@@ -22,12 +24,14 @@ import {
SuspenseListComponent,
OffscreenComponent,
LegacyHiddenComponent,
CacheComponent,
} from './ReactWorkTags';
import {DidCapture, NoFlags, ShouldCapture} from './ReactFiberFlags';
import {NoMode, ProfileMode} from './ReactTypeOfMode';
import {
enableSuspenseServerRenderer,
enableProfilerTimer,
enableCache,
} from 'shared/ReactFeatureFlags';
import {popHostContainer, popHostContext} from './ReactFiberHostContext.old';
@@ -40,6 +44,11 @@ import {
} from './ReactFiberContext.old';
import {popProvider} from './ReactFiberNewContext.old';
import {popRenderLanes} from './ReactFiberWorkLoop.old';
import {
popCacheProvider,
popRootCachePool,
popCachePool,
} from './ReactFiberCacheComponent.old';
import {transferActualDuration} from './ReactProfilerTimer.old';
import invariant from 'shared/invariant';
@@ -65,6 +74,13 @@ function unwindWork(workInProgress: Fiber, renderLanes: Lanes) {
return null;
}
case HostRoot: {
if (enableCache) {
const root: FiberRoot = workInProgress.stateNode;
popRootCachePool(root, renderLanes);
const cache: Cache = workInProgress.memoizedState.cache;
popCacheProvider(workInProgress, cache);
}
popHostContainer(workInProgress);
popTopLevelLegacyContextObject(workInProgress);
resetMutableSourceWorkInProgressVersions();
@@ -120,18 +136,31 @@ function unwindWork(workInProgress: Fiber, renderLanes: Lanes) {
popHostContainer(workInProgress);
return null;
case ContextProvider:
popProvider(workInProgress);
const context: ReactContext<any> = workInProgress.type._context;
popProvider(context, workInProgress);
return null;
case OffscreenComponent:
case LegacyHiddenComponent:
popRenderLanes(workInProgress);
if (enableCache) {
const spawnedCachePool: SpawnedCachePool | null = (workInProgress.updateQueue: any);
if (spawnedCachePool !== null) {
popCachePool(workInProgress);
}
}
return null;
case CacheComponent:
if (enableCache) {
const cache: Cache = workInProgress.memoizedState.cache;
popCacheProvider(workInProgress, cache);
}
return null;
default:
return null;
}
}
function unwindInterruptedWork(interruptedWork: Fiber) {
function unwindInterruptedWork(interruptedWork: Fiber, renderLanes: Lanes) {
switch (interruptedWork.tag) {
case ClassComponent: {
const childContextTypes = interruptedWork.type.childContextTypes;
@@ -141,6 +170,13 @@ function unwindInterruptedWork(interruptedWork: Fiber) {
break;
}
case HostRoot: {
if (enableCache) {
const root: FiberRoot = interruptedWork.stateNode;
popRootCachePool(root, renderLanes);
const cache: Cache = interruptedWork.memoizedState.cache;
popCacheProvider(interruptedWork, cache);
}
popHostContainer(interruptedWork);
popTopLevelLegacyContextObject(interruptedWork);
resetMutableSourceWorkInProgressVersions();
@@ -160,11 +196,25 @@ function unwindInterruptedWork(interruptedWork: Fiber) {
popSuspenseContext(interruptedWork);
break;
case ContextProvider:
popProvider(interruptedWork);
const context: ReactContext<any> = interruptedWork.type._context;
popProvider(context, interruptedWork);
break;
case OffscreenComponent:
case LegacyHiddenComponent:
popRenderLanes(interruptedWork);
if (enableCache) {
const spawnedCachePool: SpawnedCachePool | null = (interruptedWork.updateQueue: any);
if (spawnedCachePool !== null) {
popCachePool(interruptedWork);
}
}
break;
case CacheComponent:
if (enableCache) {
const cache: Cache = interruptedWork.memoizedState.cache;
popCacheProvider(interruptedWork, cache);
}
break;
default:
break;
@@ -531,7 +531,7 @@ export function scheduleUpdateOnFiber(
fiber: Fiber,
lane: Lane,
eventTime: number,
) {
): FiberRoot | null {
checkForNestedUpdates();
warnAboutRenderPhaseUpdatesInDEV(fiber);
@@ -653,6 +653,8 @@ export function scheduleUpdateOnFiber(
// the same root, then it's not a huge deal, we just might batch more stuff
// together more than necessary.
mostRecentlyUpdatedRoot = root;
return root;
}
// This is split into a separate function so we can mark a fiber with pending
@@ -1371,7 +1373,7 @@ function prepareFreshStack(root: FiberRoot, lanes: Lanes) {
if (workInProgress !== null) {
let interruptedWork = workInProgress.return;
while (interruptedWork !== null) {
unwindInterruptedWork(interruptedWork);
unwindInterruptedWork(interruptedWork, workInProgressRootRenderLanes);
interruptedWork = interruptedWork.return;
}
}
@@ -2303,7 +2305,10 @@ function commitBeforeMutationEffects() {
}
}
function commitMutationEffects(root: FiberRoot, renderPriorityLevel) {
function commitMutationEffects(
root: FiberRoot,
renderPriorityLevel: ReactPriorityLevel,
) {
// TODO: Should probably move the bulk of this function to commitWork.
while (nextEffect !== null) {
setCurrentDebugFiberInDEV(nextEffect);
@@ -3047,7 +3052,7 @@ if (__DEV__ && replayFailedUnitOfWorkWithInvokeGuardedCallback) {
// same fiber again.
// Unwind the failed stack frame
unwindInterruptedWork(unitOfWork);
unwindInterruptedWork(unitOfWork, workInProgressRootRenderLanes);
// Restore the original properties of the fiber.
assignFiberPropertiesInDEV(unitOfWork, originalWorkInProgressCopy);
@@ -531,7 +531,7 @@ export function scheduleUpdateOnFiber(
fiber: Fiber,
lane: Lane,
eventTime: number,
) {
): FiberRoot | null {
checkForNestedUpdates();
warnAboutRenderPhaseUpdatesInDEV(fiber);
@@ -653,6 +653,8 @@ export function scheduleUpdateOnFiber(
// the same root, then it's not a huge deal, we just might batch more stuff
// together more than necessary.
mostRecentlyUpdatedRoot = root;
return root;
}
// This is split into a separate function so we can mark a fiber with pending
@@ -1371,7 +1373,7 @@ function prepareFreshStack(root: FiberRoot, lanes: Lanes) {
if (workInProgress !== null) {
let interruptedWork = workInProgress.return;
while (interruptedWork !== null) {
unwindInterruptedWork(interruptedWork);
unwindInterruptedWork(interruptedWork, workInProgressRootRenderLanes);
interruptedWork = interruptedWork.return;
}
}
@@ -2303,7 +2305,10 @@ function commitBeforeMutationEffects() {
}
}
function commitMutationEffects(root: FiberRoot, renderPriorityLevel) {
function commitMutationEffects(
root: FiberRoot,
renderPriorityLevel: ReactPriorityLevel,
) {
// TODO: Should probably move the bulk of this function to commitWork.
while (nextEffect !== null) {
setCurrentDebugFiberInDEV(nextEffect);
@@ -3047,7 +3052,7 @@ if (__DEV__ && replayFailedUnitOfWorkWithInvokeGuardedCallback) {
// same fiber again.
// Unwind the failed stack frame
unwindInterruptedWork(unitOfWork);
unwindInterruptedWork(unitOfWork, workInProgressRootRenderLanes);
// Restore the original properties of the fiber.
assignFiberPropertiesInDEV(unitOfWork, originalWorkInProgressCopy);
+7 -1
View File
@@ -25,6 +25,7 @@ import type {RootTag} from './ReactRootTags';
import type {TimeoutHandle, NoTimeout} from './ReactFiberHostConfig';
import type {Wakeable} from 'shared/ReactTypes';
import type {Interaction} from 'scheduler/src/Tracing';
import type {Cache} from './ReactFiberCacheComponent.old';
// Unwind Circular: moved from ReactFiberHooks.old
export type HookType =
@@ -41,7 +42,8 @@ export type HookType =
| 'useDeferredValue'
| 'useTransition'
| 'useMutableSource'
| 'useOpaqueIdentifier';
| 'useOpaqueIdentifier'
| 'useCacheRefresh';
export type ReactPriorityLevel = 99 | 98 | 97 | 96 | 95 | 90;
@@ -235,6 +237,9 @@ type BaseFiberRootProperties = {|
entangledLanes: Lanes,
entanglements: LaneMap<Lanes>,
pooledCache: Cache | null,
pooledCacheLanes: Lanes,
|};
// The following attributes are only used by interaction tracing builds.
@@ -314,6 +319,7 @@ export type Dispatcher = {|
subscribe: MutableSourceSubscribeFn<Source, Snapshot>,
): Snapshot,
useOpaqueIdentifier(): any,
useCacheRefresh?: () => <T>(?() => T, ?T) => void,
unstable_isNewReconciler?: boolean,
|};
+3 -1
View File
@@ -31,7 +31,8 @@ export type WorkTag =
| 20
| 21
| 22
| 23;
| 23
| 24;
export const FunctionComponent = 0;
export const ClassComponent = 1;
@@ -57,3 +58,4 @@ export const FundamentalComponent = 20;
export const ScopeComponent = 21;
export const OffscreenComponent = 22;
export const LegacyHiddenComponent = 23;
export const CacheComponent = 24;
@@ -0,0 +1,916 @@
let React;
let ReactNoop;
let Cache;
let getCacheForType;
let Scheduler;
let Suspense;
let useCacheRefresh;
let startTransition;
let useState;
let textService;
let textServiceVersion;
describe('ReactCache', () => {
beforeEach(() => {
jest.resetModules();
React = require('react');
ReactNoop = require('react-noop-renderer');
Cache = React.unstable_Cache;
Scheduler = require('scheduler');
Suspense = React.Suspense;
getCacheForType = React.unstable_getCacheForType;
useCacheRefresh = React.unstable_useCacheRefresh;
startTransition = React.unstable_startTransition;
useState = React.useState;
// Represents some data service that returns text. It likely has additional
// caching layers, like a CDN or the local browser cache. It can be mutated
// or emptied independently of the React cache.
textService = new Map();
textServiceVersion = 1;
});
function createTextCache() {
return new Map();
}
function readText(text) {
const textCache = getCacheForType(createTextCache);
const record = textCache.get(text);
if (record !== undefined) {
switch (record.status) {
case 'pending':
throw record.value;
case 'rejected':
throw record.value;
case 'resolved':
return record.value;
}
} else {
Scheduler.unstable_yieldValue(`Cache miss! [${text}]`);
let request = textService.get(text);
if (request === undefined) {
let resolve;
let reject;
request = new Promise((res, rej) => {
resolve = res;
reject = rej;
});
request.resolve = resolve;
request.reject = reject;
// Add the request to a backing cache. This may outlive the lifetime
// of the component that is currently reading the data.
textService.set(text, request);
}
const thenable = request.then(
value => {
if (newRecord.status === 'pending') {
newRecord.status = 'resolved';
newRecord.value = value;
}
},
error => {
if (newRecord.status === 'pending') {
newRecord.status = 'rejected';
newRecord.value = error;
}
},
);
const newRecord = {
ping: null,
status: 'pending',
value: thenable,
};
textCache.set(text, newRecord);
throw thenable;
}
}
function mutateRemoteTextService() {
textService = new Map();
textServiceVersion++;
}
function resolveText(text) {
const request = textService.get(text);
if (request !== undefined) {
request.resolve(textServiceVersion);
return request;
} else {
const newRequest = Promise.resolve(textServiceVersion);
newRequest.resolve = newRequest.reject = () => {};
textService.set(text, newRequest);
return newRequest;
}
}
function Text({text}) {
Scheduler.unstable_yieldValue(text);
return text;
}
function AsyncText({text, showVersion}) {
const version = readText(text);
const fullText = showVersion ? `${text} [v${version}]` : text;
Scheduler.unstable_yieldValue(fullText);
return fullText;
}
// @gate experimental
test('render Cache component', async () => {
const root = ReactNoop.createRoot();
await ReactNoop.act(async () => {
root.render(<Cache>Hi</Cache>);
});
expect(root).toMatchRenderedOutput('Hi');
});
// @gate experimental
test('mount new data', async () => {
const root = ReactNoop.createRoot();
await ReactNoop.act(async () => {
root.render(
<Cache>
<Suspense fallback={<Text text="Loading..." />}>
<AsyncText text="A" />
</Suspense>
</Cache>,
);
});
expect(Scheduler).toHaveYielded(['Cache miss! [A]', 'Loading...']);
expect(root).toMatchRenderedOutput('Loading...');
await ReactNoop.act(async () => {
await resolveText('A');
});
expect(Scheduler).toHaveYielded(['A']);
expect(root).toMatchRenderedOutput('A');
});
// @gate experimental
test('root acts as implicit cache boundary', async () => {
const root = ReactNoop.createRoot();
await ReactNoop.act(async () => {
root.render(
<Suspense fallback={<Text text="Loading..." />}>
<AsyncText text="A" />
</Suspense>,
);
});
expect(Scheduler).toHaveYielded(['Cache miss! [A]', 'Loading...']);
expect(root).toMatchRenderedOutput('Loading...');
await ReactNoop.act(async () => {
await resolveText('A');
});
expect(Scheduler).toHaveYielded(['A']);
expect(root).toMatchRenderedOutput('A');
});
// @gate experimental
test('multiple new Cache boundaries in the same update share the same, fresh cache', async () => {
function App({text}) {
return (
<>
<Cache>
<Suspense fallback={<Text text="Loading..." />}>
<AsyncText text="A" />
</Suspense>
</Cache>
<Cache>
<Suspense fallback={<Text text="Loading..." />}>
<AsyncText text="A" />
</Suspense>
</Cache>
</>
);
}
const root = ReactNoop.createRoot();
await ReactNoop.act(async () => {
root.render(<App showMore={false} />);
});
// 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([
'Cache miss! [A]',
'Loading...',
'Loading...',
]);
expect(root).toMatchRenderedOutput('Loading...Loading...');
await ReactNoop.act(async () => {
await resolveText('A');
});
expect(Scheduler).toHaveYielded(['A', 'A']);
expect(root).toMatchRenderedOutput('AA');
});
// @gate experimental
test(
'nested cache boundaries share the same cache as the root during ' +
'the initial render',
async () => {
function App() {
return (
<Suspense fallback={<Text text="Loading..." />}>
<AsyncText text="A" />
<Cache>
<AsyncText text="A" />
</Cache>
</Suspense>
);
}
const root = ReactNoop.createRoot();
await ReactNoop.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.
expect(Scheduler).toHaveYielded(['Cache miss! [A]', 'Loading...']);
expect(root).toMatchRenderedOutput('Loading...');
await ReactNoop.act(async () => {
await resolveText('A');
});
expect(Scheduler).toHaveYielded(['A', 'A']);
expect(root).toMatchRenderedOutput('AA');
},
);
// @gate experimental
test('new content inside an existing Cache boundary should re-use already cached data', async () => {
function App({showMore}) {
return (
<Cache>
<Suspense fallback={<Text text="Loading..." />}>
<AsyncText showVersion={true} text="A" />
</Suspense>
{showMore ? (
<Suspense fallback={<Text text="Loading..." />}>
<AsyncText showVersion={true} text="A" />
</Suspense>
) : null}
</Cache>
);
}
const root = ReactNoop.createRoot();
await ReactNoop.act(async () => {
await resolveText('A');
root.render(<App showMore={false} />);
});
expect(Scheduler).toHaveYielded([
'Cache miss! [A]',
'Loading...',
'A [v1]',
]);
expect(root).toMatchRenderedOutput('A [v1]');
// Simulate a server mutation.
mutateRemoteTextService();
// Add a new cache boundary
await ReactNoop.act(async () => {
await resolveText('A');
root.render(<App showMore={true} />);
});
expect(Scheduler).toHaveYielded([
'A [v1]',
// New tree should use already cached data
'A [v1]',
]);
expect(root).toMatchRenderedOutput('A [v1]A [v1]');
});
// @gate experimental
test('a new Cache boundary uses fresh cache', async () => {
// The only difference from the previous test is that the "Show More"
// content is wrapped in a nested <Cache /> boundary
function App({showMore}) {
return (
<Cache>
<Suspense fallback={<Text text="Loading..." />}>
<AsyncText showVersion={true} text="A" />
</Suspense>
{showMore ? (
<Cache>
<Suspense fallback={<Text text="Loading..." />}>
<AsyncText showVersion={true} text="A" />
</Suspense>
</Cache>
) : null}
</Cache>
);
}
const root = ReactNoop.createRoot();
await ReactNoop.act(async () => {
await resolveText('A');
root.render(<App showMore={false} />);
});
expect(Scheduler).toHaveYielded([
'Cache miss! [A]',
'Loading...',
'A [v1]',
]);
expect(root).toMatchRenderedOutput('A [v1]');
// Simulate a server mutation.
mutateRemoteTextService();
// Add a new cache boundary
await ReactNoop.act(async () => {
await resolveText('A');
root.render(<App showMore={true} />);
});
expect(Scheduler).toHaveYielded([
'A [v1]',
// New tree should load fresh data.
'Cache miss! [A]',
'Loading...',
'A [v2]',
]);
expect(root).toMatchRenderedOutput('A [v1]A [v2]');
});
// @gate experimental
test('inner content uses same cache as shell if spawned by the same transition', async () => {
const root = ReactNoop.createRoot();
function App() {
return (
<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>
);
}
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 ReactNoop.act(async () => {
root.render(<App />);
});
expect(Scheduler).toHaveYielded(['Cache miss! [A]', 'Loading shell...']);
expect(root).toMatchRenderedOutput('Loading shell...');
await ReactNoop.act(async () => {
await resolveText('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 ReactNoop.act(async () => {
await resolveText('B');
});
expect(Scheduler).toHaveYielded(['Content']);
expect(root).toMatchRenderedOutput(
<>
<div>Shell</div>
<div>Content</div>
</>,
);
});
// @gate experimental
test('refresh a cache', async () => {
let refresh;
function App() {
refresh = useCacheRefresh();
return <AsyncText showVersion={true} text="A" />;
}
// Mount initial data
const root = ReactNoop.createRoot();
await ReactNoop.act(async () => {
root.render(
<Cache>
<Suspense fallback={<Text text="Loading..." />}>
<App />
</Suspense>
</Cache>,
);
});
expect(Scheduler).toHaveYielded(['Cache miss! [A]', 'Loading...']);
expect(root).toMatchRenderedOutput('Loading...');
await ReactNoop.act(async () => {
await resolveText('A');
});
expect(Scheduler).toHaveYielded(['A [v1]']);
expect(root).toMatchRenderedOutput('A [v1]');
// Mutate the text service, then refresh for new data.
mutateRemoteTextService();
await ReactNoop.act(async () => {
startTransition(() => refresh());
});
expect(Scheduler).toHaveYielded(['Cache miss! [A]', 'Loading...']);
expect(root).toMatchRenderedOutput('A [v1]');
await ReactNoop.act(async () => {
await resolveText('A');
});
// Note that the version has updated
expect(Scheduler).toHaveYielded(['A [v2]']);
expect(root).toMatchRenderedOutput('A [v2]');
});
// @gate experimental
test('refresh the root cache', async () => {
let refresh;
function App() {
refresh = useCacheRefresh();
return <AsyncText showVersion={true} text="A" />;
}
// Mount initial data
const root = ReactNoop.createRoot();
await ReactNoop.act(async () => {
root.render(
<Suspense fallback={<Text text="Loading..." />}>
<App />
</Suspense>,
);
});
expect(Scheduler).toHaveYielded(['Cache miss! [A]', 'Loading...']);
expect(root).toMatchRenderedOutput('Loading...');
await ReactNoop.act(async () => {
await resolveText('A');
});
expect(Scheduler).toHaveYielded(['A [v1]']);
expect(root).toMatchRenderedOutput('A [v1]');
// Mutate the text service, then refresh for new data.
mutateRemoteTextService();
await ReactNoop.act(async () => {
startTransition(() => refresh());
});
expect(Scheduler).toHaveYielded(['Cache miss! [A]', 'Loading...']);
expect(root).toMatchRenderedOutput('A [v1]');
await ReactNoop.act(async () => {
await resolveText('A');
});
// Note that the version has updated
expect(Scheduler).toHaveYielded(['A [v2]']);
expect(root).toMatchRenderedOutput('A [v2]');
});
// @gate experimental
test('refresh a cache with seed data', async () => {
let refresh;
function App() {
refresh = useCacheRefresh();
return <AsyncText showVersion={true} text="A" />;
}
// Mount initial data
const root = ReactNoop.createRoot();
await ReactNoop.act(async () => {
root.render(
<Cache>
<Suspense fallback={<Text text="Loading..." />}>
<App />
</Suspense>
</Cache>,
);
});
expect(Scheduler).toHaveYielded(['Cache miss! [A]', 'Loading...']);
expect(root).toMatchRenderedOutput('Loading...');
await ReactNoop.act(async () => {
await resolveText('A');
});
expect(Scheduler).toHaveYielded(['A [v1]']);
expect(root).toMatchRenderedOutput('A [v1]');
// Mutate the text service, then refresh for new data.
mutateRemoteTextService();
await ReactNoop.act(async () => {
// Refresh the cache with seeded data, like you would receive from a
// server mutation.
// TODO: Seeding multiple typed caches. Should work by calling `refresh`
// multiple times with different key/value pairs
const seededCache = new Map();
seededCache.set('A', {
ping: null,
status: 'resolved',
value: textServiceVersion,
});
startTransition(() => refresh(createTextCache, seededCache));
});
// The root should re-render without a cache miss.
expect(Scheduler).toHaveYielded(['A [v2]']);
expect(root).toMatchRenderedOutput('A [v2]');
});
// @gate experimental
test('refreshing a parent cache also refreshes its children', async () => {
let refreshShell;
function RefreshShell() {
refreshShell = useCacheRefresh();
return null;
}
function App({showMore}) {
return (
<Cache>
<RefreshShell />
<Suspense fallback={<Text text="Loading..." />}>
<AsyncText showVersion={true} text="A" />
</Suspense>
{showMore ? (
<Cache>
<Suspense fallback={<Text text="Loading..." />}>
<AsyncText showVersion={true} text="A" />
</Suspense>
</Cache>
) : null}
</Cache>
);
}
const root = ReactNoop.createRoot();
await ReactNoop.act(async () => {
await resolveText('A');
root.render(<App showMore={false} />);
});
expect(Scheduler).toHaveYielded([
'Cache miss! [A]',
'Loading...',
'A [v1]',
]);
expect(root).toMatchRenderedOutput('A [v1]');
// Simulate a server mutation.
mutateRemoteTextService();
// Add a new cache boundary
await ReactNoop.act(async () => {
await resolveText('A');
root.render(<App showMore={true} />);
});
expect(Scheduler).toHaveYielded([
'A [v1]',
// New tree should load fresh data.
'Cache miss! [A]',
'Loading...',
'A [v2]',
]);
expect(root).toMatchRenderedOutput('A [v1]A [v2]');
// Now refresh the shell. This should also cause the "Show More" contents to
// refresh, since its cache is nested inside the outer one.
mutateRemoteTextService();
await ReactNoop.act(async () => {
startTransition(() => refreshShell());
});
expect(Scheduler).toHaveYielded([
'Cache miss! [A]',
'Loading...',
'Loading...',
]);
expect(root).toMatchRenderedOutput('A [v1]A [v2]');
await ReactNoop.act(async () => {
await resolveText('A');
});
expect(Scheduler).toHaveYielded(['A [v3]', 'A [v3]']);
expect(root).toMatchRenderedOutput('A [v3]A [v3]');
});
// @gate experimental
test(
'refreshing a cache boundary does not refresh the other boundaries ' +
'that mounted at the same time (i.e. the ones that share the same cache)',
async () => {
let refreshFirstBoundary;
function RefreshFirstBoundary() {
refreshFirstBoundary = useCacheRefresh();
return null;
}
function App({showMore}) {
return showMore ? (
<>
<Cache>
<Suspense fallback={<Text text="Loading..." />}>
<RefreshFirstBoundary />
<AsyncText showVersion={true} text="A" />
</Suspense>
</Cache>
<Cache>
<Suspense fallback={<Text text="Loading..." />}>
<AsyncText showVersion={true} text="A" />
</Suspense>
</Cache>
</>
) : null;
}
// First mount the initial shell without the nested boundaries. This is
// necessary for this test because we want the two inner boundaries to be
// treated like sibling providers that happen to share an underlying
// cache, as opposed to consumers of the root-level cache.
const root = ReactNoop.createRoot();
await ReactNoop.act(async () => {
root.render(<App showMore={false} />);
});
// Now reveal the boundaries. In a real app this would be a navigation.
await ReactNoop.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([
'Cache miss! [A]',
'Loading...',
'Loading...',
]);
expect(root).toMatchRenderedOutput('Loading...Loading...');
await ReactNoop.act(async () => {
await resolveText('A');
});
expect(Scheduler).toHaveYielded(['A [v1]', 'A [v1]']);
expect(root).toMatchRenderedOutput('A [v1]A [v1]');
// Refresh the first boundary. It should not refresh the second boundary,
// even though they previously shared the same underlying cache.
mutateRemoteTextService();
await ReactNoop.act(async () => {
await refreshFirstBoundary();
});
expect(Scheduler).toHaveYielded(['Cache miss! [A]', 'Loading...']);
await ReactNoop.act(async () => {
await resolveText('A');
});
expect(Scheduler).toHaveYielded(['A [v2]']);
expect(root).toMatchRenderedOutput('A [v2]A [v1]');
},
);
// @gate experimental
test(
'mount a new Cache boundary in a sibling while simultaneously ' +
'resolving a Suspense boundary',
async () => {
function App({showMore}) {
return (
<>
{showMore ? (
<Suspense fallback={<Text text="Loading..." />}>
<Cache>
<AsyncText showVersion={true} text="A" />
</Cache>
</Suspense>
) : null}
<Suspense fallback={<Text text="Loading..." />}>
<Cache>
{' '}
<AsyncText showVersion={true} text="A" />{' '}
<AsyncText showVersion={true} text="B" />
</Cache>
</Suspense>
</>
);
}
const root = ReactNoop.createRoot();
await ReactNoop.act(async () => {
root.render(<App showMore={false} />);
});
expect(Scheduler).toHaveYielded([
'Cache miss! [A]',
'Cache miss! [B]',
'Loading...',
]);
await ReactNoop.act(async () => {
// This will resolve the content in the first cache
resolveText('A');
resolveText('B');
// Now let's simulate a mutation
mutateRemoteTextService();
// And mount the second tree, which includes new content
root.render(<App showMore={true} />);
});
expect(Scheduler).toHaveYielded([
// The new tree should use a fresh cache
'Cache miss! [A]',
'Loading...',
// The other tree uses the cached responses. This demonstrates that the
// requests are not dropped.
'A [v1]',
'B [v1]',
]);
// Now resolve the second tree
await ReactNoop.act(async () => {
resolveText('A');
});
expect(Scheduler).toHaveYielded(['A [v2]']);
expect(root).toMatchRenderedOutput('A [v2] A [v1] B [v1]');
},
);
// @gate experimental
test('cache pool is cleared once transitions that depend on it commit their shell', async () => {
function Child({text}) {
return (
<Cache>
<AsyncText text={text} />
</Cache>
);
}
const root = ReactNoop.createRoot();
await ReactNoop.act(async () => {
root.render(
<Suspense fallback={<Text text="Loading..." />}>(empty)</Suspense>,
);
});
expect(Scheduler).toHaveYielded([]);
expect(root).toMatchRenderedOutput('(empty)');
await ReactNoop.act(async () => {
startTransition(() => {
root.render(
<Suspense fallback={<Text text="Loading..." />}>
<Child text="A" />
</Suspense>,
);
});
});
expect(Scheduler).toHaveYielded(['Cache miss! [A]', 'Loading...']);
expect(root).toMatchRenderedOutput('(empty)');
await ReactNoop.act(async () => {
startTransition(() => {
root.render(
<Suspense fallback={<Text text="Loading..." />}>
<Child text="A" />
<Child text="A" />
</Suspense>,
);
});
});
expect(Scheduler).toHaveYielded([
// No cache miss, because it uses the pooled cache
'Loading...',
]);
expect(root).toMatchRenderedOutput('(empty)');
// Resolve the request
await ReactNoop.act(async () => {
await resolveText('A');
});
expect(Scheduler).toHaveYielded(['A', 'A']);
expect(root).toMatchRenderedOutput('AA');
// Now do another transition
await ReactNoop.act(async () => {
startTransition(() => {
root.render(
<Suspense fallback={<Text text="Loading..." />}>
<Child text="A" />
<Child text="A" />
<Child text="A" />
</Suspense>,
);
});
});
expect(Scheduler).toHaveYielded([
// First two children use the old cache because they already finished
'A',
'A',
// The new child uses a fresh cache
'Cache miss! [A]',
'Loading...',
'A',
'A',
'A',
]);
expect(root).toMatchRenderedOutput('AAA');
});
// @gate experimental
test('cache pool is not cleared by arbitrary commits', async () => {
function App() {
return (
<>
<ShowMore />
<Unrelated />
</>
);
}
let showMore;
function ShowMore() {
const [shouldShow, _showMore] = useState(false);
showMore = () => _showMore(true);
return (
<>
<Suspense fallback={<Text text="Loading..." />}>
{shouldShow ? (
<Cache>
<AsyncText text="A" />
</Cache>
) : null}
</Suspense>
</>
);
}
let updateUnrelated;
function Unrelated() {
const [count, _updateUnrelated] = useState(0);
updateUnrelated = _updateUnrelated;
return <Text text={count + ''} />;
}
const root = ReactNoop.createRoot();
await ReactNoop.act(async () => {
root.render(<App showMore={false} />);
});
expect(Scheduler).toHaveYielded(['0']);
expect(root).toMatchRenderedOutput('0');
await ReactNoop.act(async () => {
startTransition(() => {
showMore();
});
});
expect(Scheduler).toHaveYielded(['Cache miss! [A]', 'Loading...']);
expect(root).toMatchRenderedOutput('0');
await ReactNoop.act(async () => {
updateUnrelated(1);
});
expect(Scheduler).toHaveYielded([
'1',
// Happens to re-render the fallback. Doesn't need to, but not relevant
// to this test.
'Loading...',
]);
expect(root).toMatchRenderedOutput('1');
await ReactNoop.act(async () => {
resolveText('A');
mutateRemoteTextService();
});
expect(Scheduler).toHaveYielded(['A']);
expect(root).toMatchRenderedOutput('A1');
});
});
+10
View File
@@ -758,6 +758,13 @@ function unsupportedHook(): void {
invariant(false, 'This Hook is not supported in Server Components.');
}
function unsupportedRefresh(): void {
invariant(
currentCache,
'Refreshing the cache is not supported in Server Components.',
);
}
let currentCache: Map<Function, mixed> | null = null;
const Dispatcher: DispatcherType = {
@@ -797,4 +804,7 @@ const Dispatcher: DispatcherType = {
useEffect: (unsupportedHook: any),
useOpaqueIdentifier: (unsupportedHook: any),
useMutableSource: (unsupportedHook: any),
useCacheRefresh(): <T>(?() => T, ?T) => void {
return unsupportedRefresh;
},
};
@@ -44,6 +44,7 @@ export function waitForSuspense<T>(fn: () => T): Promise<T> {
useTransition: unsupported,
useOpaqueIdentifier: unsupported,
useMutableSource: unsupported,
useCacheRefresh: unsupported,
};
// Not using async/await because we don't compile it.
return new Promise((resolve, reject) => {
+2
View File
@@ -51,6 +51,8 @@ export {
SuspenseList,
SuspenseList as unstable_SuspenseList,
unstable_getCacheForType,
unstable_Cache,
unstable_useCacheRefresh,
// enableScopeAPI
unstable_Scope,
unstable_useOpaqueIdentifier,
+2
View File
@@ -46,6 +46,8 @@ export {
SuspenseList as unstable_SuspenseList,
unstable_useOpaqueIdentifier,
unstable_getCacheForType,
unstable_Cache,
unstable_useCacheRefresh,
// enableDebugTracing
unstable_DebugTracingMode,
} from './src/React';
+2
View File
@@ -83,4 +83,6 @@ export {
unstable_Scope,
unstable_useOpaqueIdentifier,
unstable_getCacheForType,
unstable_Cache,
unstable_useCacheRefresh,
} from './src/React';
+2
View File
@@ -50,6 +50,8 @@ export {
SuspenseList,
SuspenseList as unstable_SuspenseList,
unstable_getCacheForType,
unstable_Cache,
unstable_useCacheRefresh,
// enableScopeAPI
unstable_Scope,
unstable_useOpaqueIdentifier,
+4
View File
@@ -17,6 +17,7 @@ import {
REACT_SUSPENSE_LIST_TYPE,
REACT_LEGACY_HIDDEN_TYPE,
REACT_SCOPE_TYPE,
REACT_CACHE_TYPE,
} from 'shared/ReactSymbols';
import {Component, PureComponent} from './ReactBaseClasses';
@@ -48,6 +49,7 @@ import {
useTransition,
useDeferredValue,
useOpaqueIdentifier,
useCacheRefresh,
} from './ReactHooks';
import {
createElementWithValidation,
@@ -112,6 +114,8 @@ export {
REACT_SUSPENSE_LIST_TYPE as SuspenseList,
REACT_LEGACY_HIDDEN_TYPE as unstable_LegacyHidden,
getCacheForType as unstable_getCacheForType,
useCacheRefresh as unstable_useCacheRefresh,
REACT_CACHE_TYPE as unstable_Cache,
// enableFundamentalAPI
createFundamental as unstable_createFundamental,
// enableScopeAPI
+6
View File
@@ -180,3 +180,9 @@ export function useMutableSource<Source, Snapshot>(
const dispatcher = resolveDispatcher();
return dispatcher.useMutableSource(source, getSnapshot, subscribe);
}
export function useCacheRefresh(): <T>(?() => T, ?T) => void {
const dispatcher = resolveDispatcher();
// $FlowFixMe This is unstable, thus optional
return dispatcher.useCacheRefresh();
}
+2
View File
@@ -31,6 +31,7 @@ export let REACT_OPAQUE_ID_TYPE = 0xeae0;
export let REACT_DEBUG_TRACING_MODE_TYPE = 0xeae1;
export let REACT_OFFSCREEN_TYPE = 0xeae2;
export let REACT_LEGACY_HIDDEN_TYPE = 0xeae3;
export let REACT_CACHE_TYPE = 0xeae4;
if (typeof Symbol === 'function' && Symbol.for) {
const symbolFor = Symbol.for;
@@ -52,6 +53,7 @@ if (typeof Symbol === 'function' && Symbol.for) {
REACT_DEBUG_TRACING_MODE_TYPE = symbolFor('react.debug_trace_mode');
REACT_OFFSCREEN_TYPE = symbolFor('react.offscreen');
REACT_LEGACY_HIDDEN_TYPE = symbolFor('react.legacy_hidden');
REACT_CACHE_TYPE = symbolFor('react.cache');
}
const MAYBE_ITERATOR_SYMBOL = typeof Symbol === 'function' && Symbol.iterator;
+3
View File
@@ -21,6 +21,7 @@ import {
REACT_SUSPENSE_TYPE,
REACT_SUSPENSE_LIST_TYPE,
REACT_LAZY_TYPE,
REACT_CACHE_TYPE,
} from 'shared/ReactSymbols';
import type {ReactContext, ReactProviderType} from 'shared/ReactTypes';
@@ -72,6 +73,8 @@ function getComponentName(type: mixed): string | null {
return 'Suspense';
case REACT_SUSPENSE_LIST_TYPE:
return 'SuspenseList';
case REACT_CACHE_TYPE:
return 'Cache';
}
if (typeof type === 'object') {
switch (type.$$typeof) {
+4 -2
View File
@@ -22,8 +22,9 @@ import {
REACT_FUNDAMENTAL_TYPE,
REACT_SCOPE_TYPE,
REACT_LEGACY_HIDDEN_TYPE,
REACT_CACHE_TYPE,
} from 'shared/ReactSymbols';
import {enableScopeAPI} from './ReactFeatureFlags';
import {enableScopeAPI, enableCache} from './ReactFeatureFlags';
let REACT_MODULE_REFERENCE: number | Symbol = 0;
if (typeof Symbol === 'function') {
@@ -44,7 +45,8 @@ export default function isValidElementType(type: mixed) {
type === REACT_SUSPENSE_TYPE ||
type === REACT_SUSPENSE_LIST_TYPE ||
type === REACT_LEGACY_HIDDEN_TYPE ||
(enableScopeAPI && type === REACT_SCOPE_TYPE)
(enableScopeAPI && type === REACT_SCOPE_TYPE) ||
(enableCache && type === REACT_CACHE_TYPE)
) {
return true;
}
+2 -1
View File
@@ -371,5 +371,6 @@
"380": "Reading the cache is only supported while rendering.",
"381": "This feature is not supported by ReactSuspenseTestUtils.",
"382": "This query has received more parameters than the last time the same query was used. Always pass the exact number of parameters that the query needs.",
"383": "This query has received fewer parameters than the last time the same query was used. Always pass the exact number of parameters that the query needs."
"383": "This query has received fewer parameters than the last time the same query was used. Always pass the exact number of parameters that the query needs.",
"384": "Refreshing the cache is not supported in Server Components."
}