Don't fire passive effects during initial mount of a hidden Offscreen tree (#24967)
* Change OffscreenInstance isHidden to bitmask The isHidden field of OffscreenInstance is a boolean that represents whether the tree is currently hidden. To implement resuable effects, we need to also track whether the passive effects are currently connected. So I've changed this field to a bitmask. No other behavior has changed in this commit. I'll update the effects behavior in the following steps. * Extract passive mount effects to separate functions I'm about to add a "reappear passive effects" function that will share much of the same code as commitPassiveMountEffectOnFiber. To minimize the duplicated code, I've extracted the shared parts into separate functions, similar to what I did for commitLayoutEffectOnFiber and reappearLayoutEffects. This may not save much on code size because Closure will likely inline some of it, anyway, but it makes it harder for the two paths to accidentally diverge. * Don't mount passive effects in a new hidden tree This changes the behavior of Offscreen so that passive effects do not fire when prerendering a brand new tree. Previously, Offscreen did not affect passive effects at all — only layout effects, which mount or unmount whenever the visibility of the tree changes. When hiding an already visible tree, the behavior of passive effects is unchanged, for now; unlike layout effects, the passive effects will not get unmounted. Pre-rendered updates to a hidden tree in this state will also fire normally. This is only temporary, though — the plan is for passive effects to act more like layout effects, and unmount them when the tree is hidden. Perhaps after a delay so that if the visibility toggles quickly back and forth, the effects don't need to remount. I'll implement this separately. * "Atomic" passive commit effects must always fire There are a few cases where commit phase logic always needs to fire even inside a hidden tree. In general, we should try to design algorithms that don't depend on a commit effect running during prerendering, but there's at least one case where I think it makes sense. The experimental Cache component uses reference counting to keep track of the lifetime of a cache instance. This allows us to expose an AbortSignal object that data frameworks can use to cancel aborted requests. These cache objects are considered alive even inside a prerendered tree. To implement this I added an "atomic" passive effect traversal that runs even when a tree is hidden. (As a follow up, we should add a special subtree flag so that we can skip over nodes that don't have them. There are a number of similar subtree flag optimizations that we have planned, so I'll leave them for a later refactor.) The only other feature that currently depends on this behavior is Transition Tracing. I did not add a test for this because Transition Tracing is still in development and doesn't yet work with Offscreen.
This commit is contained in:
parent
2c7dea7365
commit
4ea064eb09
|
@ -61,6 +61,7 @@ import {
|
||||||
CacheComponent,
|
CacheComponent,
|
||||||
TracingMarkerComponent,
|
TracingMarkerComponent,
|
||||||
} from './ReactWorkTags';
|
} from './ReactWorkTags';
|
||||||
|
import {OffscreenVisible} from './ReactFiberOffscreenComponent';
|
||||||
import getComponentNameFromFiber from 'react-reconciler/src/getComponentNameFromFiber';
|
import getComponentNameFromFiber from 'react-reconciler/src/getComponentNameFromFiber';
|
||||||
|
|
||||||
import {isDevToolsPresent} from './ReactFiberDevToolsHook.new';
|
import {isDevToolsPresent} from './ReactFiberDevToolsHook.new';
|
||||||
|
@ -717,7 +718,7 @@ export function createFiberFromOffscreen(
|
||||||
fiber.elementType = REACT_OFFSCREEN_TYPE;
|
fiber.elementType = REACT_OFFSCREEN_TYPE;
|
||||||
fiber.lanes = lanes;
|
fiber.lanes = lanes;
|
||||||
const primaryChildInstance: OffscreenInstance = {
|
const primaryChildInstance: OffscreenInstance = {
|
||||||
isHidden: false,
|
visibility: OffscreenVisible,
|
||||||
pendingMarkers: null,
|
pendingMarkers: null,
|
||||||
retryCache: null,
|
retryCache: null,
|
||||||
transitions: null,
|
transitions: null,
|
||||||
|
@ -738,7 +739,7 @@ export function createFiberFromLegacyHidden(
|
||||||
// Adding a stateNode for legacy hidden because it's currently using
|
// Adding a stateNode for legacy hidden because it's currently using
|
||||||
// the offscreen implementation, which depends on a state node
|
// the offscreen implementation, which depends on a state node
|
||||||
const instance: OffscreenInstance = {
|
const instance: OffscreenInstance = {
|
||||||
isHidden: false,
|
visibility: OffscreenVisible,
|
||||||
pendingMarkers: null,
|
pendingMarkers: null,
|
||||||
transitions: null,
|
transitions: null,
|
||||||
retryCache: null,
|
retryCache: null,
|
||||||
|
|
|
@ -61,6 +61,7 @@ import {
|
||||||
CacheComponent,
|
CacheComponent,
|
||||||
TracingMarkerComponent,
|
TracingMarkerComponent,
|
||||||
} from './ReactWorkTags';
|
} from './ReactWorkTags';
|
||||||
|
import {OffscreenVisible} from './ReactFiberOffscreenComponent';
|
||||||
import getComponentNameFromFiber from 'react-reconciler/src/getComponentNameFromFiber';
|
import getComponentNameFromFiber from 'react-reconciler/src/getComponentNameFromFiber';
|
||||||
|
|
||||||
import {isDevToolsPresent} from './ReactFiberDevToolsHook.old';
|
import {isDevToolsPresent} from './ReactFiberDevToolsHook.old';
|
||||||
|
@ -717,7 +718,7 @@ export function createFiberFromOffscreen(
|
||||||
fiber.elementType = REACT_OFFSCREEN_TYPE;
|
fiber.elementType = REACT_OFFSCREEN_TYPE;
|
||||||
fiber.lanes = lanes;
|
fiber.lanes = lanes;
|
||||||
const primaryChildInstance: OffscreenInstance = {
|
const primaryChildInstance: OffscreenInstance = {
|
||||||
isHidden: false,
|
visibility: OffscreenVisible,
|
||||||
pendingMarkers: null,
|
pendingMarkers: null,
|
||||||
retryCache: null,
|
retryCache: null,
|
||||||
transitions: null,
|
transitions: null,
|
||||||
|
@ -738,7 +739,7 @@ export function createFiberFromLegacyHidden(
|
||||||
// Adding a stateNode for legacy hidden because it's currently using
|
// Adding a stateNode for legacy hidden because it's currently using
|
||||||
// the offscreen implementation, which depends on a state node
|
// the offscreen implementation, which depends on a state node
|
||||||
const instance: OffscreenInstance = {
|
const instance: OffscreenInstance = {
|
||||||
isHidden: false,
|
visibility: OffscreenVisible,
|
||||||
pendingMarkers: null,
|
pendingMarkers: null,
|
||||||
transitions: null,
|
transitions: null,
|
||||||
retryCache: null,
|
retryCache: null,
|
||||||
|
|
|
@ -173,6 +173,10 @@ import {
|
||||||
} from './ReactFiberDevToolsHook.new';
|
} from './ReactFiberDevToolsHook.new';
|
||||||
import {releaseCache, retainCache} from './ReactFiberCacheComponent.new';
|
import {releaseCache, retainCache} from './ReactFiberCacheComponent.new';
|
||||||
import {clearTransitionsForLanes} from './ReactFiberLane.new';
|
import {clearTransitionsForLanes} from './ReactFiberLane.new';
|
||||||
|
import {
|
||||||
|
OffscreenVisible,
|
||||||
|
OffscreenPassiveEffectsConnected,
|
||||||
|
} from './ReactFiberOffscreenComponent';
|
||||||
|
|
||||||
let didWarnAboutUndefinedSnapshotBeforeUpdate: Set<mixed> | null = null;
|
let didWarnAboutUndefinedSnapshotBeforeUpdate: Set<mixed> | null = null;
|
||||||
if (__DEV__) {
|
if (__DEV__) {
|
||||||
|
@ -2424,14 +2428,8 @@ function commitMutationEffectsOnFiber(
|
||||||
const offscreenFiber: Fiber = (finishedWork.child: any);
|
const offscreenFiber: Fiber = (finishedWork.child: any);
|
||||||
|
|
||||||
if (offscreenFiber.flags & Visibility) {
|
if (offscreenFiber.flags & Visibility) {
|
||||||
const offscreenInstance: OffscreenInstance = offscreenFiber.stateNode;
|
|
||||||
const newState: OffscreenState | null = offscreenFiber.memoizedState;
|
const newState: OffscreenState | null = offscreenFiber.memoizedState;
|
||||||
const isHidden = newState !== null;
|
const isHidden = newState !== null;
|
||||||
|
|
||||||
// Track the current state on the Offscreen instance so we can
|
|
||||||
// read it during an event
|
|
||||||
offscreenInstance.isHidden = isHidden;
|
|
||||||
|
|
||||||
if (isHidden) {
|
if (isHidden) {
|
||||||
const wasHidden =
|
const wasHidden =
|
||||||
offscreenFiber.alternate !== null &&
|
offscreenFiber.alternate !== null &&
|
||||||
|
@ -2485,7 +2483,11 @@ function commitMutationEffectsOnFiber(
|
||||||
|
|
||||||
// Track the current state on the Offscreen instance so we can
|
// Track the current state on the Offscreen instance so we can
|
||||||
// read it during an event
|
// read it during an event
|
||||||
offscreenInstance.isHidden = isHidden;
|
if (isHidden) {
|
||||||
|
offscreenInstance.visibility &= ~OffscreenVisible;
|
||||||
|
} else {
|
||||||
|
offscreenInstance.visibility |= OffscreenVisible;
|
||||||
|
}
|
||||||
|
|
||||||
if (isHidden) {
|
if (isHidden) {
|
||||||
if (!wasHidden) {
|
if (!wasHidden) {
|
||||||
|
@ -2871,6 +2873,167 @@ function recursivelyTraverseReappearLayoutEffects(
|
||||||
setCurrentDebugFiberInDEV(prevDebugFiber);
|
setCurrentDebugFiberInDEV(prevDebugFiber);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function commitHookPassiveMountEffects(
|
||||||
|
finishedWork: Fiber,
|
||||||
|
hookFlags: HookFlags,
|
||||||
|
) {
|
||||||
|
if (
|
||||||
|
enableProfilerTimer &&
|
||||||
|
enableProfilerCommitHooks &&
|
||||||
|
finishedWork.mode & ProfileMode
|
||||||
|
) {
|
||||||
|
startPassiveEffectTimer();
|
||||||
|
try {
|
||||||
|
commitHookEffectListMount(hookFlags, finishedWork);
|
||||||
|
} catch (error) {
|
||||||
|
captureCommitPhaseError(finishedWork, finishedWork.return, error);
|
||||||
|
}
|
||||||
|
recordPassiveEffectDuration(finishedWork);
|
||||||
|
} else {
|
||||||
|
try {
|
||||||
|
commitHookEffectListMount(hookFlags, finishedWork);
|
||||||
|
} catch (error) {
|
||||||
|
captureCommitPhaseError(finishedWork, finishedWork.return, error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function commitOffscreenPassiveMountEffects(
|
||||||
|
current: Fiber | null,
|
||||||
|
finishedWork: Fiber,
|
||||||
|
instance: OffscreenInstance,
|
||||||
|
) {
|
||||||
|
if (enableCache) {
|
||||||
|
let previousCache: Cache | null = null;
|
||||||
|
if (
|
||||||
|
current !== null &&
|
||||||
|
current.memoizedState !== null &&
|
||||||
|
current.memoizedState.cachePool !== null
|
||||||
|
) {
|
||||||
|
previousCache = current.memoizedState.cachePool.pool;
|
||||||
|
}
|
||||||
|
let nextCache: Cache | null = null;
|
||||||
|
if (
|
||||||
|
finishedWork.memoizedState !== null &&
|
||||||
|
finishedWork.memoizedState.cachePool !== null
|
||||||
|
) {
|
||||||
|
nextCache = finishedWork.memoizedState.cachePool.pool;
|
||||||
|
}
|
||||||
|
// Retain/release the cache used for pending (suspended) nodes.
|
||||||
|
// Note that this is only reached in the non-suspended/visible case:
|
||||||
|
// when the content is suspended/hidden, the retain/release occurs
|
||||||
|
// via the parent Suspense component (see case above).
|
||||||
|
if (nextCache !== previousCache) {
|
||||||
|
if (nextCache != null) {
|
||||||
|
retainCache(nextCache);
|
||||||
|
}
|
||||||
|
if (previousCache != null) {
|
||||||
|
releaseCache(previousCache);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (enableTransitionTracing) {
|
||||||
|
// TODO: Pre-rendering should not be counted as part of a transition. We
|
||||||
|
// may add separate logs for pre-rendering, but it's not part of the
|
||||||
|
// primary metrics.
|
||||||
|
const offscreenState: OffscreenState = finishedWork.memoizedState;
|
||||||
|
const queue: OffscreenQueue | null = (finishedWork.updateQueue: any);
|
||||||
|
|
||||||
|
const isHidden = offscreenState !== null;
|
||||||
|
if (queue !== null) {
|
||||||
|
if (isHidden) {
|
||||||
|
const transitions = queue.transitions;
|
||||||
|
if (transitions !== null) {
|
||||||
|
transitions.forEach(transition => {
|
||||||
|
// Add all the transitions saved in the update queue during
|
||||||
|
// the render phase (ie the transitions associated with this boundary)
|
||||||
|
// into the transitions set.
|
||||||
|
if (instance.transitions === null) {
|
||||||
|
instance.transitions = new Set();
|
||||||
|
}
|
||||||
|
instance.transitions.add(transition);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const markerInstances = queue.markerInstances;
|
||||||
|
if (markerInstances !== null) {
|
||||||
|
markerInstances.forEach(markerInstance => {
|
||||||
|
const markerTransitions = markerInstance.transitions;
|
||||||
|
// There should only be a few tracing marker transitions because
|
||||||
|
// they should be only associated with the transition that
|
||||||
|
// caused them
|
||||||
|
if (markerTransitions !== null) {
|
||||||
|
markerTransitions.forEach(transition => {
|
||||||
|
if (instance.transitions === null) {
|
||||||
|
instance.transitions = new Set();
|
||||||
|
} else if (instance.transitions.has(transition)) {
|
||||||
|
if (markerInstance.pendingBoundaries === null) {
|
||||||
|
markerInstance.pendingBoundaries = new Map();
|
||||||
|
}
|
||||||
|
if (instance.pendingMarkers === null) {
|
||||||
|
instance.pendingMarkers = new Set();
|
||||||
|
}
|
||||||
|
|
||||||
|
instance.pendingMarkers.add(markerInstance);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
finishedWork.updateQueue = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
commitTransitionProgress(finishedWork);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function commitCachePassiveMountEffect(
|
||||||
|
current: Fiber | null,
|
||||||
|
finishedWork: Fiber,
|
||||||
|
) {
|
||||||
|
if (enableCache) {
|
||||||
|
let previousCache: Cache | null = null;
|
||||||
|
if (finishedWork.alternate !== null) {
|
||||||
|
previousCache = finishedWork.alternate.memoizedState.cache;
|
||||||
|
}
|
||||||
|
const nextCache = finishedWork.memoizedState.cache;
|
||||||
|
// Retain/release the cache. In theory the cache component
|
||||||
|
// could be "borrowing" a cache instance owned by some parent,
|
||||||
|
// in which case we could avoid retaining/releasing. But it
|
||||||
|
// is non-trivial to determine when that is the case, so we
|
||||||
|
// always retain/release.
|
||||||
|
if (nextCache !== previousCache) {
|
||||||
|
retainCache(nextCache);
|
||||||
|
if (previousCache != null) {
|
||||||
|
releaseCache(previousCache);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function commitTracingMarkerPassiveMountEffect(finishedWork: Fiber) {
|
||||||
|
// Get the transitions that were initiatized during the render
|
||||||
|
// and add a start transition callback for each of them
|
||||||
|
const instance = finishedWork.stateNode;
|
||||||
|
if (
|
||||||
|
instance.transitions !== null &&
|
||||||
|
(instance.pendingBoundaries === null ||
|
||||||
|
instance.pendingBoundaries.size === 0)
|
||||||
|
) {
|
||||||
|
instance.transitions.forEach(transition => {
|
||||||
|
addMarkerCompleteCallbackToPendingTransition(
|
||||||
|
finishedWork.memoizedProps.name,
|
||||||
|
instance.transitions,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
instance.transitions = null;
|
||||||
|
instance.pendingBoundaries = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
export function commitPassiveMountEffects(
|
export function commitPassiveMountEffects(
|
||||||
root: FiberRoot,
|
root: FiberRoot,
|
||||||
finishedWork: Fiber,
|
finishedWork: Fiber,
|
||||||
|
@ -2916,6 +3079,9 @@ function commitPassiveMountOnFiber(
|
||||||
committedLanes: Lanes,
|
committedLanes: Lanes,
|
||||||
committedTransitions: Array<Transition> | null,
|
committedTransitions: Array<Transition> | null,
|
||||||
): void {
|
): void {
|
||||||
|
// When updating this function, also update reconnectPassiveEffects, which does
|
||||||
|
// most of the same things when an offscreen tree goes from hidden -> visible,
|
||||||
|
// or when toggling effects inside a hidden tree.
|
||||||
const flags = finishedWork.flags;
|
const flags = finishedWork.flags;
|
||||||
switch (finishedWork.tag) {
|
switch (finishedWork.tag) {
|
||||||
case FunctionComponent:
|
case FunctionComponent:
|
||||||
|
@ -2928,31 +3094,10 @@ function commitPassiveMountOnFiber(
|
||||||
committedTransitions,
|
committedTransitions,
|
||||||
);
|
);
|
||||||
if (flags & Passive) {
|
if (flags & Passive) {
|
||||||
if (
|
commitHookPassiveMountEffects(
|
||||||
enableProfilerTimer &&
|
finishedWork,
|
||||||
enableProfilerCommitHooks &&
|
HookPassive | HookHasEffect,
|
||||||
finishedWork.mode & ProfileMode
|
);
|
||||||
) {
|
|
||||||
startPassiveEffectTimer();
|
|
||||||
try {
|
|
||||||
commitHookEffectListMount(
|
|
||||||
HookPassive | HookHasEffect,
|
|
||||||
finishedWork,
|
|
||||||
);
|
|
||||||
} catch (error) {
|
|
||||||
captureCommitPhaseError(finishedWork, finishedWork.return, error);
|
|
||||||
}
|
|
||||||
recordPassiveEffectDuration(finishedWork);
|
|
||||||
} else {
|
|
||||||
try {
|
|
||||||
commitHookEffectListMount(
|
|
||||||
HookPassive | HookHasEffect,
|
|
||||||
finishedWork,
|
|
||||||
);
|
|
||||||
} catch (error) {
|
|
||||||
captureCommitPhaseError(finishedWork, finishedWork.return, error);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
@ -3013,95 +3158,78 @@ function commitPassiveMountOnFiber(
|
||||||
}
|
}
|
||||||
case LegacyHiddenComponent:
|
case LegacyHiddenComponent:
|
||||||
case OffscreenComponent: {
|
case OffscreenComponent: {
|
||||||
recursivelyTraversePassiveMountEffects(
|
// TODO: Pass `current` as argument to this function
|
||||||
finishedRoot,
|
const instance: OffscreenInstance = finishedWork.stateNode;
|
||||||
finishedWork,
|
const nextState: OffscreenState | null = finishedWork.memoizedState;
|
||||||
committedLanes,
|
|
||||||
committedTransitions,
|
const isHidden = nextState !== null;
|
||||||
);
|
|
||||||
|
if (isHidden) {
|
||||||
|
if (instance.visibility & OffscreenPassiveEffectsConnected) {
|
||||||
|
// The effects are currently connected. Update them.
|
||||||
|
recursivelyTraversePassiveMountEffects(
|
||||||
|
finishedRoot,
|
||||||
|
finishedWork,
|
||||||
|
committedLanes,
|
||||||
|
committedTransitions,
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
if (finishedWork.mode & ConcurrentMode) {
|
||||||
|
// The effects are currently disconnected. Since the tree is hidden,
|
||||||
|
// don't connect them. This also applies to the initial render.
|
||||||
|
if (enableCache || enableTransitionTracing) {
|
||||||
|
// "Atomic" effects are ones that need to fire on every commit,
|
||||||
|
// even during pre-rendering. An example is updating the reference
|
||||||
|
// count on cache instances.
|
||||||
|
recursivelyTraverseAtomicPassiveEffects(
|
||||||
|
finishedRoot,
|
||||||
|
finishedWork,
|
||||||
|
committedLanes,
|
||||||
|
committedTransitions,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Legacy Mode: Fire the effects even if the tree is hidden.
|
||||||
|
instance.visibility |= OffscreenPassiveEffectsConnected;
|
||||||
|
recursivelyTraversePassiveMountEffects(
|
||||||
|
finishedRoot,
|
||||||
|
finishedWork,
|
||||||
|
committedLanes,
|
||||||
|
committedTransitions,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Tree is visible
|
||||||
|
if (instance.visibility & OffscreenPassiveEffectsConnected) {
|
||||||
|
// The effects are currently connected. Update them.
|
||||||
|
recursivelyTraversePassiveMountEffects(
|
||||||
|
finishedRoot,
|
||||||
|
finishedWork,
|
||||||
|
committedLanes,
|
||||||
|
committedTransitions,
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
// The effects are currently disconnected. Reconnect them, while also
|
||||||
|
// firing effects inside newly mounted trees. This also applies to
|
||||||
|
// the initial render.
|
||||||
|
instance.visibility |= OffscreenPassiveEffectsConnected;
|
||||||
|
|
||||||
|
const includeWorkInProgressEffects =
|
||||||
|
(finishedWork.subtreeFlags & PassiveMask) !== NoFlags;
|
||||||
|
recursivelyTraverseReconnectPassiveEffects(
|
||||||
|
finishedRoot,
|
||||||
|
finishedWork,
|
||||||
|
committedLanes,
|
||||||
|
committedTransitions,
|
||||||
|
includeWorkInProgressEffects,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (flags & Passive) {
|
if (flags & Passive) {
|
||||||
if (enableCache) {
|
const current = finishedWork.alternate;
|
||||||
let previousCache: Cache | null = null;
|
commitOffscreenPassiveMountEffects(current, finishedWork, instance);
|
||||||
if (
|
|
||||||
finishedWork.alternate !== null &&
|
|
||||||
finishedWork.alternate.memoizedState !== null &&
|
|
||||||
finishedWork.alternate.memoizedState.cachePool !== null
|
|
||||||
) {
|
|
||||||
previousCache = finishedWork.alternate.memoizedState.cachePool.pool;
|
|
||||||
}
|
|
||||||
let nextCache: Cache | null = null;
|
|
||||||
if (
|
|
||||||
finishedWork.memoizedState !== null &&
|
|
||||||
finishedWork.memoizedState.cachePool !== null
|
|
||||||
) {
|
|
||||||
nextCache = finishedWork.memoizedState.cachePool.pool;
|
|
||||||
}
|
|
||||||
// Retain/release the cache used for pending (suspended) nodes.
|
|
||||||
// Note that this is only reached in the non-suspended/visible case:
|
|
||||||
// when the content is suspended/hidden, the retain/release occurs
|
|
||||||
// via the parent Suspense component (see case above).
|
|
||||||
if (nextCache !== previousCache) {
|
|
||||||
if (nextCache != null) {
|
|
||||||
retainCache(nextCache);
|
|
||||||
}
|
|
||||||
if (previousCache != null) {
|
|
||||||
releaseCache(previousCache);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (enableTransitionTracing) {
|
|
||||||
const isFallback = finishedWork.memoizedState;
|
|
||||||
const queue: OffscreenQueue | null = (finishedWork.updateQueue: any);
|
|
||||||
const instance: OffscreenInstance = finishedWork.stateNode;
|
|
||||||
|
|
||||||
if (queue !== null) {
|
|
||||||
if (isFallback) {
|
|
||||||
const transitions = queue.transitions;
|
|
||||||
if (transitions !== null) {
|
|
||||||
transitions.forEach(transition => {
|
|
||||||
// Add all the transitions saved in the update queue during
|
|
||||||
// the render phase (ie the transitions associated with this boundary)
|
|
||||||
// into the transitions set.
|
|
||||||
if (instance.transitions === null) {
|
|
||||||
instance.transitions = new Set();
|
|
||||||
}
|
|
||||||
instance.transitions.add(transition);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
const markerInstances = queue.markerInstances;
|
|
||||||
if (markerInstances !== null) {
|
|
||||||
markerInstances.forEach(markerInstance => {
|
|
||||||
const markerTransitions = markerInstance.transitions;
|
|
||||||
// There should only be a few tracing marker transitions because
|
|
||||||
// they should be only associated with the transition that
|
|
||||||
// caused them
|
|
||||||
if (markerTransitions !== null) {
|
|
||||||
markerTransitions.forEach(transition => {
|
|
||||||
if (instance.transitions === null) {
|
|
||||||
instance.transitions = new Set();
|
|
||||||
} else if (instance.transitions.has(transition)) {
|
|
||||||
if (markerInstance.pendingBoundaries === null) {
|
|
||||||
markerInstance.pendingBoundaries = new Map();
|
|
||||||
}
|
|
||||||
if (instance.pendingMarkers === null) {
|
|
||||||
instance.pendingMarkers = new Set();
|
|
||||||
}
|
|
||||||
|
|
||||||
instance.pendingMarkers.add(markerInstance);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
finishedWork.updateQueue = null;
|
|
||||||
}
|
|
||||||
|
|
||||||
commitTransitionProgress(finishedWork);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
@ -3113,24 +3241,9 @@ function commitPassiveMountOnFiber(
|
||||||
committedTransitions,
|
committedTransitions,
|
||||||
);
|
);
|
||||||
if (flags & Passive) {
|
if (flags & Passive) {
|
||||||
if (enableCache) {
|
// TODO: Pass `current` as argument to this function
|
||||||
let previousCache: Cache | null = null;
|
const current = finishedWork.alternate;
|
||||||
if (finishedWork.alternate !== null) {
|
commitCachePassiveMountEffect(current, finishedWork);
|
||||||
previousCache = finishedWork.alternate.memoizedState.cache;
|
|
||||||
}
|
|
||||||
const nextCache = finishedWork.memoizedState.cache;
|
|
||||||
// Retain/release the cache. In theory the cache component
|
|
||||||
// could be "borrowing" a cache instance owned by some parent,
|
|
||||||
// in which case we could avoid retaining/releasing. But it
|
|
||||||
// is non-trivial to determine when that is the case, so we
|
|
||||||
// always retain/release.
|
|
||||||
if (nextCache !== previousCache) {
|
|
||||||
retainCache(nextCache);
|
|
||||||
if (previousCache != null) {
|
|
||||||
releaseCache(previousCache);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
@ -3143,23 +3256,7 @@ function commitPassiveMountOnFiber(
|
||||||
committedTransitions,
|
committedTransitions,
|
||||||
);
|
);
|
||||||
if (flags & Passive) {
|
if (flags & Passive) {
|
||||||
// Get the transitions that were initiatized during the render
|
commitTracingMarkerPassiveMountEffect(finishedWork);
|
||||||
// and add a start transition callback for each of them
|
|
||||||
const instance = finishedWork.stateNode;
|
|
||||||
if (
|
|
||||||
instance.transitions !== null &&
|
|
||||||
(instance.pendingBoundaries === null ||
|
|
||||||
instance.pendingBoundaries.size === 0)
|
|
||||||
) {
|
|
||||||
instance.transitions.forEach(transition => {
|
|
||||||
addMarkerCompleteCallbackToPendingTransition(
|
|
||||||
finishedWork.memoizedProps.name,
|
|
||||||
instance.transitions,
|
|
||||||
);
|
|
||||||
});
|
|
||||||
instance.transitions = null;
|
|
||||||
instance.pendingBoundaries = null;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
@ -3178,6 +3275,278 @@ function commitPassiveMountOnFiber(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function recursivelyTraverseReconnectPassiveEffects(
|
||||||
|
finishedRoot: FiberRoot,
|
||||||
|
parentFiber: Fiber,
|
||||||
|
committedLanes: Lanes,
|
||||||
|
committedTransitions: Array<Transition> | null,
|
||||||
|
includeWorkInProgressEffects: boolean,
|
||||||
|
) {
|
||||||
|
// This function visits both newly finished work and nodes that were re-used
|
||||||
|
// from a previously committed tree. We cannot check non-static flags if the
|
||||||
|
// node was reused.
|
||||||
|
const childShouldIncludeWorkInProgressEffects =
|
||||||
|
includeWorkInProgressEffects &&
|
||||||
|
(parentFiber.subtreeFlags & PassiveMask) !== NoFlags;
|
||||||
|
|
||||||
|
// TODO (Offscreen) Check: flags & (RefStatic | LayoutStatic)
|
||||||
|
const prevDebugFiber = getCurrentDebugFiberInDEV();
|
||||||
|
let child = parentFiber.child;
|
||||||
|
while (child !== null) {
|
||||||
|
reconnectPassiveEffects(
|
||||||
|
finishedRoot,
|
||||||
|
child,
|
||||||
|
committedLanes,
|
||||||
|
committedTransitions,
|
||||||
|
childShouldIncludeWorkInProgressEffects,
|
||||||
|
);
|
||||||
|
child = child.sibling;
|
||||||
|
}
|
||||||
|
setCurrentDebugFiberInDEV(prevDebugFiber);
|
||||||
|
}
|
||||||
|
|
||||||
|
function reconnectPassiveEffects(
|
||||||
|
finishedRoot: FiberRoot,
|
||||||
|
finishedWork: Fiber,
|
||||||
|
committedLanes: Lanes,
|
||||||
|
committedTransitions: Array<Transition> | null,
|
||||||
|
// This function visits both newly finished work and nodes that were re-used
|
||||||
|
// from a previously committed tree. We cannot check non-static flags if the
|
||||||
|
// node was reused.
|
||||||
|
includeWorkInProgressEffects: boolean,
|
||||||
|
) {
|
||||||
|
const flags = finishedWork.flags;
|
||||||
|
switch (finishedWork.tag) {
|
||||||
|
case FunctionComponent:
|
||||||
|
case ForwardRef:
|
||||||
|
case SimpleMemoComponent: {
|
||||||
|
recursivelyTraverseReconnectPassiveEffects(
|
||||||
|
finishedRoot,
|
||||||
|
finishedWork,
|
||||||
|
committedLanes,
|
||||||
|
committedTransitions,
|
||||||
|
includeWorkInProgressEffects,
|
||||||
|
);
|
||||||
|
// TODO: Check for PassiveStatic flag
|
||||||
|
commitHookPassiveMountEffects(finishedWork, HookPassive);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
// Unlike commitPassiveMountOnFiber, we don't need to handle HostRoot
|
||||||
|
// because this function only visits nodes that are inside an
|
||||||
|
// Offscreen fiber.
|
||||||
|
// case HostRoot: {
|
||||||
|
// ...
|
||||||
|
// }
|
||||||
|
case LegacyHiddenComponent:
|
||||||
|
case OffscreenComponent: {
|
||||||
|
const instance: OffscreenInstance = finishedWork.stateNode;
|
||||||
|
const nextState: OffscreenState | null = finishedWork.memoizedState;
|
||||||
|
|
||||||
|
const isHidden = nextState !== null;
|
||||||
|
|
||||||
|
if (isHidden) {
|
||||||
|
if (instance.visibility & OffscreenPassiveEffectsConnected) {
|
||||||
|
// The effects are currently connected. Update them.
|
||||||
|
recursivelyTraverseReconnectPassiveEffects(
|
||||||
|
finishedRoot,
|
||||||
|
finishedWork,
|
||||||
|
committedLanes,
|
||||||
|
committedTransitions,
|
||||||
|
includeWorkInProgressEffects,
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
if (finishedWork.mode & ConcurrentMode) {
|
||||||
|
// The effects are currently disconnected. Since the tree is hidden,
|
||||||
|
// don't connect them. This also applies to the initial render.
|
||||||
|
if (enableCache || enableTransitionTracing) {
|
||||||
|
// "Atomic" effects are ones that need to fire on every commit,
|
||||||
|
// even during pre-rendering. An example is updating the reference
|
||||||
|
// count on cache instances.
|
||||||
|
recursivelyTraverseAtomicPassiveEffects(
|
||||||
|
finishedRoot,
|
||||||
|
finishedWork,
|
||||||
|
committedLanes,
|
||||||
|
committedTransitions,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Legacy Mode: Fire the effects even if the tree is hidden.
|
||||||
|
instance.visibility |= OffscreenPassiveEffectsConnected;
|
||||||
|
recursivelyTraverseReconnectPassiveEffects(
|
||||||
|
finishedRoot,
|
||||||
|
finishedWork,
|
||||||
|
committedLanes,
|
||||||
|
committedTransitions,
|
||||||
|
includeWorkInProgressEffects,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Tree is visible
|
||||||
|
|
||||||
|
// Since we're already inside a reconnecting tree, it doesn't matter
|
||||||
|
// whether the effects are currently connected. In either case, we'll
|
||||||
|
// continue traversing the tree and firing all the effects.
|
||||||
|
//
|
||||||
|
// We do need to set the "connected" flag on the instance, though.
|
||||||
|
instance.visibility |= OffscreenPassiveEffectsConnected;
|
||||||
|
|
||||||
|
recursivelyTraverseReconnectPassiveEffects(
|
||||||
|
finishedRoot,
|
||||||
|
finishedWork,
|
||||||
|
committedLanes,
|
||||||
|
committedTransitions,
|
||||||
|
includeWorkInProgressEffects,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (includeWorkInProgressEffects && flags & Passive) {
|
||||||
|
// TODO: Pass `current` as argument to this function
|
||||||
|
const current: Fiber | null = finishedWork.alternate;
|
||||||
|
commitOffscreenPassiveMountEffects(current, finishedWork, instance);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case CacheComponent: {
|
||||||
|
recursivelyTraverseReconnectPassiveEffects(
|
||||||
|
finishedRoot,
|
||||||
|
finishedWork,
|
||||||
|
committedLanes,
|
||||||
|
committedTransitions,
|
||||||
|
includeWorkInProgressEffects,
|
||||||
|
);
|
||||||
|
if (includeWorkInProgressEffects && flags & Passive) {
|
||||||
|
// TODO: Pass `current` as argument to this function
|
||||||
|
const current = finishedWork.alternate;
|
||||||
|
commitCachePassiveMountEffect(current, finishedWork);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case TracingMarkerComponent: {
|
||||||
|
if (enableTransitionTracing) {
|
||||||
|
recursivelyTraverseReconnectPassiveEffects(
|
||||||
|
finishedRoot,
|
||||||
|
finishedWork,
|
||||||
|
committedLanes,
|
||||||
|
committedTransitions,
|
||||||
|
includeWorkInProgressEffects,
|
||||||
|
);
|
||||||
|
if (includeWorkInProgressEffects && flags & Passive) {
|
||||||
|
commitTracingMarkerPassiveMountEffect(finishedWork);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
// Intentional fallthrough to next branch
|
||||||
|
}
|
||||||
|
// eslint-disable-next-line-no-fallthrough
|
||||||
|
default: {
|
||||||
|
recursivelyTraverseReconnectPassiveEffects(
|
||||||
|
finishedRoot,
|
||||||
|
finishedWork,
|
||||||
|
committedLanes,
|
||||||
|
committedTransitions,
|
||||||
|
includeWorkInProgressEffects,
|
||||||
|
);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function recursivelyTraverseAtomicPassiveEffects(
|
||||||
|
finishedRoot: FiberRoot,
|
||||||
|
parentFiber: Fiber,
|
||||||
|
committedLanes: Lanes,
|
||||||
|
committedTransitions: Array<Transition> | null,
|
||||||
|
) {
|
||||||
|
// "Atomic" effects are ones that need to fire on every commit, even during
|
||||||
|
// pre-rendering. We call this function when traversing a hidden tree whose
|
||||||
|
// regular effects are currently disconnected.
|
||||||
|
const prevDebugFiber = getCurrentDebugFiberInDEV();
|
||||||
|
// TODO: Add special flag for atomic effects
|
||||||
|
if (parentFiber.subtreeFlags & PassiveMask) {
|
||||||
|
let child = parentFiber.child;
|
||||||
|
while (child !== null) {
|
||||||
|
setCurrentDebugFiberInDEV(child);
|
||||||
|
commitAtomicPassiveEffects(
|
||||||
|
finishedRoot,
|
||||||
|
child,
|
||||||
|
committedLanes,
|
||||||
|
committedTransitions,
|
||||||
|
);
|
||||||
|
child = child.sibling;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
setCurrentDebugFiberInDEV(prevDebugFiber);
|
||||||
|
}
|
||||||
|
|
||||||
|
function commitAtomicPassiveEffects(
|
||||||
|
finishedRoot: FiberRoot,
|
||||||
|
finishedWork: Fiber,
|
||||||
|
committedLanes: Lanes,
|
||||||
|
committedTransitions: Array<Transition> | null,
|
||||||
|
) {
|
||||||
|
// "Atomic" effects are ones that need to fire on every commit, even during
|
||||||
|
// pre-rendering. We call this function when traversing a hidden tree whose
|
||||||
|
// regular effects are currently disconnected.
|
||||||
|
const flags = finishedWork.flags;
|
||||||
|
switch (finishedWork.tag) {
|
||||||
|
case OffscreenComponent: {
|
||||||
|
recursivelyTraverseAtomicPassiveEffects(
|
||||||
|
finishedRoot,
|
||||||
|
finishedWork,
|
||||||
|
committedLanes,
|
||||||
|
committedTransitions,
|
||||||
|
);
|
||||||
|
if (flags & Passive) {
|
||||||
|
// TODO: Pass `current` as argument to this function
|
||||||
|
const current = finishedWork.alternate;
|
||||||
|
const instance: OffscreenInstance = finishedWork.stateNode;
|
||||||
|
commitOffscreenPassiveMountEffects(current, finishedWork, instance);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case CacheComponent: {
|
||||||
|
recursivelyTraverseAtomicPassiveEffects(
|
||||||
|
finishedRoot,
|
||||||
|
finishedWork,
|
||||||
|
committedLanes,
|
||||||
|
committedTransitions,
|
||||||
|
);
|
||||||
|
if (flags & Passive) {
|
||||||
|
// TODO: Pass `current` as argument to this function
|
||||||
|
const current = finishedWork.alternate;
|
||||||
|
commitCachePassiveMountEffect(current, finishedWork);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case TracingMarkerComponent: {
|
||||||
|
if (enableTransitionTracing) {
|
||||||
|
recursivelyTraverseAtomicPassiveEffects(
|
||||||
|
finishedRoot,
|
||||||
|
finishedWork,
|
||||||
|
committedLanes,
|
||||||
|
committedTransitions,
|
||||||
|
);
|
||||||
|
if (flags & Passive) {
|
||||||
|
commitTracingMarkerPassiveMountEffect(finishedWork);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
// Intentional fallthrough to next branch
|
||||||
|
}
|
||||||
|
// eslint-disable-next-line-no-fallthrough
|
||||||
|
default: {
|
||||||
|
recursivelyTraverseAtomicPassiveEffects(
|
||||||
|
finishedRoot,
|
||||||
|
finishedWork,
|
||||||
|
committedLanes,
|
||||||
|
committedTransitions,
|
||||||
|
);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
export function commitPassiveUnmountEffects(finishedWork: Fiber): void {
|
export function commitPassiveUnmountEffects(finishedWork: Fiber): void {
|
||||||
setCurrentDebugFiberInDEV(finishedWork);
|
setCurrentDebugFiberInDEV(finishedWork);
|
||||||
commitPassiveUnmountOnFiber(finishedWork);
|
commitPassiveUnmountOnFiber(finishedWork);
|
||||||
|
@ -3275,6 +3644,11 @@ function commitPassiveUnmountOnFiber(finishedWork: Fiber): void {
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
// TODO: Disconnect passive effects when a tree is hidden, perhaps after
|
||||||
|
// a delay.
|
||||||
|
// case OffscreenComponent: {
|
||||||
|
// ...
|
||||||
|
// }
|
||||||
default: {
|
default: {
|
||||||
recursivelyTraversePassiveUnmountEffects(finishedWork);
|
recursivelyTraversePassiveUnmountEffects(finishedWork);
|
||||||
break;
|
break;
|
||||||
|
|
|
@ -173,6 +173,10 @@ import {
|
||||||
} from './ReactFiberDevToolsHook.old';
|
} from './ReactFiberDevToolsHook.old';
|
||||||
import {releaseCache, retainCache} from './ReactFiberCacheComponent.old';
|
import {releaseCache, retainCache} from './ReactFiberCacheComponent.old';
|
||||||
import {clearTransitionsForLanes} from './ReactFiberLane.old';
|
import {clearTransitionsForLanes} from './ReactFiberLane.old';
|
||||||
|
import {
|
||||||
|
OffscreenVisible,
|
||||||
|
OffscreenPassiveEffectsConnected,
|
||||||
|
} from './ReactFiberOffscreenComponent';
|
||||||
|
|
||||||
let didWarnAboutUndefinedSnapshotBeforeUpdate: Set<mixed> | null = null;
|
let didWarnAboutUndefinedSnapshotBeforeUpdate: Set<mixed> | null = null;
|
||||||
if (__DEV__) {
|
if (__DEV__) {
|
||||||
|
@ -2424,14 +2428,8 @@ function commitMutationEffectsOnFiber(
|
||||||
const offscreenFiber: Fiber = (finishedWork.child: any);
|
const offscreenFiber: Fiber = (finishedWork.child: any);
|
||||||
|
|
||||||
if (offscreenFiber.flags & Visibility) {
|
if (offscreenFiber.flags & Visibility) {
|
||||||
const offscreenInstance: OffscreenInstance = offscreenFiber.stateNode;
|
|
||||||
const newState: OffscreenState | null = offscreenFiber.memoizedState;
|
const newState: OffscreenState | null = offscreenFiber.memoizedState;
|
||||||
const isHidden = newState !== null;
|
const isHidden = newState !== null;
|
||||||
|
|
||||||
// Track the current state on the Offscreen instance so we can
|
|
||||||
// read it during an event
|
|
||||||
offscreenInstance.isHidden = isHidden;
|
|
||||||
|
|
||||||
if (isHidden) {
|
if (isHidden) {
|
||||||
const wasHidden =
|
const wasHidden =
|
||||||
offscreenFiber.alternate !== null &&
|
offscreenFiber.alternate !== null &&
|
||||||
|
@ -2485,7 +2483,11 @@ function commitMutationEffectsOnFiber(
|
||||||
|
|
||||||
// Track the current state on the Offscreen instance so we can
|
// Track the current state on the Offscreen instance so we can
|
||||||
// read it during an event
|
// read it during an event
|
||||||
offscreenInstance.isHidden = isHidden;
|
if (isHidden) {
|
||||||
|
offscreenInstance.visibility &= ~OffscreenVisible;
|
||||||
|
} else {
|
||||||
|
offscreenInstance.visibility |= OffscreenVisible;
|
||||||
|
}
|
||||||
|
|
||||||
if (isHidden) {
|
if (isHidden) {
|
||||||
if (!wasHidden) {
|
if (!wasHidden) {
|
||||||
|
@ -2871,6 +2873,167 @@ function recursivelyTraverseReappearLayoutEffects(
|
||||||
setCurrentDebugFiberInDEV(prevDebugFiber);
|
setCurrentDebugFiberInDEV(prevDebugFiber);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function commitHookPassiveMountEffects(
|
||||||
|
finishedWork: Fiber,
|
||||||
|
hookFlags: HookFlags,
|
||||||
|
) {
|
||||||
|
if (
|
||||||
|
enableProfilerTimer &&
|
||||||
|
enableProfilerCommitHooks &&
|
||||||
|
finishedWork.mode & ProfileMode
|
||||||
|
) {
|
||||||
|
startPassiveEffectTimer();
|
||||||
|
try {
|
||||||
|
commitHookEffectListMount(hookFlags, finishedWork);
|
||||||
|
} catch (error) {
|
||||||
|
captureCommitPhaseError(finishedWork, finishedWork.return, error);
|
||||||
|
}
|
||||||
|
recordPassiveEffectDuration(finishedWork);
|
||||||
|
} else {
|
||||||
|
try {
|
||||||
|
commitHookEffectListMount(hookFlags, finishedWork);
|
||||||
|
} catch (error) {
|
||||||
|
captureCommitPhaseError(finishedWork, finishedWork.return, error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function commitOffscreenPassiveMountEffects(
|
||||||
|
current: Fiber | null,
|
||||||
|
finishedWork: Fiber,
|
||||||
|
instance: OffscreenInstance,
|
||||||
|
) {
|
||||||
|
if (enableCache) {
|
||||||
|
let previousCache: Cache | null = null;
|
||||||
|
if (
|
||||||
|
current !== null &&
|
||||||
|
current.memoizedState !== null &&
|
||||||
|
current.memoizedState.cachePool !== null
|
||||||
|
) {
|
||||||
|
previousCache = current.memoizedState.cachePool.pool;
|
||||||
|
}
|
||||||
|
let nextCache: Cache | null = null;
|
||||||
|
if (
|
||||||
|
finishedWork.memoizedState !== null &&
|
||||||
|
finishedWork.memoizedState.cachePool !== null
|
||||||
|
) {
|
||||||
|
nextCache = finishedWork.memoizedState.cachePool.pool;
|
||||||
|
}
|
||||||
|
// Retain/release the cache used for pending (suspended) nodes.
|
||||||
|
// Note that this is only reached in the non-suspended/visible case:
|
||||||
|
// when the content is suspended/hidden, the retain/release occurs
|
||||||
|
// via the parent Suspense component (see case above).
|
||||||
|
if (nextCache !== previousCache) {
|
||||||
|
if (nextCache != null) {
|
||||||
|
retainCache(nextCache);
|
||||||
|
}
|
||||||
|
if (previousCache != null) {
|
||||||
|
releaseCache(previousCache);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (enableTransitionTracing) {
|
||||||
|
// TODO: Pre-rendering should not be counted as part of a transition. We
|
||||||
|
// may add separate logs for pre-rendering, but it's not part of the
|
||||||
|
// primary metrics.
|
||||||
|
const offscreenState: OffscreenState = finishedWork.memoizedState;
|
||||||
|
const queue: OffscreenQueue | null = (finishedWork.updateQueue: any);
|
||||||
|
|
||||||
|
const isHidden = offscreenState !== null;
|
||||||
|
if (queue !== null) {
|
||||||
|
if (isHidden) {
|
||||||
|
const transitions = queue.transitions;
|
||||||
|
if (transitions !== null) {
|
||||||
|
transitions.forEach(transition => {
|
||||||
|
// Add all the transitions saved in the update queue during
|
||||||
|
// the render phase (ie the transitions associated with this boundary)
|
||||||
|
// into the transitions set.
|
||||||
|
if (instance.transitions === null) {
|
||||||
|
instance.transitions = new Set();
|
||||||
|
}
|
||||||
|
instance.transitions.add(transition);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const markerInstances = queue.markerInstances;
|
||||||
|
if (markerInstances !== null) {
|
||||||
|
markerInstances.forEach(markerInstance => {
|
||||||
|
const markerTransitions = markerInstance.transitions;
|
||||||
|
// There should only be a few tracing marker transitions because
|
||||||
|
// they should be only associated with the transition that
|
||||||
|
// caused them
|
||||||
|
if (markerTransitions !== null) {
|
||||||
|
markerTransitions.forEach(transition => {
|
||||||
|
if (instance.transitions === null) {
|
||||||
|
instance.transitions = new Set();
|
||||||
|
} else if (instance.transitions.has(transition)) {
|
||||||
|
if (markerInstance.pendingBoundaries === null) {
|
||||||
|
markerInstance.pendingBoundaries = new Map();
|
||||||
|
}
|
||||||
|
if (instance.pendingMarkers === null) {
|
||||||
|
instance.pendingMarkers = new Set();
|
||||||
|
}
|
||||||
|
|
||||||
|
instance.pendingMarkers.add(markerInstance);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
finishedWork.updateQueue = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
commitTransitionProgress(finishedWork);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function commitCachePassiveMountEffect(
|
||||||
|
current: Fiber | null,
|
||||||
|
finishedWork: Fiber,
|
||||||
|
) {
|
||||||
|
if (enableCache) {
|
||||||
|
let previousCache: Cache | null = null;
|
||||||
|
if (finishedWork.alternate !== null) {
|
||||||
|
previousCache = finishedWork.alternate.memoizedState.cache;
|
||||||
|
}
|
||||||
|
const nextCache = finishedWork.memoizedState.cache;
|
||||||
|
// Retain/release the cache. In theory the cache component
|
||||||
|
// could be "borrowing" a cache instance owned by some parent,
|
||||||
|
// in which case we could avoid retaining/releasing. But it
|
||||||
|
// is non-trivial to determine when that is the case, so we
|
||||||
|
// always retain/release.
|
||||||
|
if (nextCache !== previousCache) {
|
||||||
|
retainCache(nextCache);
|
||||||
|
if (previousCache != null) {
|
||||||
|
releaseCache(previousCache);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function commitTracingMarkerPassiveMountEffect(finishedWork: Fiber) {
|
||||||
|
// Get the transitions that were initiatized during the render
|
||||||
|
// and add a start transition callback for each of them
|
||||||
|
const instance = finishedWork.stateNode;
|
||||||
|
if (
|
||||||
|
instance.transitions !== null &&
|
||||||
|
(instance.pendingBoundaries === null ||
|
||||||
|
instance.pendingBoundaries.size === 0)
|
||||||
|
) {
|
||||||
|
instance.transitions.forEach(transition => {
|
||||||
|
addMarkerCompleteCallbackToPendingTransition(
|
||||||
|
finishedWork.memoizedProps.name,
|
||||||
|
instance.transitions,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
instance.transitions = null;
|
||||||
|
instance.pendingBoundaries = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
export function commitPassiveMountEffects(
|
export function commitPassiveMountEffects(
|
||||||
root: FiberRoot,
|
root: FiberRoot,
|
||||||
finishedWork: Fiber,
|
finishedWork: Fiber,
|
||||||
|
@ -2916,6 +3079,9 @@ function commitPassiveMountOnFiber(
|
||||||
committedLanes: Lanes,
|
committedLanes: Lanes,
|
||||||
committedTransitions: Array<Transition> | null,
|
committedTransitions: Array<Transition> | null,
|
||||||
): void {
|
): void {
|
||||||
|
// When updating this function, also update reconnectPassiveEffects, which does
|
||||||
|
// most of the same things when an offscreen tree goes from hidden -> visible,
|
||||||
|
// or when toggling effects inside a hidden tree.
|
||||||
const flags = finishedWork.flags;
|
const flags = finishedWork.flags;
|
||||||
switch (finishedWork.tag) {
|
switch (finishedWork.tag) {
|
||||||
case FunctionComponent:
|
case FunctionComponent:
|
||||||
|
@ -2928,31 +3094,10 @@ function commitPassiveMountOnFiber(
|
||||||
committedTransitions,
|
committedTransitions,
|
||||||
);
|
);
|
||||||
if (flags & Passive) {
|
if (flags & Passive) {
|
||||||
if (
|
commitHookPassiveMountEffects(
|
||||||
enableProfilerTimer &&
|
finishedWork,
|
||||||
enableProfilerCommitHooks &&
|
HookPassive | HookHasEffect,
|
||||||
finishedWork.mode & ProfileMode
|
);
|
||||||
) {
|
|
||||||
startPassiveEffectTimer();
|
|
||||||
try {
|
|
||||||
commitHookEffectListMount(
|
|
||||||
HookPassive | HookHasEffect,
|
|
||||||
finishedWork,
|
|
||||||
);
|
|
||||||
} catch (error) {
|
|
||||||
captureCommitPhaseError(finishedWork, finishedWork.return, error);
|
|
||||||
}
|
|
||||||
recordPassiveEffectDuration(finishedWork);
|
|
||||||
} else {
|
|
||||||
try {
|
|
||||||
commitHookEffectListMount(
|
|
||||||
HookPassive | HookHasEffect,
|
|
||||||
finishedWork,
|
|
||||||
);
|
|
||||||
} catch (error) {
|
|
||||||
captureCommitPhaseError(finishedWork, finishedWork.return, error);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
@ -3013,95 +3158,78 @@ function commitPassiveMountOnFiber(
|
||||||
}
|
}
|
||||||
case LegacyHiddenComponent:
|
case LegacyHiddenComponent:
|
||||||
case OffscreenComponent: {
|
case OffscreenComponent: {
|
||||||
recursivelyTraversePassiveMountEffects(
|
// TODO: Pass `current` as argument to this function
|
||||||
finishedRoot,
|
const instance: OffscreenInstance = finishedWork.stateNode;
|
||||||
finishedWork,
|
const nextState: OffscreenState | null = finishedWork.memoizedState;
|
||||||
committedLanes,
|
|
||||||
committedTransitions,
|
const isHidden = nextState !== null;
|
||||||
);
|
|
||||||
|
if (isHidden) {
|
||||||
|
if (instance.visibility & OffscreenPassiveEffectsConnected) {
|
||||||
|
// The effects are currently connected. Update them.
|
||||||
|
recursivelyTraversePassiveMountEffects(
|
||||||
|
finishedRoot,
|
||||||
|
finishedWork,
|
||||||
|
committedLanes,
|
||||||
|
committedTransitions,
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
if (finishedWork.mode & ConcurrentMode) {
|
||||||
|
// The effects are currently disconnected. Since the tree is hidden,
|
||||||
|
// don't connect them. This also applies to the initial render.
|
||||||
|
if (enableCache || enableTransitionTracing) {
|
||||||
|
// "Atomic" effects are ones that need to fire on every commit,
|
||||||
|
// even during pre-rendering. An example is updating the reference
|
||||||
|
// count on cache instances.
|
||||||
|
recursivelyTraverseAtomicPassiveEffects(
|
||||||
|
finishedRoot,
|
||||||
|
finishedWork,
|
||||||
|
committedLanes,
|
||||||
|
committedTransitions,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Legacy Mode: Fire the effects even if the tree is hidden.
|
||||||
|
instance.visibility |= OffscreenPassiveEffectsConnected;
|
||||||
|
recursivelyTraversePassiveMountEffects(
|
||||||
|
finishedRoot,
|
||||||
|
finishedWork,
|
||||||
|
committedLanes,
|
||||||
|
committedTransitions,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Tree is visible
|
||||||
|
if (instance.visibility & OffscreenPassiveEffectsConnected) {
|
||||||
|
// The effects are currently connected. Update them.
|
||||||
|
recursivelyTraversePassiveMountEffects(
|
||||||
|
finishedRoot,
|
||||||
|
finishedWork,
|
||||||
|
committedLanes,
|
||||||
|
committedTransitions,
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
// The effects are currently disconnected. Reconnect them, while also
|
||||||
|
// firing effects inside newly mounted trees. This also applies to
|
||||||
|
// the initial render.
|
||||||
|
instance.visibility |= OffscreenPassiveEffectsConnected;
|
||||||
|
|
||||||
|
const includeWorkInProgressEffects =
|
||||||
|
(finishedWork.subtreeFlags & PassiveMask) !== NoFlags;
|
||||||
|
recursivelyTraverseReconnectPassiveEffects(
|
||||||
|
finishedRoot,
|
||||||
|
finishedWork,
|
||||||
|
committedLanes,
|
||||||
|
committedTransitions,
|
||||||
|
includeWorkInProgressEffects,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (flags & Passive) {
|
if (flags & Passive) {
|
||||||
if (enableCache) {
|
const current = finishedWork.alternate;
|
||||||
let previousCache: Cache | null = null;
|
commitOffscreenPassiveMountEffects(current, finishedWork, instance);
|
||||||
if (
|
|
||||||
finishedWork.alternate !== null &&
|
|
||||||
finishedWork.alternate.memoizedState !== null &&
|
|
||||||
finishedWork.alternate.memoizedState.cachePool !== null
|
|
||||||
) {
|
|
||||||
previousCache = finishedWork.alternate.memoizedState.cachePool.pool;
|
|
||||||
}
|
|
||||||
let nextCache: Cache | null = null;
|
|
||||||
if (
|
|
||||||
finishedWork.memoizedState !== null &&
|
|
||||||
finishedWork.memoizedState.cachePool !== null
|
|
||||||
) {
|
|
||||||
nextCache = finishedWork.memoizedState.cachePool.pool;
|
|
||||||
}
|
|
||||||
// Retain/release the cache used for pending (suspended) nodes.
|
|
||||||
// Note that this is only reached in the non-suspended/visible case:
|
|
||||||
// when the content is suspended/hidden, the retain/release occurs
|
|
||||||
// via the parent Suspense component (see case above).
|
|
||||||
if (nextCache !== previousCache) {
|
|
||||||
if (nextCache != null) {
|
|
||||||
retainCache(nextCache);
|
|
||||||
}
|
|
||||||
if (previousCache != null) {
|
|
||||||
releaseCache(previousCache);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (enableTransitionTracing) {
|
|
||||||
const isFallback = finishedWork.memoizedState;
|
|
||||||
const queue: OffscreenQueue | null = (finishedWork.updateQueue: any);
|
|
||||||
const instance: OffscreenInstance = finishedWork.stateNode;
|
|
||||||
|
|
||||||
if (queue !== null) {
|
|
||||||
if (isFallback) {
|
|
||||||
const transitions = queue.transitions;
|
|
||||||
if (transitions !== null) {
|
|
||||||
transitions.forEach(transition => {
|
|
||||||
// Add all the transitions saved in the update queue during
|
|
||||||
// the render phase (ie the transitions associated with this boundary)
|
|
||||||
// into the transitions set.
|
|
||||||
if (instance.transitions === null) {
|
|
||||||
instance.transitions = new Set();
|
|
||||||
}
|
|
||||||
instance.transitions.add(transition);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
const markerInstances = queue.markerInstances;
|
|
||||||
if (markerInstances !== null) {
|
|
||||||
markerInstances.forEach(markerInstance => {
|
|
||||||
const markerTransitions = markerInstance.transitions;
|
|
||||||
// There should only be a few tracing marker transitions because
|
|
||||||
// they should be only associated with the transition that
|
|
||||||
// caused them
|
|
||||||
if (markerTransitions !== null) {
|
|
||||||
markerTransitions.forEach(transition => {
|
|
||||||
if (instance.transitions === null) {
|
|
||||||
instance.transitions = new Set();
|
|
||||||
} else if (instance.transitions.has(transition)) {
|
|
||||||
if (markerInstance.pendingBoundaries === null) {
|
|
||||||
markerInstance.pendingBoundaries = new Map();
|
|
||||||
}
|
|
||||||
if (instance.pendingMarkers === null) {
|
|
||||||
instance.pendingMarkers = new Set();
|
|
||||||
}
|
|
||||||
|
|
||||||
instance.pendingMarkers.add(markerInstance);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
finishedWork.updateQueue = null;
|
|
||||||
}
|
|
||||||
|
|
||||||
commitTransitionProgress(finishedWork);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
@ -3113,24 +3241,9 @@ function commitPassiveMountOnFiber(
|
||||||
committedTransitions,
|
committedTransitions,
|
||||||
);
|
);
|
||||||
if (flags & Passive) {
|
if (flags & Passive) {
|
||||||
if (enableCache) {
|
// TODO: Pass `current` as argument to this function
|
||||||
let previousCache: Cache | null = null;
|
const current = finishedWork.alternate;
|
||||||
if (finishedWork.alternate !== null) {
|
commitCachePassiveMountEffect(current, finishedWork);
|
||||||
previousCache = finishedWork.alternate.memoizedState.cache;
|
|
||||||
}
|
|
||||||
const nextCache = finishedWork.memoizedState.cache;
|
|
||||||
// Retain/release the cache. In theory the cache component
|
|
||||||
// could be "borrowing" a cache instance owned by some parent,
|
|
||||||
// in which case we could avoid retaining/releasing. But it
|
|
||||||
// is non-trivial to determine when that is the case, so we
|
|
||||||
// always retain/release.
|
|
||||||
if (nextCache !== previousCache) {
|
|
||||||
retainCache(nextCache);
|
|
||||||
if (previousCache != null) {
|
|
||||||
releaseCache(previousCache);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
@ -3143,23 +3256,7 @@ function commitPassiveMountOnFiber(
|
||||||
committedTransitions,
|
committedTransitions,
|
||||||
);
|
);
|
||||||
if (flags & Passive) {
|
if (flags & Passive) {
|
||||||
// Get the transitions that were initiatized during the render
|
commitTracingMarkerPassiveMountEffect(finishedWork);
|
||||||
// and add a start transition callback for each of them
|
|
||||||
const instance = finishedWork.stateNode;
|
|
||||||
if (
|
|
||||||
instance.transitions !== null &&
|
|
||||||
(instance.pendingBoundaries === null ||
|
|
||||||
instance.pendingBoundaries.size === 0)
|
|
||||||
) {
|
|
||||||
instance.transitions.forEach(transition => {
|
|
||||||
addMarkerCompleteCallbackToPendingTransition(
|
|
||||||
finishedWork.memoizedProps.name,
|
|
||||||
instance.transitions,
|
|
||||||
);
|
|
||||||
});
|
|
||||||
instance.transitions = null;
|
|
||||||
instance.pendingBoundaries = null;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
@ -3178,6 +3275,278 @@ function commitPassiveMountOnFiber(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function recursivelyTraverseReconnectPassiveEffects(
|
||||||
|
finishedRoot: FiberRoot,
|
||||||
|
parentFiber: Fiber,
|
||||||
|
committedLanes: Lanes,
|
||||||
|
committedTransitions: Array<Transition> | null,
|
||||||
|
includeWorkInProgressEffects: boolean,
|
||||||
|
) {
|
||||||
|
// This function visits both newly finished work and nodes that were re-used
|
||||||
|
// from a previously committed tree. We cannot check non-static flags if the
|
||||||
|
// node was reused.
|
||||||
|
const childShouldIncludeWorkInProgressEffects =
|
||||||
|
includeWorkInProgressEffects &&
|
||||||
|
(parentFiber.subtreeFlags & PassiveMask) !== NoFlags;
|
||||||
|
|
||||||
|
// TODO (Offscreen) Check: flags & (RefStatic | LayoutStatic)
|
||||||
|
const prevDebugFiber = getCurrentDebugFiberInDEV();
|
||||||
|
let child = parentFiber.child;
|
||||||
|
while (child !== null) {
|
||||||
|
reconnectPassiveEffects(
|
||||||
|
finishedRoot,
|
||||||
|
child,
|
||||||
|
committedLanes,
|
||||||
|
committedTransitions,
|
||||||
|
childShouldIncludeWorkInProgressEffects,
|
||||||
|
);
|
||||||
|
child = child.sibling;
|
||||||
|
}
|
||||||
|
setCurrentDebugFiberInDEV(prevDebugFiber);
|
||||||
|
}
|
||||||
|
|
||||||
|
function reconnectPassiveEffects(
|
||||||
|
finishedRoot: FiberRoot,
|
||||||
|
finishedWork: Fiber,
|
||||||
|
committedLanes: Lanes,
|
||||||
|
committedTransitions: Array<Transition> | null,
|
||||||
|
// This function visits both newly finished work and nodes that were re-used
|
||||||
|
// from a previously committed tree. We cannot check non-static flags if the
|
||||||
|
// node was reused.
|
||||||
|
includeWorkInProgressEffects: boolean,
|
||||||
|
) {
|
||||||
|
const flags = finishedWork.flags;
|
||||||
|
switch (finishedWork.tag) {
|
||||||
|
case FunctionComponent:
|
||||||
|
case ForwardRef:
|
||||||
|
case SimpleMemoComponent: {
|
||||||
|
recursivelyTraverseReconnectPassiveEffects(
|
||||||
|
finishedRoot,
|
||||||
|
finishedWork,
|
||||||
|
committedLanes,
|
||||||
|
committedTransitions,
|
||||||
|
includeWorkInProgressEffects,
|
||||||
|
);
|
||||||
|
// TODO: Check for PassiveStatic flag
|
||||||
|
commitHookPassiveMountEffects(finishedWork, HookPassive);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
// Unlike commitPassiveMountOnFiber, we don't need to handle HostRoot
|
||||||
|
// because this function only visits nodes that are inside an
|
||||||
|
// Offscreen fiber.
|
||||||
|
// case HostRoot: {
|
||||||
|
// ...
|
||||||
|
// }
|
||||||
|
case LegacyHiddenComponent:
|
||||||
|
case OffscreenComponent: {
|
||||||
|
const instance: OffscreenInstance = finishedWork.stateNode;
|
||||||
|
const nextState: OffscreenState | null = finishedWork.memoizedState;
|
||||||
|
|
||||||
|
const isHidden = nextState !== null;
|
||||||
|
|
||||||
|
if (isHidden) {
|
||||||
|
if (instance.visibility & OffscreenPassiveEffectsConnected) {
|
||||||
|
// The effects are currently connected. Update them.
|
||||||
|
recursivelyTraverseReconnectPassiveEffects(
|
||||||
|
finishedRoot,
|
||||||
|
finishedWork,
|
||||||
|
committedLanes,
|
||||||
|
committedTransitions,
|
||||||
|
includeWorkInProgressEffects,
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
if (finishedWork.mode & ConcurrentMode) {
|
||||||
|
// The effects are currently disconnected. Since the tree is hidden,
|
||||||
|
// don't connect them. This also applies to the initial render.
|
||||||
|
if (enableCache || enableTransitionTracing) {
|
||||||
|
// "Atomic" effects are ones that need to fire on every commit,
|
||||||
|
// even during pre-rendering. An example is updating the reference
|
||||||
|
// count on cache instances.
|
||||||
|
recursivelyTraverseAtomicPassiveEffects(
|
||||||
|
finishedRoot,
|
||||||
|
finishedWork,
|
||||||
|
committedLanes,
|
||||||
|
committedTransitions,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Legacy Mode: Fire the effects even if the tree is hidden.
|
||||||
|
instance.visibility |= OffscreenPassiveEffectsConnected;
|
||||||
|
recursivelyTraverseReconnectPassiveEffects(
|
||||||
|
finishedRoot,
|
||||||
|
finishedWork,
|
||||||
|
committedLanes,
|
||||||
|
committedTransitions,
|
||||||
|
includeWorkInProgressEffects,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Tree is visible
|
||||||
|
|
||||||
|
// Since we're already inside a reconnecting tree, it doesn't matter
|
||||||
|
// whether the effects are currently connected. In either case, we'll
|
||||||
|
// continue traversing the tree and firing all the effects.
|
||||||
|
//
|
||||||
|
// We do need to set the "connected" flag on the instance, though.
|
||||||
|
instance.visibility |= OffscreenPassiveEffectsConnected;
|
||||||
|
|
||||||
|
recursivelyTraverseReconnectPassiveEffects(
|
||||||
|
finishedRoot,
|
||||||
|
finishedWork,
|
||||||
|
committedLanes,
|
||||||
|
committedTransitions,
|
||||||
|
includeWorkInProgressEffects,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (includeWorkInProgressEffects && flags & Passive) {
|
||||||
|
// TODO: Pass `current` as argument to this function
|
||||||
|
const current: Fiber | null = finishedWork.alternate;
|
||||||
|
commitOffscreenPassiveMountEffects(current, finishedWork, instance);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case CacheComponent: {
|
||||||
|
recursivelyTraverseReconnectPassiveEffects(
|
||||||
|
finishedRoot,
|
||||||
|
finishedWork,
|
||||||
|
committedLanes,
|
||||||
|
committedTransitions,
|
||||||
|
includeWorkInProgressEffects,
|
||||||
|
);
|
||||||
|
if (includeWorkInProgressEffects && flags & Passive) {
|
||||||
|
// TODO: Pass `current` as argument to this function
|
||||||
|
const current = finishedWork.alternate;
|
||||||
|
commitCachePassiveMountEffect(current, finishedWork);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case TracingMarkerComponent: {
|
||||||
|
if (enableTransitionTracing) {
|
||||||
|
recursivelyTraverseReconnectPassiveEffects(
|
||||||
|
finishedRoot,
|
||||||
|
finishedWork,
|
||||||
|
committedLanes,
|
||||||
|
committedTransitions,
|
||||||
|
includeWorkInProgressEffects,
|
||||||
|
);
|
||||||
|
if (includeWorkInProgressEffects && flags & Passive) {
|
||||||
|
commitTracingMarkerPassiveMountEffect(finishedWork);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
// Intentional fallthrough to next branch
|
||||||
|
}
|
||||||
|
// eslint-disable-next-line-no-fallthrough
|
||||||
|
default: {
|
||||||
|
recursivelyTraverseReconnectPassiveEffects(
|
||||||
|
finishedRoot,
|
||||||
|
finishedWork,
|
||||||
|
committedLanes,
|
||||||
|
committedTransitions,
|
||||||
|
includeWorkInProgressEffects,
|
||||||
|
);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function recursivelyTraverseAtomicPassiveEffects(
|
||||||
|
finishedRoot: FiberRoot,
|
||||||
|
parentFiber: Fiber,
|
||||||
|
committedLanes: Lanes,
|
||||||
|
committedTransitions: Array<Transition> | null,
|
||||||
|
) {
|
||||||
|
// "Atomic" effects are ones that need to fire on every commit, even during
|
||||||
|
// pre-rendering. We call this function when traversing a hidden tree whose
|
||||||
|
// regular effects are currently disconnected.
|
||||||
|
const prevDebugFiber = getCurrentDebugFiberInDEV();
|
||||||
|
// TODO: Add special flag for atomic effects
|
||||||
|
if (parentFiber.subtreeFlags & PassiveMask) {
|
||||||
|
let child = parentFiber.child;
|
||||||
|
while (child !== null) {
|
||||||
|
setCurrentDebugFiberInDEV(child);
|
||||||
|
commitAtomicPassiveEffects(
|
||||||
|
finishedRoot,
|
||||||
|
child,
|
||||||
|
committedLanes,
|
||||||
|
committedTransitions,
|
||||||
|
);
|
||||||
|
child = child.sibling;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
setCurrentDebugFiberInDEV(prevDebugFiber);
|
||||||
|
}
|
||||||
|
|
||||||
|
function commitAtomicPassiveEffects(
|
||||||
|
finishedRoot: FiberRoot,
|
||||||
|
finishedWork: Fiber,
|
||||||
|
committedLanes: Lanes,
|
||||||
|
committedTransitions: Array<Transition> | null,
|
||||||
|
) {
|
||||||
|
// "Atomic" effects are ones that need to fire on every commit, even during
|
||||||
|
// pre-rendering. We call this function when traversing a hidden tree whose
|
||||||
|
// regular effects are currently disconnected.
|
||||||
|
const flags = finishedWork.flags;
|
||||||
|
switch (finishedWork.tag) {
|
||||||
|
case OffscreenComponent: {
|
||||||
|
recursivelyTraverseAtomicPassiveEffects(
|
||||||
|
finishedRoot,
|
||||||
|
finishedWork,
|
||||||
|
committedLanes,
|
||||||
|
committedTransitions,
|
||||||
|
);
|
||||||
|
if (flags & Passive) {
|
||||||
|
// TODO: Pass `current` as argument to this function
|
||||||
|
const current = finishedWork.alternate;
|
||||||
|
const instance: OffscreenInstance = finishedWork.stateNode;
|
||||||
|
commitOffscreenPassiveMountEffects(current, finishedWork, instance);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case CacheComponent: {
|
||||||
|
recursivelyTraverseAtomicPassiveEffects(
|
||||||
|
finishedRoot,
|
||||||
|
finishedWork,
|
||||||
|
committedLanes,
|
||||||
|
committedTransitions,
|
||||||
|
);
|
||||||
|
if (flags & Passive) {
|
||||||
|
// TODO: Pass `current` as argument to this function
|
||||||
|
const current = finishedWork.alternate;
|
||||||
|
commitCachePassiveMountEffect(current, finishedWork);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case TracingMarkerComponent: {
|
||||||
|
if (enableTransitionTracing) {
|
||||||
|
recursivelyTraverseAtomicPassiveEffects(
|
||||||
|
finishedRoot,
|
||||||
|
finishedWork,
|
||||||
|
committedLanes,
|
||||||
|
committedTransitions,
|
||||||
|
);
|
||||||
|
if (flags & Passive) {
|
||||||
|
commitTracingMarkerPassiveMountEffect(finishedWork);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
// Intentional fallthrough to next branch
|
||||||
|
}
|
||||||
|
// eslint-disable-next-line-no-fallthrough
|
||||||
|
default: {
|
||||||
|
recursivelyTraverseAtomicPassiveEffects(
|
||||||
|
finishedRoot,
|
||||||
|
finishedWork,
|
||||||
|
committedLanes,
|
||||||
|
committedTransitions,
|
||||||
|
);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
export function commitPassiveUnmountEffects(finishedWork: Fiber): void {
|
export function commitPassiveUnmountEffects(finishedWork: Fiber): void {
|
||||||
setCurrentDebugFiberInDEV(finishedWork);
|
setCurrentDebugFiberInDEV(finishedWork);
|
||||||
commitPassiveUnmountOnFiber(finishedWork);
|
commitPassiveUnmountOnFiber(finishedWork);
|
||||||
|
@ -3275,6 +3644,11 @@ function commitPassiveUnmountOnFiber(finishedWork: Fiber): void {
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
// TODO: Disconnect passive effects when a tree is hidden, perhaps after
|
||||||
|
// a delay.
|
||||||
|
// case OffscreenComponent: {
|
||||||
|
// ...
|
||||||
|
// }
|
||||||
default: {
|
default: {
|
||||||
recursivelyTraversePassiveUnmountEffects(finishedWork);
|
recursivelyTraversePassiveUnmountEffects(finishedWork);
|
||||||
break;
|
break;
|
||||||
|
|
|
@ -31,6 +31,7 @@ import {
|
||||||
} from './ReactFiberLane.new';
|
} from './ReactFiberLane.new';
|
||||||
import {NoFlags, Placement, Hydrating} from './ReactFiberFlags';
|
import {NoFlags, Placement, Hydrating} from './ReactFiberFlags';
|
||||||
import {HostRoot, OffscreenComponent} from './ReactWorkTags';
|
import {HostRoot, OffscreenComponent} from './ReactWorkTags';
|
||||||
|
import {OffscreenVisible} from './ReactFiberOffscreenComponent';
|
||||||
|
|
||||||
export type ConcurrentUpdate = {
|
export type ConcurrentUpdate = {
|
||||||
next: ConcurrentUpdate,
|
next: ConcurrentUpdate,
|
||||||
|
@ -217,7 +218,10 @@ function markUpdateLaneFromFiberToRoot(
|
||||||
// account for it. (There may be other cases that we haven't discovered,
|
// account for it. (There may be other cases that we haven't discovered,
|
||||||
// too.)
|
// too.)
|
||||||
const offscreenInstance: OffscreenInstance | null = parent.stateNode;
|
const offscreenInstance: OffscreenInstance | null = parent.stateNode;
|
||||||
if (offscreenInstance !== null && offscreenInstance.isHidden) {
|
if (
|
||||||
|
offscreenInstance !== null &&
|
||||||
|
!(offscreenInstance.visibility & OffscreenVisible)
|
||||||
|
) {
|
||||||
isHidden = true;
|
isHidden = true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -31,6 +31,7 @@ import {
|
||||||
} from './ReactFiberLane.old';
|
} from './ReactFiberLane.old';
|
||||||
import {NoFlags, Placement, Hydrating} from './ReactFiberFlags';
|
import {NoFlags, Placement, Hydrating} from './ReactFiberFlags';
|
||||||
import {HostRoot, OffscreenComponent} from './ReactWorkTags';
|
import {HostRoot, OffscreenComponent} from './ReactWorkTags';
|
||||||
|
import {OffscreenVisible} from './ReactFiberOffscreenComponent';
|
||||||
|
|
||||||
export type ConcurrentUpdate = {
|
export type ConcurrentUpdate = {
|
||||||
next: ConcurrentUpdate,
|
next: ConcurrentUpdate,
|
||||||
|
@ -217,7 +218,10 @@ function markUpdateLaneFromFiberToRoot(
|
||||||
// account for it. (There may be other cases that we haven't discovered,
|
// account for it. (There may be other cases that we haven't discovered,
|
||||||
// too.)
|
// too.)
|
||||||
const offscreenInstance: OffscreenInstance | null = parent.stateNode;
|
const offscreenInstance: OffscreenInstance | null = parent.stateNode;
|
||||||
if (offscreenInstance !== null && offscreenInstance.isHidden) {
|
if (
|
||||||
|
offscreenInstance !== null &&
|
||||||
|
!(offscreenInstance.visibility & OffscreenVisible)
|
||||||
|
) {
|
||||||
isHidden = true;
|
isHidden = true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -87,7 +87,7 @@ export const MutationMask =
|
||||||
export const LayoutMask = Update | Callback | Ref | Visibility;
|
export const LayoutMask = Update | Callback | Ref | Visibility;
|
||||||
|
|
||||||
// TODO: Split into PassiveMountMask and PassiveUnmountMask
|
// TODO: Split into PassiveMountMask and PassiveUnmountMask
|
||||||
export const PassiveMask = Passive | ChildDeletion;
|
export const PassiveMask = Passive | Visibility | ChildDeletion;
|
||||||
|
|
||||||
// Union of tags that don't get reset on clones.
|
// Union of tags that don't get reset on clones.
|
||||||
// This allows certain concepts to persist without recalculating them,
|
// This allows certain concepts to persist without recalculating them,
|
||||||
|
|
|
@ -42,8 +42,13 @@ export type OffscreenQueue = {|
|
||||||
wakeables: Set<Wakeable> | null,
|
wakeables: Set<Wakeable> | null,
|
||||||
|};
|
|};
|
||||||
|
|
||||||
|
type OffscreenVisibility = number;
|
||||||
|
|
||||||
|
export const OffscreenVisible = /* */ 0b01;
|
||||||
|
export const OffscreenPassiveEffectsConnected = /* */ 0b10;
|
||||||
|
|
||||||
export type OffscreenInstance = {|
|
export type OffscreenInstance = {|
|
||||||
isHidden: boolean,
|
visibility: OffscreenVisibility,
|
||||||
pendingMarkers: Set<TracingMarkerInstance> | null,
|
pendingMarkers: Set<TracingMarkerInstance> | null,
|
||||||
transitions: Set<Transition> | null,
|
transitions: Set<Transition> | null,
|
||||||
retryCache: WeakSet<Wakeable> | Set<Wakeable> | null,
|
retryCache: WeakSet<Wakeable> | Set<Wakeable> | null,
|
||||||
|
|
|
@ -6,6 +6,7 @@ let getCacheForType;
|
||||||
let Scheduler;
|
let Scheduler;
|
||||||
let act;
|
let act;
|
||||||
let Suspense;
|
let Suspense;
|
||||||
|
let Offscreen;
|
||||||
let useCacheRefresh;
|
let useCacheRefresh;
|
||||||
let startTransition;
|
let startTransition;
|
||||||
let useState;
|
let useState;
|
||||||
|
@ -23,6 +24,7 @@ describe('ReactCache', () => {
|
||||||
Scheduler = require('scheduler');
|
Scheduler = require('scheduler');
|
||||||
act = require('jest-react').act;
|
act = require('jest-react').act;
|
||||||
Suspense = React.Suspense;
|
Suspense = React.Suspense;
|
||||||
|
Offscreen = React.unstable_Offscreen;
|
||||||
getCacheSignal = React.unstable_getCacheSignal;
|
getCacheSignal = React.unstable_getCacheSignal;
|
||||||
getCacheForType = React.unstable_getCacheForType;
|
getCacheForType = React.unstable_getCacheForType;
|
||||||
useCacheRefresh = React.unstable_useCacheRefresh;
|
useCacheRefresh = React.unstable_useCacheRefresh;
|
||||||
|
@ -1590,4 +1592,36 @@ describe('ReactCache', () => {
|
||||||
]);
|
]);
|
||||||
expect(root).toMatchRenderedOutput('Bye!');
|
expect(root).toMatchRenderedOutput('Bye!');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// @gate enableOffscreen
|
||||||
|
// @gate enableCache
|
||||||
|
test('prerender a new cache boundary inside an Offscreen tree', async () => {
|
||||||
|
function App({prerenderMore}) {
|
||||||
|
return (
|
||||||
|
<Offscreen mode="hidden">
|
||||||
|
<div>
|
||||||
|
{prerenderMore ? (
|
||||||
|
<Cache>
|
||||||
|
<AsyncText text="More" />
|
||||||
|
</Cache>
|
||||||
|
) : null}
|
||||||
|
</div>
|
||||||
|
</Offscreen>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const root = ReactNoop.createRoot();
|
||||||
|
await act(async () => {
|
||||||
|
root.render(<App prerenderMore={false} />);
|
||||||
|
});
|
||||||
|
expect(Scheduler).toHaveYielded([]);
|
||||||
|
expect(root).toMatchRenderedOutput(<div hidden={true} />);
|
||||||
|
|
||||||
|
seedNextTextCache('More');
|
||||||
|
await act(async () => {
|
||||||
|
root.render(<App prerenderMore={true} />);
|
||||||
|
});
|
||||||
|
expect(Scheduler).toHaveYielded(['More']);
|
||||||
|
expect(root).toMatchRenderedOutput(<div hidden={true}>More</div>);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
|
@ -486,22 +486,29 @@ describe('ReactOffscreen', () => {
|
||||||
// hidden tree share the same lane, but are processed at different times
|
// hidden tree share the same lane, but are processed at different times
|
||||||
// because of the timing of when they were scheduled.
|
// because of the timing of when they were scheduled.
|
||||||
|
|
||||||
|
// This functions checks whether the "outer" and "inner" states are
|
||||||
|
// consistent in the rendered output.
|
||||||
|
let currentOuter = null;
|
||||||
|
let currentInner = null;
|
||||||
|
function areOuterAndInnerConsistent() {
|
||||||
|
return (
|
||||||
|
currentOuter === null ||
|
||||||
|
currentInner === null ||
|
||||||
|
currentOuter === currentInner
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
let setInner;
|
let setInner;
|
||||||
function Child({outer}) {
|
function Child() {
|
||||||
const [inner, _setInner] = useState(0);
|
const [inner, _setInner] = useState(0);
|
||||||
setInner = _setInner;
|
setInner = _setInner;
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
// Inner and outer values are always updated simultaneously, so they
|
currentInner = inner;
|
||||||
// should always be consistent.
|
return () => {
|
||||||
if (inner !== outer) {
|
currentInner = null;
|
||||||
Scheduler.unstable_yieldValue(
|
};
|
||||||
'Tearing! Inner and outer are inconsistent!',
|
}, [inner]);
|
||||||
);
|
|
||||||
} else {
|
|
||||||
Scheduler.unstable_yieldValue('Inner and outer are consistent');
|
|
||||||
}
|
|
||||||
}, [inner, outer]);
|
|
||||||
|
|
||||||
return <Text text={'Inner: ' + inner} />;
|
return <Text text={'Inner: ' + inner} />;
|
||||||
}
|
}
|
||||||
|
@ -510,11 +517,19 @@ describe('ReactOffscreen', () => {
|
||||||
function App({show}) {
|
function App({show}) {
|
||||||
const [outer, _setOuter] = useState(0);
|
const [outer, _setOuter] = useState(0);
|
||||||
setOuter = _setOuter;
|
setOuter = _setOuter;
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
currentOuter = outer;
|
||||||
|
return () => {
|
||||||
|
currentOuter = null;
|
||||||
|
};
|
||||||
|
}, [outer]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Text text={'Outer: ' + outer} />
|
<Text text={'Outer: ' + outer} />
|
||||||
<Offscreen mode={show ? 'visible' : 'hidden'}>
|
<Offscreen mode={show ? 'visible' : 'hidden'}>
|
||||||
<Child outer={outer} />
|
<Child />
|
||||||
</Offscreen>
|
</Offscreen>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
|
@ -525,17 +540,14 @@ describe('ReactOffscreen', () => {
|
||||||
await act(async () => {
|
await act(async () => {
|
||||||
root.render(<App show={false} />);
|
root.render(<App show={false} />);
|
||||||
});
|
});
|
||||||
expect(Scheduler).toHaveYielded([
|
expect(Scheduler).toHaveYielded(['Outer: 0', 'Inner: 0']);
|
||||||
'Outer: 0',
|
|
||||||
'Inner: 0',
|
|
||||||
'Inner and outer are consistent',
|
|
||||||
]);
|
|
||||||
expect(root).toMatchRenderedOutput(
|
expect(root).toMatchRenderedOutput(
|
||||||
<>
|
<>
|
||||||
<span prop="Outer: 0" />
|
<span prop="Outer: 0" />
|
||||||
<span hidden={true} prop="Inner: 0" />
|
<span hidden={true} prop="Inner: 0" />
|
||||||
</>,
|
</>,
|
||||||
);
|
);
|
||||||
|
expect(areOuterAndInnerConsistent()).toBe(true);
|
||||||
|
|
||||||
await act(async () => {
|
await act(async () => {
|
||||||
// Update a value both inside and outside the hidden tree. These values
|
// Update a value both inside and outside the hidden tree. These values
|
||||||
|
@ -570,8 +582,6 @@ describe('ReactOffscreen', () => {
|
||||||
// update were erroneously processed, then Inner would be inconsistent
|
// update were erroneously processed, then Inner would be inconsistent
|
||||||
// with Outer.
|
// with Outer.
|
||||||
'Inner: 1',
|
'Inner: 1',
|
||||||
|
|
||||||
'Inner and outer are consistent',
|
|
||||||
]);
|
]);
|
||||||
expect(root).toMatchRenderedOutput(
|
expect(root).toMatchRenderedOutput(
|
||||||
<>
|
<>
|
||||||
|
@ -579,18 +589,16 @@ describe('ReactOffscreen', () => {
|
||||||
<span prop="Inner: 1" />
|
<span prop="Inner: 1" />
|
||||||
</>,
|
</>,
|
||||||
);
|
);
|
||||||
|
expect(areOuterAndInnerConsistent()).toBe(true);
|
||||||
});
|
});
|
||||||
expect(Scheduler).toHaveYielded([
|
expect(Scheduler).toHaveYielded(['Outer: 2', 'Inner: 2']);
|
||||||
'Outer: 2',
|
|
||||||
'Inner: 2',
|
|
||||||
'Inner and outer are consistent',
|
|
||||||
]);
|
|
||||||
expect(root).toMatchRenderedOutput(
|
expect(root).toMatchRenderedOutput(
|
||||||
<>
|
<>
|
||||||
<span prop="Outer: 2" />
|
<span prop="Outer: 2" />
|
||||||
<span prop="Inner: 2" />
|
<span prop="Inner: 2" />
|
||||||
</>,
|
</>,
|
||||||
);
|
);
|
||||||
|
expect(areOuterAndInnerConsistent()).toBe(true);
|
||||||
});
|
});
|
||||||
|
|
||||||
// @gate enableOffscreen
|
// @gate enableOffscreen
|
||||||
|
@ -867,4 +875,127 @@ describe('ReactOffscreen', () => {
|
||||||
]);
|
]);
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// @gate enableOffscreen
|
||||||
|
it('defer passive effects when prerendering a new Offscreen tree', async () => {
|
||||||
|
function Child({label}) {
|
||||||
|
useEffect(() => {
|
||||||
|
Scheduler.unstable_yieldValue('Mount ' + label);
|
||||||
|
return () => {
|
||||||
|
Scheduler.unstable_yieldValue('Unmount ' + label);
|
||||||
|
};
|
||||||
|
}, [label]);
|
||||||
|
return <Text text={label} />;
|
||||||
|
}
|
||||||
|
|
||||||
|
function App({showMore}) {
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<Child label="Shell" />
|
||||||
|
<Offscreen mode={showMore ? 'visible' : 'hidden'}>
|
||||||
|
<Child label="More" />
|
||||||
|
</Offscreen>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const root = ReactNoop.createRoot();
|
||||||
|
|
||||||
|
// Mount the app without showing the extra content
|
||||||
|
await act(async () => {
|
||||||
|
root.render(<App showMore={false} />);
|
||||||
|
});
|
||||||
|
expect(Scheduler).toHaveYielded([
|
||||||
|
// First mount the outer visible shell
|
||||||
|
'Shell',
|
||||||
|
'Mount Shell',
|
||||||
|
|
||||||
|
// Then prerender the hidden extra context. The passive effects in the
|
||||||
|
// hidden tree should not fire
|
||||||
|
'More',
|
||||||
|
// Does not fire
|
||||||
|
// 'Mount More',
|
||||||
|
]);
|
||||||
|
// The hidden content has been prerendered
|
||||||
|
expect(root).toMatchRenderedOutput(
|
||||||
|
<>
|
||||||
|
<span prop="Shell" />
|
||||||
|
<span hidden={true} prop="More" />
|
||||||
|
</>,
|
||||||
|
);
|
||||||
|
|
||||||
|
// Reveal the prerendered tree
|
||||||
|
await act(async () => {
|
||||||
|
root.render(<App showMore={true} />);
|
||||||
|
});
|
||||||
|
expect(Scheduler).toHaveYielded([
|
||||||
|
'Shell',
|
||||||
|
'More',
|
||||||
|
|
||||||
|
// Mount the passive effects in the newly revealed tree, the ones that
|
||||||
|
// were skipped during pre-rendering.
|
||||||
|
'Mount More',
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
|
// @gate enableOffscreen
|
||||||
|
it("don't defer passive effects when prerendering in a tree whose effects are already connected", async () => {
|
||||||
|
function Child({label}) {
|
||||||
|
useEffect(() => {
|
||||||
|
Scheduler.unstable_yieldValue('Mount ' + label);
|
||||||
|
return () => {
|
||||||
|
Scheduler.unstable_yieldValue('Unmount ' + label);
|
||||||
|
};
|
||||||
|
}, [label]);
|
||||||
|
return <Text text={label} />;
|
||||||
|
}
|
||||||
|
|
||||||
|
function App({showMore, step}) {
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<Child label={'Shell ' + step} />
|
||||||
|
<Offscreen mode={showMore ? 'visible' : 'hidden'}>
|
||||||
|
<Child label={'More ' + step} />
|
||||||
|
</Offscreen>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const root = ReactNoop.createRoot();
|
||||||
|
|
||||||
|
// Mount the app, including the extra content
|
||||||
|
await act(async () => {
|
||||||
|
root.render(<App showMore={true} step={1} />);
|
||||||
|
});
|
||||||
|
expect(Scheduler).toHaveYielded([
|
||||||
|
'Shell 1',
|
||||||
|
'More 1',
|
||||||
|
'Mount Shell 1',
|
||||||
|
'Mount More 1',
|
||||||
|
]);
|
||||||
|
expect(root).toMatchRenderedOutput(
|
||||||
|
<>
|
||||||
|
<span prop="Shell 1" />
|
||||||
|
<span prop="More 1" />
|
||||||
|
</>,
|
||||||
|
);
|
||||||
|
|
||||||
|
// Hide the extra content. while also updating one of its props
|
||||||
|
await act(async () => {
|
||||||
|
root.render(<App showMore={false} step={2} />);
|
||||||
|
});
|
||||||
|
expect(Scheduler).toHaveYielded([
|
||||||
|
// First update the outer visible shell
|
||||||
|
'Shell 2',
|
||||||
|
'Unmount Shell 1',
|
||||||
|
'Mount Shell 2',
|
||||||
|
|
||||||
|
// Then prerender the update to the hidden content. Since the effects
|
||||||
|
// are already connected inside the hidden tree, we don't defer updates
|
||||||
|
// to them.
|
||||||
|
'More 2',
|
||||||
|
'Unmount More 1',
|
||||||
|
'Mount More 2',
|
||||||
|
]);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
Loading…
Reference in New Issue