diff --git a/packages/react-reconciler/src/ReactFiber.new.js b/packages/react-reconciler/src/ReactFiber.new.js index d6a6c8f7adc19..46f35fabf1fef 100644 --- a/packages/react-reconciler/src/ReactFiber.new.js +++ b/packages/react-reconciler/src/ReactFiber.new.js @@ -719,10 +719,10 @@ export function createFiberFromOffscreen( fiber.elementType = REACT_OFFSCREEN_TYPE; fiber.lanes = lanes; const primaryChildInstance: OffscreenInstance = { - visibility: OffscreenVisible, - pendingMarkers: null, - retryCache: null, - transitions: null, + _visibility: OffscreenVisible, + _pendingMarkers: null, + _retryCache: null, + _transitions: null, }; fiber.stateNode = primaryChildInstance; return fiber; @@ -740,10 +740,10 @@ 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 = { - visibility: OffscreenVisible, - pendingMarkers: null, - transitions: null, - retryCache: null, + _visibility: OffscreenVisible, + _pendingMarkers: null, + _transitions: null, + _retryCache: null, }; fiber.stateNode = instance; return fiber; diff --git a/packages/react-reconciler/src/ReactFiber.old.js b/packages/react-reconciler/src/ReactFiber.old.js index d8f1ef424bd13..22712b6e18eed 100644 --- a/packages/react-reconciler/src/ReactFiber.old.js +++ b/packages/react-reconciler/src/ReactFiber.old.js @@ -719,10 +719,10 @@ export function createFiberFromOffscreen( fiber.elementType = REACT_OFFSCREEN_TYPE; fiber.lanes = lanes; const primaryChildInstance: OffscreenInstance = { - visibility: OffscreenVisible, - pendingMarkers: null, - retryCache: null, - transitions: null, + _visibility: OffscreenVisible, + _pendingMarkers: null, + _retryCache: null, + _transitions: null, }; fiber.stateNode = primaryChildInstance; return fiber; @@ -740,10 +740,10 @@ 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 = { - visibility: OffscreenVisible, - pendingMarkers: null, - transitions: null, - retryCache: null, + _visibility: OffscreenVisible, + _pendingMarkers: null, + _transitions: null, + _retryCache: null, }; fiber.stateNode = instance; return fiber; diff --git a/packages/react-reconciler/src/ReactFiberBeginWork.new.js b/packages/react-reconciler/src/ReactFiberBeginWork.new.js index fa417e08114c9..e0e605b14df36 100644 --- a/packages/react-reconciler/src/ReactFiberBeginWork.new.js +++ b/packages/react-reconciler/src/ReactFiberBeginWork.new.js @@ -677,6 +677,8 @@ function updateOffscreenComponent( const prevState: OffscreenState | null = current !== null ? current.memoizedState : null; + markRef(current, workInProgress); + if ( nextProps.mode === 'hidden' || (enableLegacyHidden && nextProps.mode === 'unstable-defer-without-hiding') @@ -811,8 +813,8 @@ function updateOffscreenComponent( // We have now gone from hidden to visible, so any transitions should // be added to the stack to get added to any Offscreen/suspense children const instance: OffscreenInstance | null = workInProgress.stateNode; - if (instance !== null && instance.transitions != null) { - transitions = Array.from(instance.transitions); + if (instance !== null && instance._transitions != null) { + transitions = Array.from(instance._transitions); } } diff --git a/packages/react-reconciler/src/ReactFiberBeginWork.old.js b/packages/react-reconciler/src/ReactFiberBeginWork.old.js index 449f306e7ef89..acbba6a6bedd1 100644 --- a/packages/react-reconciler/src/ReactFiberBeginWork.old.js +++ b/packages/react-reconciler/src/ReactFiberBeginWork.old.js @@ -677,6 +677,8 @@ function updateOffscreenComponent( const prevState: OffscreenState | null = current !== null ? current.memoizedState : null; + markRef(current, workInProgress); + if ( nextProps.mode === 'hidden' || (enableLegacyHidden && nextProps.mode === 'unstable-defer-without-hiding') @@ -811,8 +813,8 @@ function updateOffscreenComponent( // We have now gone from hidden to visible, so any transitions should // be added to the stack to get added to any Offscreen/suspense children const instance: OffscreenInstance | null = workInProgress.stateNode; - if (instance !== null && instance.transitions != null) { - transitions = Array.from(instance.transitions); + if (instance !== null && instance._transitions != null) { + transitions = Array.from(instance._transitions); } } diff --git a/packages/react-reconciler/src/ReactFiberCommitWork.new.js b/packages/react-reconciler/src/ReactFiberCommitWork.new.js index c70e4ae3470e7..dabc8b1987e8b 100644 --- a/packages/react-reconciler/src/ReactFiberCommitWork.new.js +++ b/packages/react-reconciler/src/ReactFiberCommitWork.new.js @@ -26,6 +26,7 @@ import type { OffscreenState, OffscreenInstance, OffscreenQueue, + OffscreenProps, } from './ReactFiberOffscreenComponent'; import type {HookFlags} from './ReactHookEffectTags'; import type {Cache} from './ReactFiberCacheComponent.new'; @@ -1123,6 +1124,14 @@ function commitLayoutEffectOnFiber( committedLanes, ); } + if (flags & Ref) { + const props: OffscreenProps = finishedWork.memoizedProps; + if (props.mode === 'manual') { + safelyAttachRef(finishedWork, finishedWork.return); + } else { + safelyDetachRef(finishedWork, finishedWork.return); + } + } break; } default: { @@ -1296,7 +1305,7 @@ function commitTransitionProgress(offscreenFiber: Fiber) { const wasHidden = prevState !== null; const isHidden = nextState !== null; - const pendingMarkers = offscreenInstance.pendingMarkers; + const pendingMarkers = offscreenInstance._pendingMarkers; // If there is a name on the suspense boundary, store that in // the pending boundaries. let name = null; @@ -2126,6 +2135,7 @@ function commitDeletionEffectsOnFiber( return; } case OffscreenComponent: { + safelyDetachRef(deletedFiber, nearestMountedAncestor); if (deletedFiber.mode & ConcurrentMode) { // If this offscreen component is hidden, we already unmounted it. Before // deleting the children, track that it's already unmounted so that we @@ -2232,9 +2242,9 @@ function getRetryCache(finishedWork) { } case OffscreenComponent: { const instance: OffscreenInstance = finishedWork.stateNode; - let retryCache = instance.retryCache; + let retryCache = instance._retryCache; if (retryCache === null) { - retryCache = instance.retryCache = new PossiblyWeakSet(); + retryCache = instance._retryCache = new PossiblyWeakSet(); } return retryCache; } @@ -2605,6 +2615,12 @@ function commitMutationEffectsOnFiber( return; } case OffscreenComponent: { + if (flags & Ref) { + if (current !== null) { + safelyDetachRef(current, current.return); + } + } + const newState: OffscreenState | null = finishedWork.memoizedState; const isHidden = newState !== null; const wasHidden = current !== null && current.memoizedState !== null; @@ -2633,9 +2649,9 @@ function commitMutationEffectsOnFiber( // Track the current state on the Offscreen instance so we can // read it during an event if (isHidden) { - offscreenInstance.visibility &= ~OffscreenVisible; + offscreenInstance._visibility &= ~OffscreenVisible; } else { - offscreenInstance.visibility |= OffscreenVisible; + offscreenInstance._visibility |= OffscreenVisible; } if (isHidden) { @@ -2820,6 +2836,9 @@ export function disappearLayoutEffects(finishedWork: Fiber) { break; } case OffscreenComponent: { + // TODO (Offscreen) Check: flags & RefStatic + safelyDetachRef(finishedWork, finishedWork.return); + const isHidden = finishedWork.memoizedState !== null; if (isHidden) { // Nested Offscreen tree is already hidden. Don't disappear @@ -2967,6 +2986,8 @@ export function reappearLayoutEffects( includeWorkInProgressEffects, ); } + // TODO: Check flags & Ref + safelyAttachRef(finishedWork, finishedWork.return); break; } default: { @@ -3080,10 +3101,10 @@ function commitOffscreenPassiveMountEffects( // 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(); + if (instance._transitions === null) { + instance._transitions = new Set(); } - instance.transitions.add(transition); + instance._transitions.add(transition); }); } @@ -3096,17 +3117,17 @@ function commitOffscreenPassiveMountEffects( // caused them if (markerTransitions !== null) { markerTransitions.forEach(transition => { - if (instance.transitions === null) { - instance.transitions = new Set(); - } else if (instance.transitions.has(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(); + if (instance._pendingMarkers === null) { + instance._pendingMarkers = new Set(); } - instance.pendingMarkers.add(markerInstance); + instance._pendingMarkers.add(markerInstance); } }); } @@ -3121,8 +3142,8 @@ function commitOffscreenPassiveMountEffects( // TODO: Refactor this into an if/else branch if (!isHidden) { - instance.transitions = null; - instance.pendingMarkers = null; + instance._transitions = null; + instance._pendingMarkers = null; } } } @@ -3302,7 +3323,7 @@ function commitPassiveMountOnFiber( const isHidden = nextState !== null; if (isHidden) { - if (instance.visibility & OffscreenPassiveEffectsConnected) { + if (instance._visibility & OffscreenPassiveEffectsConnected) { // The effects are currently connected. Update them. recursivelyTraversePassiveMountEffects( finishedRoot, @@ -3327,7 +3348,7 @@ function commitPassiveMountOnFiber( } } else { // Legacy Mode: Fire the effects even if the tree is hidden. - instance.visibility |= OffscreenPassiveEffectsConnected; + instance._visibility |= OffscreenPassiveEffectsConnected; recursivelyTraversePassiveMountEffects( finishedRoot, finishedWork, @@ -3338,7 +3359,7 @@ function commitPassiveMountOnFiber( } } else { // Tree is visible - if (instance.visibility & OffscreenPassiveEffectsConnected) { + if (instance._visibility & OffscreenPassiveEffectsConnected) { // The effects are currently connected. Update them. recursivelyTraversePassiveMountEffects( finishedRoot, @@ -3350,7 +3371,7 @@ function commitPassiveMountOnFiber( // 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; + instance._visibility |= OffscreenPassiveEffectsConnected; const includeWorkInProgressEffects = (finishedWork.subtreeFlags & PassiveMask) !== NoFlags; @@ -3482,7 +3503,7 @@ export function reconnectPassiveEffects( const isHidden = nextState !== null; if (isHidden) { - if (instance.visibility & OffscreenPassiveEffectsConnected) { + if (instance._visibility & OffscreenPassiveEffectsConnected) { // The effects are currently connected. Update them. recursivelyTraverseReconnectPassiveEffects( finishedRoot, @@ -3508,7 +3529,7 @@ export function reconnectPassiveEffects( } } else { // Legacy Mode: Fire the effects even if the tree is hidden. - instance.visibility |= OffscreenPassiveEffectsConnected; + instance._visibility |= OffscreenPassiveEffectsConnected; recursivelyTraverseReconnectPassiveEffects( finishedRoot, finishedWork, @@ -3526,7 +3547,7 @@ export function reconnectPassiveEffects( // continue traversing the tree and firing all the effects. // // We do need to set the "connected" flag on the instance, though. - instance.visibility |= OffscreenPassiveEffectsConnected; + instance._visibility |= OffscreenPassiveEffectsConnected; recursivelyTraverseReconnectPassiveEffects( finishedRoot, @@ -3781,7 +3802,7 @@ function commitPassiveUnmountOnFiber(finishedWork: Fiber): void { if ( isHidden && - instance.visibility & OffscreenPassiveEffectsConnected && + instance._visibility & OffscreenPassiveEffectsConnected && // For backwards compatibility, don't unmount when a tree suspends. In // the future we may change this to unmount after a delay. (finishedWork.return === null || @@ -3791,7 +3812,7 @@ function commitPassiveUnmountOnFiber(finishedWork: Fiber): void { // TODO: Add option or heuristic to delay before disconnecting the // effects. Then if the tree reappears before the delay has elapsed, we // can skip toggling the effects entirely. - instance.visibility &= ~OffscreenPassiveEffectsConnected; + instance._visibility &= ~OffscreenPassiveEffectsConnected; recursivelyTraverseDisconnectPassiveEffects(finishedWork); } else { recursivelyTraversePassiveUnmountEffects(finishedWork); @@ -3855,8 +3876,8 @@ export function disconnectPassiveEffect(finishedWork: Fiber): void { } case OffscreenComponent: { const instance: OffscreenInstance = finishedWork.stateNode; - if (instance.visibility & OffscreenPassiveEffectsConnected) { - instance.visibility &= ~OffscreenPassiveEffectsConnected; + if (instance._visibility & OffscreenPassiveEffectsConnected) { + instance._visibility &= ~OffscreenPassiveEffectsConnected; recursivelyTraverseDisconnectPassiveEffects(finishedWork); } else { // The effects are already disconnected. @@ -3984,7 +4005,7 @@ function commitPassiveUnmountInsideDeletedTreeOnFiber( // We need to mark this fiber's parents as deleted const offscreenFiber: Fiber = (current.child: any); const instance: OffscreenInstance = offscreenFiber.stateNode; - const transitions = instance.transitions; + const transitions = instance._transitions; if (transitions !== null) { const abortReason = { reason: 'suspense', diff --git a/packages/react-reconciler/src/ReactFiberCommitWork.old.js b/packages/react-reconciler/src/ReactFiberCommitWork.old.js index 4b5076a61c51f..a5cad4407b63a 100644 --- a/packages/react-reconciler/src/ReactFiberCommitWork.old.js +++ b/packages/react-reconciler/src/ReactFiberCommitWork.old.js @@ -26,6 +26,7 @@ import type { OffscreenState, OffscreenInstance, OffscreenQueue, + OffscreenProps, } from './ReactFiberOffscreenComponent'; import type {HookFlags} from './ReactHookEffectTags'; import type {Cache} from './ReactFiberCacheComponent.old'; @@ -1123,6 +1124,14 @@ function commitLayoutEffectOnFiber( committedLanes, ); } + if (flags & Ref) { + const props: OffscreenProps = finishedWork.memoizedProps; + if (props.mode === 'manual') { + safelyAttachRef(finishedWork, finishedWork.return); + } else { + safelyDetachRef(finishedWork, finishedWork.return); + } + } break; } default: { @@ -1296,7 +1305,7 @@ function commitTransitionProgress(offscreenFiber: Fiber) { const wasHidden = prevState !== null; const isHidden = nextState !== null; - const pendingMarkers = offscreenInstance.pendingMarkers; + const pendingMarkers = offscreenInstance._pendingMarkers; // If there is a name on the suspense boundary, store that in // the pending boundaries. let name = null; @@ -2126,6 +2135,7 @@ function commitDeletionEffectsOnFiber( return; } case OffscreenComponent: { + safelyDetachRef(deletedFiber, nearestMountedAncestor); if (deletedFiber.mode & ConcurrentMode) { // If this offscreen component is hidden, we already unmounted it. Before // deleting the children, track that it's already unmounted so that we @@ -2232,9 +2242,9 @@ function getRetryCache(finishedWork) { } case OffscreenComponent: { const instance: OffscreenInstance = finishedWork.stateNode; - let retryCache = instance.retryCache; + let retryCache = instance._retryCache; if (retryCache === null) { - retryCache = instance.retryCache = new PossiblyWeakSet(); + retryCache = instance._retryCache = new PossiblyWeakSet(); } return retryCache; } @@ -2605,6 +2615,12 @@ function commitMutationEffectsOnFiber( return; } case OffscreenComponent: { + if (flags & Ref) { + if (current !== null) { + safelyDetachRef(current, current.return); + } + } + const newState: OffscreenState | null = finishedWork.memoizedState; const isHidden = newState !== null; const wasHidden = current !== null && current.memoizedState !== null; @@ -2633,9 +2649,9 @@ function commitMutationEffectsOnFiber( // Track the current state on the Offscreen instance so we can // read it during an event if (isHidden) { - offscreenInstance.visibility &= ~OffscreenVisible; + offscreenInstance._visibility &= ~OffscreenVisible; } else { - offscreenInstance.visibility |= OffscreenVisible; + offscreenInstance._visibility |= OffscreenVisible; } if (isHidden) { @@ -2820,6 +2836,9 @@ export function disappearLayoutEffects(finishedWork: Fiber) { break; } case OffscreenComponent: { + // TODO (Offscreen) Check: flags & RefStatic + safelyDetachRef(finishedWork, finishedWork.return); + const isHidden = finishedWork.memoizedState !== null; if (isHidden) { // Nested Offscreen tree is already hidden. Don't disappear @@ -2967,6 +2986,8 @@ export function reappearLayoutEffects( includeWorkInProgressEffects, ); } + // TODO: Check flags & Ref + safelyAttachRef(finishedWork, finishedWork.return); break; } default: { @@ -3080,10 +3101,10 @@ function commitOffscreenPassiveMountEffects( // 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(); + if (instance._transitions === null) { + instance._transitions = new Set(); } - instance.transitions.add(transition); + instance._transitions.add(transition); }); } @@ -3096,17 +3117,17 @@ function commitOffscreenPassiveMountEffects( // caused them if (markerTransitions !== null) { markerTransitions.forEach(transition => { - if (instance.transitions === null) { - instance.transitions = new Set(); - } else if (instance.transitions.has(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(); + if (instance._pendingMarkers === null) { + instance._pendingMarkers = new Set(); } - instance.pendingMarkers.add(markerInstance); + instance._pendingMarkers.add(markerInstance); } }); } @@ -3121,8 +3142,8 @@ function commitOffscreenPassiveMountEffects( // TODO: Refactor this into an if/else branch if (!isHidden) { - instance.transitions = null; - instance.pendingMarkers = null; + instance._transitions = null; + instance._pendingMarkers = null; } } } @@ -3302,7 +3323,7 @@ function commitPassiveMountOnFiber( const isHidden = nextState !== null; if (isHidden) { - if (instance.visibility & OffscreenPassiveEffectsConnected) { + if (instance._visibility & OffscreenPassiveEffectsConnected) { // The effects are currently connected. Update them. recursivelyTraversePassiveMountEffects( finishedRoot, @@ -3327,7 +3348,7 @@ function commitPassiveMountOnFiber( } } else { // Legacy Mode: Fire the effects even if the tree is hidden. - instance.visibility |= OffscreenPassiveEffectsConnected; + instance._visibility |= OffscreenPassiveEffectsConnected; recursivelyTraversePassiveMountEffects( finishedRoot, finishedWork, @@ -3338,7 +3359,7 @@ function commitPassiveMountOnFiber( } } else { // Tree is visible - if (instance.visibility & OffscreenPassiveEffectsConnected) { + if (instance._visibility & OffscreenPassiveEffectsConnected) { // The effects are currently connected. Update them. recursivelyTraversePassiveMountEffects( finishedRoot, @@ -3350,7 +3371,7 @@ function commitPassiveMountOnFiber( // 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; + instance._visibility |= OffscreenPassiveEffectsConnected; const includeWorkInProgressEffects = (finishedWork.subtreeFlags & PassiveMask) !== NoFlags; @@ -3482,7 +3503,7 @@ export function reconnectPassiveEffects( const isHidden = nextState !== null; if (isHidden) { - if (instance.visibility & OffscreenPassiveEffectsConnected) { + if (instance._visibility & OffscreenPassiveEffectsConnected) { // The effects are currently connected. Update them. recursivelyTraverseReconnectPassiveEffects( finishedRoot, @@ -3508,7 +3529,7 @@ export function reconnectPassiveEffects( } } else { // Legacy Mode: Fire the effects even if the tree is hidden. - instance.visibility |= OffscreenPassiveEffectsConnected; + instance._visibility |= OffscreenPassiveEffectsConnected; recursivelyTraverseReconnectPassiveEffects( finishedRoot, finishedWork, @@ -3526,7 +3547,7 @@ export function reconnectPassiveEffects( // continue traversing the tree and firing all the effects. // // We do need to set the "connected" flag on the instance, though. - instance.visibility |= OffscreenPassiveEffectsConnected; + instance._visibility |= OffscreenPassiveEffectsConnected; recursivelyTraverseReconnectPassiveEffects( finishedRoot, @@ -3781,7 +3802,7 @@ function commitPassiveUnmountOnFiber(finishedWork: Fiber): void { if ( isHidden && - instance.visibility & OffscreenPassiveEffectsConnected && + instance._visibility & OffscreenPassiveEffectsConnected && // For backwards compatibility, don't unmount when a tree suspends. In // the future we may change this to unmount after a delay. (finishedWork.return === null || @@ -3791,7 +3812,7 @@ function commitPassiveUnmountOnFiber(finishedWork: Fiber): void { // TODO: Add option or heuristic to delay before disconnecting the // effects. Then if the tree reappears before the delay has elapsed, we // can skip toggling the effects entirely. - instance.visibility &= ~OffscreenPassiveEffectsConnected; + instance._visibility &= ~OffscreenPassiveEffectsConnected; recursivelyTraverseDisconnectPassiveEffects(finishedWork); } else { recursivelyTraversePassiveUnmountEffects(finishedWork); @@ -3855,8 +3876,8 @@ export function disconnectPassiveEffect(finishedWork: Fiber): void { } case OffscreenComponent: { const instance: OffscreenInstance = finishedWork.stateNode; - if (instance.visibility & OffscreenPassiveEffectsConnected) { - instance.visibility &= ~OffscreenPassiveEffectsConnected; + if (instance._visibility & OffscreenPassiveEffectsConnected) { + instance._visibility &= ~OffscreenPassiveEffectsConnected; recursivelyTraverseDisconnectPassiveEffects(finishedWork); } else { // The effects are already disconnected. @@ -3984,7 +4005,7 @@ function commitPassiveUnmountInsideDeletedTreeOnFiber( // We need to mark this fiber's parents as deleted const offscreenFiber: Fiber = (current.child: any); const instance: OffscreenInstance = offscreenFiber.stateNode; - const transitions = instance.transitions; + const transitions = instance._transitions; if (transitions !== null) { const abortReason = { reason: 'suspense', diff --git a/packages/react-reconciler/src/ReactFiberConcurrentUpdates.new.js b/packages/react-reconciler/src/ReactFiberConcurrentUpdates.new.js index 6b1ecbde7f018..ba5c68fb0d0ca 100644 --- a/packages/react-reconciler/src/ReactFiberConcurrentUpdates.new.js +++ b/packages/react-reconciler/src/ReactFiberConcurrentUpdates.new.js @@ -220,7 +220,7 @@ function markUpdateLaneFromFiberToRoot( const offscreenInstance: OffscreenInstance | null = parent.stateNode; if ( offscreenInstance !== null && - !(offscreenInstance.visibility & OffscreenVisible) + !(offscreenInstance._visibility & OffscreenVisible) ) { isHidden = true; } diff --git a/packages/react-reconciler/src/ReactFiberConcurrentUpdates.old.js b/packages/react-reconciler/src/ReactFiberConcurrentUpdates.old.js index 798506fd9b5ea..de1450fc228a9 100644 --- a/packages/react-reconciler/src/ReactFiberConcurrentUpdates.old.js +++ b/packages/react-reconciler/src/ReactFiberConcurrentUpdates.old.js @@ -220,7 +220,7 @@ function markUpdateLaneFromFiberToRoot( const offscreenInstance: OffscreenInstance | null = parent.stateNode; if ( offscreenInstance !== null && - !(offscreenInstance.visibility & OffscreenVisible) + !(offscreenInstance._visibility & OffscreenVisible) ) { isHidden = true; } diff --git a/packages/react-reconciler/src/ReactFiberOffscreenComponent.js b/packages/react-reconciler/src/ReactFiberOffscreenComponent.js index 1c45f6135d689..081f6a5a519ea 100644 --- a/packages/react-reconciler/src/ReactFiberOffscreenComponent.js +++ b/packages/react-reconciler/src/ReactFiberOffscreenComponent.js @@ -48,8 +48,8 @@ export const OffscreenVisible = /* */ 0b01; export const OffscreenPassiveEffectsConnected = /* */ 0b10; export type OffscreenInstance = { - visibility: OffscreenVisibility, - pendingMarkers: Set | null, - transitions: Set | null, - retryCache: WeakSet | Set | null, + _visibility: OffscreenVisibility, + _pendingMarkers: Set | null, + _transitions: Set | null, + _retryCache: WeakSet | Set | null, }; diff --git a/packages/react-reconciler/src/ReactFiberWorkLoop.new.js b/packages/react-reconciler/src/ReactFiberWorkLoop.new.js index 6f62aa4b1c0b1..b7169f5493c6b 100644 --- a/packages/react-reconciler/src/ReactFiberWorkLoop.new.js +++ b/packages/react-reconciler/src/ReactFiberWorkLoop.new.js @@ -3085,7 +3085,7 @@ export function resolveRetryWakeable(boundaryFiber: Fiber, wakeable: Wakeable) { break; case OffscreenComponent: { const instance: OffscreenInstance = boundaryFiber.stateNode; - retryCache = instance.retryCache; + retryCache = instance._retryCache; break; } default: diff --git a/packages/react-reconciler/src/ReactFiberWorkLoop.old.js b/packages/react-reconciler/src/ReactFiberWorkLoop.old.js index 414e2e3343ed0..3071dc5a16260 100644 --- a/packages/react-reconciler/src/ReactFiberWorkLoop.old.js +++ b/packages/react-reconciler/src/ReactFiberWorkLoop.old.js @@ -3085,7 +3085,7 @@ export function resolveRetryWakeable(boundaryFiber: Fiber, wakeable: Wakeable) { break; case OffscreenComponent: { const instance: OffscreenInstance = boundaryFiber.stateNode; - retryCache = instance.retryCache; + retryCache = instance._retryCache; break; } default: diff --git a/packages/react-reconciler/src/__tests__/ReactOffscreen-test.js b/packages/react-reconciler/src/__tests__/ReactOffscreen-test.js index 4e77f831f9289..30bbaea058995 100644 --- a/packages/react-reconciler/src/__tests__/ReactOffscreen-test.js +++ b/packages/react-reconciler/src/__tests__/ReactOffscreen-test.js @@ -8,6 +8,7 @@ let useState; let useLayoutEffect; let useEffect; let useMemo; +let useRef; let startTransition; describe('ReactOffscreen', () => { @@ -24,6 +25,7 @@ describe('ReactOffscreen', () => { useLayoutEffect = React.useLayoutEffect; useEffect = React.useEffect; useMemo = React.useMemo; + useRef = React.useRef; startTransition = React.startTransition; }); @@ -1259,4 +1261,126 @@ describe('ReactOffscreen', () => { , ); }); + + describe('manual interactivity', () => { + // @gate enableOffscreen + it('should attach ref only for mode null', async () => { + let offscreenRef; + + function App({mode}) { + offscreenRef = useRef(null); + return ( + { + offscreenRef.current = ref; + }}> +
+ + ); + } + + const root = ReactNoop.createRoot(); + + await act(async () => { + root.render(); + }); + + expect(offscreenRef.current).not.toBeNull(); + + await act(async () => { + root.render(); + }); + + expect(offscreenRef.current).toBeNull(); + + await act(async () => { + root.render(); + }); + + expect(offscreenRef.current).toBeNull(); + + await act(async () => { + root.render(); + }); + + expect(offscreenRef.current).not.toBeNull(); + }); + }); + + // @gate enableOffscreen + it('should detach ref if Offscreen is unmounted', async () => { + let offscreenRef; + + function App({showOffscreen}) { + offscreenRef = useRef(null); + return showOffscreen ? ( + { + offscreenRef.current = ref; + }}> +
+ + ) : null; + } + + const root = ReactNoop.createRoot(); + + await act(async () => { + root.render(); + }); + + expect(offscreenRef.current).not.toBeNull(); + + await act(async () => { + root.render(); + }); + + expect(offscreenRef.current).toBeNull(); + + await act(async () => { + root.render(); + }); + + expect(offscreenRef.current).not.toBeNull(); + }); + + // @gate enableOffscreen + it('should detach ref when parent Offscreen is hidden', async () => { + let offscreenRef; + + function App({mode}) { + offscreenRef = useRef(null); + return ( + + +
+ + + ); + } + + const root = ReactNoop.createRoot(); + + await act(async () => { + root.render(); + }); + + expect(offscreenRef.current).toBeNull(); + + await act(async () => { + root.render(); + }); + + expect(offscreenRef.current).not.toBeNull(); + + await act(async () => { + root.render(); + }); + + expect(offscreenRef.current).toBeNull(); + }); + + // TODO: When attach/detach methods are implemented. Add tests for nested Offscreen case. }); diff --git a/packages/shared/ReactTypes.js b/packages/shared/ReactTypes.js index 5ef83306706af..48b4ce11c1b6e 100644 --- a/packages/shared/ReactTypes.js +++ b/packages/shared/ReactTypes.js @@ -209,7 +209,8 @@ export type Thenable = export type OffscreenMode = | 'hidden' | 'unstable-defer-without-hiding' - | 'visible'; + | 'visible' + | 'manual'; export type StartTransitionOptions = { name?: string,