diff --git a/packages/react-reconciler/src/ReactFiber.new.js b/packages/react-reconciler/src/ReactFiber.new.js index b8bbb0d07a..560a3d0ec3 100644 --- a/packages/react-reconciler/src/ReactFiber.new.js +++ b/packages/react-reconciler/src/ReactFiber.new.js @@ -61,6 +61,7 @@ import { CacheComponent, TracingMarkerComponent, } from './ReactWorkTags'; +import {OffscreenVisible} from './ReactFiberOffscreenComponent'; import getComponentNameFromFiber from 'react-reconciler/src/getComponentNameFromFiber'; import {isDevToolsPresent} from './ReactFiberDevToolsHook.new'; @@ -717,7 +718,7 @@ export function createFiberFromOffscreen( fiber.elementType = REACT_OFFSCREEN_TYPE; fiber.lanes = lanes; const primaryChildInstance: OffscreenInstance = { - isHidden: false, + visibility: OffscreenVisible, pendingMarkers: null, retryCache: null, transitions: null, @@ -738,7 +739,7 @@ export function createFiberFromLegacyHidden( // Adding a stateNode for legacy hidden because it's currently using // the offscreen implementation, which depends on a state node const instance: OffscreenInstance = { - isHidden: false, + visibility: OffscreenVisible, pendingMarkers: null, transitions: null, retryCache: null, diff --git a/packages/react-reconciler/src/ReactFiber.old.js b/packages/react-reconciler/src/ReactFiber.old.js index da24de2f63..0a29c4dce7 100644 --- a/packages/react-reconciler/src/ReactFiber.old.js +++ b/packages/react-reconciler/src/ReactFiber.old.js @@ -61,6 +61,7 @@ import { CacheComponent, TracingMarkerComponent, } from './ReactWorkTags'; +import {OffscreenVisible} from './ReactFiberOffscreenComponent'; import getComponentNameFromFiber from 'react-reconciler/src/getComponentNameFromFiber'; import {isDevToolsPresent} from './ReactFiberDevToolsHook.old'; @@ -717,7 +718,7 @@ export function createFiberFromOffscreen( fiber.elementType = REACT_OFFSCREEN_TYPE; fiber.lanes = lanes; const primaryChildInstance: OffscreenInstance = { - isHidden: false, + visibility: OffscreenVisible, pendingMarkers: null, retryCache: null, transitions: null, @@ -738,7 +739,7 @@ export function createFiberFromLegacyHidden( // Adding a stateNode for legacy hidden because it's currently using // the offscreen implementation, which depends on a state node const instance: OffscreenInstance = { - isHidden: false, + visibility: OffscreenVisible, pendingMarkers: null, transitions: null, retryCache: null, diff --git a/packages/react-reconciler/src/ReactFiberCommitWork.new.js b/packages/react-reconciler/src/ReactFiberCommitWork.new.js index 200e661c70..4f9b4ce426 100644 --- a/packages/react-reconciler/src/ReactFiberCommitWork.new.js +++ b/packages/react-reconciler/src/ReactFiberCommitWork.new.js @@ -173,6 +173,10 @@ import { } from './ReactFiberDevToolsHook.new'; import {releaseCache, retainCache} from './ReactFiberCacheComponent.new'; import {clearTransitionsForLanes} from './ReactFiberLane.new'; +import { + OffscreenVisible, + OffscreenPassiveEffectsConnected, +} from './ReactFiberOffscreenComponent'; let didWarnAboutUndefinedSnapshotBeforeUpdate: Set | null = null; if (__DEV__) { @@ -2424,14 +2428,8 @@ function commitMutationEffectsOnFiber( const offscreenFiber: Fiber = (finishedWork.child: any); if (offscreenFiber.flags & Visibility) { - const offscreenInstance: OffscreenInstance = offscreenFiber.stateNode; const newState: OffscreenState | null = offscreenFiber.memoizedState; 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) { const wasHidden = offscreenFiber.alternate !== null && @@ -2485,7 +2483,11 @@ function commitMutationEffectsOnFiber( // Track the current state on the Offscreen instance so we can // read it during an event - offscreenInstance.isHidden = isHidden; + if (isHidden) { + offscreenInstance.visibility &= ~OffscreenVisible; + } else { + offscreenInstance.visibility |= OffscreenVisible; + } if (isHidden) { if (!wasHidden) { @@ -2871,6 +2873,167 @@ function recursivelyTraverseReappearLayoutEffects( 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( root: FiberRoot, finishedWork: Fiber, @@ -2916,6 +3079,9 @@ function commitPassiveMountOnFiber( committedLanes: Lanes, committedTransitions: Array | null, ): 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; switch (finishedWork.tag) { case FunctionComponent: @@ -2928,31 +3094,10 @@ function commitPassiveMountOnFiber( committedTransitions, ); if (flags & Passive) { - if ( - enableProfilerTimer && - enableProfilerCommitHooks && - 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); - } - } + commitHookPassiveMountEffects( + finishedWork, + HookPassive | HookHasEffect, + ); } break; } @@ -3013,95 +3158,78 @@ function commitPassiveMountOnFiber( } case LegacyHiddenComponent: case OffscreenComponent: { - recursivelyTraversePassiveMountEffects( - finishedRoot, - finishedWork, - committedLanes, - committedTransitions, - ); + // TODO: Pass `current` as argument to this function + 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. + 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 (enableCache) { - let previousCache: Cache | null = null; - if ( - finishedWork.alternate !== null && - finishedWork.alternate.memoizedState !== null && - finishedWork.alternate.memoizedState.cachePool !== null - ) { - previousCache = finishedWork.alternate.memoizedState.cachePool.pool; - } - let nextCache: Cache | null = null; - if ( - finishedWork.memoizedState !== null && - finishedWork.memoizedState.cachePool !== null - ) { - nextCache = finishedWork.memoizedState.cachePool.pool; - } - // Retain/release the cache used for pending (suspended) nodes. - // Note that this is only reached in the non-suspended/visible case: - // when the content is suspended/hidden, the retain/release occurs - // via the parent Suspense component (see case above). - if (nextCache !== previousCache) { - if (nextCache != null) { - retainCache(nextCache); - } - if (previousCache != null) { - releaseCache(previousCache); - } - } - } - - 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); - } + const current = finishedWork.alternate; + commitOffscreenPassiveMountEffects(current, finishedWork, instance); } break; } @@ -3113,24 +3241,9 @@ function commitPassiveMountOnFiber( committedTransitions, ); if (flags & Passive) { - 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); - } - } - } + // TODO: Pass `current` as argument to this function + const current = finishedWork.alternate; + commitCachePassiveMountEffect(current, finishedWork); } break; } @@ -3143,23 +3256,7 @@ function commitPassiveMountOnFiber( committedTransitions, ); if (flags & Passive) { - // 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; - } + commitTracingMarkerPassiveMountEffect(finishedWork); } break; } @@ -3178,6 +3275,278 @@ function commitPassiveMountOnFiber( } } +function recursivelyTraverseReconnectPassiveEffects( + finishedRoot: FiberRoot, + parentFiber: Fiber, + committedLanes: Lanes, + committedTransitions: Array | 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 | 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 | 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 | 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 { setCurrentDebugFiberInDEV(finishedWork); commitPassiveUnmountOnFiber(finishedWork); @@ -3275,6 +3644,11 @@ function commitPassiveUnmountOnFiber(finishedWork: Fiber): void { } break; } + // TODO: Disconnect passive effects when a tree is hidden, perhaps after + // a delay. + // case OffscreenComponent: { + // ... + // } default: { recursivelyTraversePassiveUnmountEffects(finishedWork); break; diff --git a/packages/react-reconciler/src/ReactFiberCommitWork.old.js b/packages/react-reconciler/src/ReactFiberCommitWork.old.js index 4b979151da..bd16246302 100644 --- a/packages/react-reconciler/src/ReactFiberCommitWork.old.js +++ b/packages/react-reconciler/src/ReactFiberCommitWork.old.js @@ -173,6 +173,10 @@ import { } from './ReactFiberDevToolsHook.old'; import {releaseCache, retainCache} from './ReactFiberCacheComponent.old'; import {clearTransitionsForLanes} from './ReactFiberLane.old'; +import { + OffscreenVisible, + OffscreenPassiveEffectsConnected, +} from './ReactFiberOffscreenComponent'; let didWarnAboutUndefinedSnapshotBeforeUpdate: Set | null = null; if (__DEV__) { @@ -2424,14 +2428,8 @@ function commitMutationEffectsOnFiber( const offscreenFiber: Fiber = (finishedWork.child: any); if (offscreenFiber.flags & Visibility) { - const offscreenInstance: OffscreenInstance = offscreenFiber.stateNode; const newState: OffscreenState | null = offscreenFiber.memoizedState; 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) { const wasHidden = offscreenFiber.alternate !== null && @@ -2485,7 +2483,11 @@ function commitMutationEffectsOnFiber( // Track the current state on the Offscreen instance so we can // read it during an event - offscreenInstance.isHidden = isHidden; + if (isHidden) { + offscreenInstance.visibility &= ~OffscreenVisible; + } else { + offscreenInstance.visibility |= OffscreenVisible; + } if (isHidden) { if (!wasHidden) { @@ -2871,6 +2873,167 @@ function recursivelyTraverseReappearLayoutEffects( 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( root: FiberRoot, finishedWork: Fiber, @@ -2916,6 +3079,9 @@ function commitPassiveMountOnFiber( committedLanes: Lanes, committedTransitions: Array | null, ): 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; switch (finishedWork.tag) { case FunctionComponent: @@ -2928,31 +3094,10 @@ function commitPassiveMountOnFiber( committedTransitions, ); if (flags & Passive) { - if ( - enableProfilerTimer && - enableProfilerCommitHooks && - 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); - } - } + commitHookPassiveMountEffects( + finishedWork, + HookPassive | HookHasEffect, + ); } break; } @@ -3013,95 +3158,78 @@ function commitPassiveMountOnFiber( } case LegacyHiddenComponent: case OffscreenComponent: { - recursivelyTraversePassiveMountEffects( - finishedRoot, - finishedWork, - committedLanes, - committedTransitions, - ); + // TODO: Pass `current` as argument to this function + 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. + 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 (enableCache) { - let previousCache: Cache | null = null; - if ( - finishedWork.alternate !== null && - finishedWork.alternate.memoizedState !== null && - finishedWork.alternate.memoizedState.cachePool !== null - ) { - previousCache = finishedWork.alternate.memoizedState.cachePool.pool; - } - let nextCache: Cache | null = null; - if ( - finishedWork.memoizedState !== null && - finishedWork.memoizedState.cachePool !== null - ) { - nextCache = finishedWork.memoizedState.cachePool.pool; - } - // Retain/release the cache used for pending (suspended) nodes. - // Note that this is only reached in the non-suspended/visible case: - // when the content is suspended/hidden, the retain/release occurs - // via the parent Suspense component (see case above). - if (nextCache !== previousCache) { - if (nextCache != null) { - retainCache(nextCache); - } - if (previousCache != null) { - releaseCache(previousCache); - } - } - } - - 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); - } + const current = finishedWork.alternate; + commitOffscreenPassiveMountEffects(current, finishedWork, instance); } break; } @@ -3113,24 +3241,9 @@ function commitPassiveMountOnFiber( committedTransitions, ); if (flags & Passive) { - 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); - } - } - } + // TODO: Pass `current` as argument to this function + const current = finishedWork.alternate; + commitCachePassiveMountEffect(current, finishedWork); } break; } @@ -3143,23 +3256,7 @@ function commitPassiveMountOnFiber( committedTransitions, ); if (flags & Passive) { - // 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; - } + commitTracingMarkerPassiveMountEffect(finishedWork); } break; } @@ -3178,6 +3275,278 @@ function commitPassiveMountOnFiber( } } +function recursivelyTraverseReconnectPassiveEffects( + finishedRoot: FiberRoot, + parentFiber: Fiber, + committedLanes: Lanes, + committedTransitions: Array | 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 | 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 | 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 | 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 { setCurrentDebugFiberInDEV(finishedWork); commitPassiveUnmountOnFiber(finishedWork); @@ -3275,6 +3644,11 @@ function commitPassiveUnmountOnFiber(finishedWork: Fiber): void { } break; } + // TODO: Disconnect passive effects when a tree is hidden, perhaps after + // a delay. + // case OffscreenComponent: { + // ... + // } default: { recursivelyTraversePassiveUnmountEffects(finishedWork); break; diff --git a/packages/react-reconciler/src/ReactFiberConcurrentUpdates.new.js b/packages/react-reconciler/src/ReactFiberConcurrentUpdates.new.js index 7a29e8166b..e496389782 100644 --- a/packages/react-reconciler/src/ReactFiberConcurrentUpdates.new.js +++ b/packages/react-reconciler/src/ReactFiberConcurrentUpdates.new.js @@ -31,6 +31,7 @@ import { } from './ReactFiberLane.new'; import {NoFlags, Placement, Hydrating} from './ReactFiberFlags'; import {HostRoot, OffscreenComponent} from './ReactWorkTags'; +import {OffscreenVisible} from './ReactFiberOffscreenComponent'; export type ConcurrentUpdate = { next: ConcurrentUpdate, @@ -217,7 +218,10 @@ function markUpdateLaneFromFiberToRoot( // account for it. (There may be other cases that we haven't discovered, // too.) const offscreenInstance: OffscreenInstance | null = parent.stateNode; - if (offscreenInstance !== null && offscreenInstance.isHidden) { + if ( + offscreenInstance !== null && + !(offscreenInstance.visibility & OffscreenVisible) + ) { isHidden = true; } } diff --git a/packages/react-reconciler/src/ReactFiberConcurrentUpdates.old.js b/packages/react-reconciler/src/ReactFiberConcurrentUpdates.old.js index 0d2cbb44c9..7dd87358cf 100644 --- a/packages/react-reconciler/src/ReactFiberConcurrentUpdates.old.js +++ b/packages/react-reconciler/src/ReactFiberConcurrentUpdates.old.js @@ -31,6 +31,7 @@ import { } from './ReactFiberLane.old'; import {NoFlags, Placement, Hydrating} from './ReactFiberFlags'; import {HostRoot, OffscreenComponent} from './ReactWorkTags'; +import {OffscreenVisible} from './ReactFiberOffscreenComponent'; export type ConcurrentUpdate = { next: ConcurrentUpdate, @@ -217,7 +218,10 @@ function markUpdateLaneFromFiberToRoot( // account for it. (There may be other cases that we haven't discovered, // too.) const offscreenInstance: OffscreenInstance | null = parent.stateNode; - if (offscreenInstance !== null && offscreenInstance.isHidden) { + if ( + offscreenInstance !== null && + !(offscreenInstance.visibility & OffscreenVisible) + ) { isHidden = true; } } diff --git a/packages/react-reconciler/src/ReactFiberFlags.js b/packages/react-reconciler/src/ReactFiberFlags.js index d22d9a189b..961c94bdd1 100644 --- a/packages/react-reconciler/src/ReactFiberFlags.js +++ b/packages/react-reconciler/src/ReactFiberFlags.js @@ -87,7 +87,7 @@ export const MutationMask = export const LayoutMask = Update | Callback | Ref | Visibility; // 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. // This allows certain concepts to persist without recalculating them, diff --git a/packages/react-reconciler/src/ReactFiberOffscreenComponent.js b/packages/react-reconciler/src/ReactFiberOffscreenComponent.js index ca1a393841..7bdaf83b90 100644 --- a/packages/react-reconciler/src/ReactFiberOffscreenComponent.js +++ b/packages/react-reconciler/src/ReactFiberOffscreenComponent.js @@ -42,8 +42,13 @@ export type OffscreenQueue = {| wakeables: Set | null, |}; +type OffscreenVisibility = number; + +export const OffscreenVisible = /* */ 0b01; +export const OffscreenPassiveEffectsConnected = /* */ 0b10; + export type OffscreenInstance = {| - isHidden: boolean, + visibility: OffscreenVisibility, pendingMarkers: Set | null, transitions: Set | null, retryCache: WeakSet | Set | null, diff --git a/packages/react-reconciler/src/__tests__/ReactCache-test.js b/packages/react-reconciler/src/__tests__/ReactCache-test.js index 804486721f..970bf940f2 100644 --- a/packages/react-reconciler/src/__tests__/ReactCache-test.js +++ b/packages/react-reconciler/src/__tests__/ReactCache-test.js @@ -6,6 +6,7 @@ let getCacheForType; let Scheduler; let act; let Suspense; +let Offscreen; let useCacheRefresh; let startTransition; let useState; @@ -23,6 +24,7 @@ describe('ReactCache', () => { Scheduler = require('scheduler'); act = require('jest-react').act; Suspense = React.Suspense; + Offscreen = React.unstable_Offscreen; getCacheSignal = React.unstable_getCacheSignal; getCacheForType = React.unstable_getCacheForType; useCacheRefresh = React.unstable_useCacheRefresh; @@ -1590,4 +1592,36 @@ describe('ReactCache', () => { ]); expect(root).toMatchRenderedOutput('Bye!'); }); + + // @gate enableOffscreen + // @gate enableCache + test('prerender a new cache boundary inside an Offscreen tree', async () => { + function App({prerenderMore}) { + return ( + +
+ {prerenderMore ? ( + + + + ) : null} +
+
+ ); + } + + const root = ReactNoop.createRoot(); + await act(async () => { + root.render(); + }); + expect(Scheduler).toHaveYielded([]); + expect(root).toMatchRenderedOutput(