From c1d414d75851aee7f25f69c1b6fda6a14198ba24 Mon Sep 17 00:00:00 2001 From: Samuel Susla Date: Fri, 23 Sep 2022 16:31:34 +0100 Subject: [PATCH] Add ref to Offscreen component (#25254) * Expose ref to Offscreen if mode is manual * Prepend private fields on OffscreenInstance with underscore * Schedule Ref effect unconditionally on Offscreen * Make sure Offscreen's ref is detached when unmounted * Make sure ref is mounted/unmounted in all scenarious * Nit: pendingProps -> memoizedProps Co-authored-by: Andrew Clark --- .../react-reconciler/src/ReactFiber.new.js | 16 +-- .../react-reconciler/src/ReactFiber.old.js | 16 +-- .../src/ReactFiberBeginWork.new.js | 6 +- .../src/ReactFiberBeginWork.old.js | 6 +- .../src/ReactFiberCommitWork.new.js | 77 +++++++---- .../src/ReactFiberCommitWork.old.js | 77 +++++++---- .../src/ReactFiberConcurrentUpdates.new.js | 2 +- .../src/ReactFiberConcurrentUpdates.old.js | 2 +- .../src/ReactFiberOffscreenComponent.js | 8 +- .../src/ReactFiberWorkLoop.new.js | 2 +- .../src/ReactFiberWorkLoop.old.js | 2 +- .../src/__tests__/ReactOffscreen-test.js | 124 ++++++++++++++++++ packages/shared/ReactTypes.js | 3 +- 13 files changed, 256 insertions(+), 85 deletions(-) diff --git a/packages/react-reconciler/src/ReactFiber.new.js b/packages/react-reconciler/src/ReactFiber.new.js index 16297ef3bea4b..3cff042b3fdd0 100644 --- a/packages/react-reconciler/src/ReactFiber.new.js +++ b/packages/react-reconciler/src/ReactFiber.new.js @@ -722,10 +722,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; @@ -743,10 +743,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 5599141eec8a0..f4717d6374922 100644 --- a/packages/react-reconciler/src/ReactFiber.old.js +++ b/packages/react-reconciler/src/ReactFiber.old.js @@ -722,10 +722,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; @@ -743,10 +743,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 c18bed4d808d3..92eab56c0d9f6 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 7b1f5b9c21b51..84cc43148c884 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 825652939f5e4..ee3ef79f7d315 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'; @@ -1141,6 +1142,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: { @@ -1314,7 +1323,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; @@ -2144,6 +2153,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 @@ -2250,9 +2260,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; } @@ -2623,6 +2633,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; @@ -2651,9 +2667,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) { @@ -2838,6 +2854,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 @@ -2985,6 +3004,8 @@ export function reappearLayoutEffects( includeWorkInProgressEffects, ); } + // TODO: Check flags & Ref + safelyAttachRef(finishedWork, finishedWork.return); break; } default: { @@ -3098,10 +3119,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); }); } @@ -3114,17 +3135,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); } }); } @@ -3139,8 +3160,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; } } } @@ -3320,7 +3341,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, @@ -3345,7 +3366,7 @@ function commitPassiveMountOnFiber( } } else { // Legacy Mode: Fire the effects even if the tree is hidden. - instance.visibility |= OffscreenPassiveEffectsConnected; + instance._visibility |= OffscreenPassiveEffectsConnected; recursivelyTraversePassiveMountEffects( finishedRoot, finishedWork, @@ -3356,7 +3377,7 @@ function commitPassiveMountOnFiber( } } else { // Tree is visible - if (instance.visibility & OffscreenPassiveEffectsConnected) { + if (instance._visibility & OffscreenPassiveEffectsConnected) { // The effects are currently connected. Update them. recursivelyTraversePassiveMountEffects( finishedRoot, @@ -3368,7 +3389,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; @@ -3500,7 +3521,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, @@ -3526,7 +3547,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, @@ -3544,7 +3565,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, @@ -3799,7 +3820,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 || @@ -3809,7 +3830,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); @@ -3873,8 +3894,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. @@ -4002,7 +4023,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 57233404d6ff9..ccfbf430eea16 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'; @@ -1141,6 +1142,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: { @@ -1314,7 +1323,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; @@ -2144,6 +2153,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 @@ -2250,9 +2260,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; } @@ -2623,6 +2633,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; @@ -2651,9 +2667,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) { @@ -2838,6 +2854,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 @@ -2985,6 +3004,8 @@ export function reappearLayoutEffects( includeWorkInProgressEffects, ); } + // TODO: Check flags & Ref + safelyAttachRef(finishedWork, finishedWork.return); break; } default: { @@ -3098,10 +3119,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); }); } @@ -3114,17 +3135,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); } }); } @@ -3139,8 +3160,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; } } } @@ -3320,7 +3341,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, @@ -3345,7 +3366,7 @@ function commitPassiveMountOnFiber( } } else { // Legacy Mode: Fire the effects even if the tree is hidden. - instance.visibility |= OffscreenPassiveEffectsConnected; + instance._visibility |= OffscreenPassiveEffectsConnected; recursivelyTraversePassiveMountEffects( finishedRoot, finishedWork, @@ -3356,7 +3377,7 @@ function commitPassiveMountOnFiber( } } else { // Tree is visible - if (instance.visibility & OffscreenPassiveEffectsConnected) { + if (instance._visibility & OffscreenPassiveEffectsConnected) { // The effects are currently connected. Update them. recursivelyTraversePassiveMountEffects( finishedRoot, @@ -3368,7 +3389,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; @@ -3500,7 +3521,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, @@ -3526,7 +3547,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, @@ -3544,7 +3565,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, @@ -3799,7 +3820,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 || @@ -3809,7 +3830,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); @@ -3873,8 +3894,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. @@ -4002,7 +4023,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 33e3bbe441abb..affc79306f77d 100644 --- a/packages/react-reconciler/src/ReactFiberConcurrentUpdates.new.js +++ b/packages/react-reconciler/src/ReactFiberConcurrentUpdates.new.js @@ -233,7 +233,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 0cd158010bbfb..2d40c3386a473 100644 --- a/packages/react-reconciler/src/ReactFiberConcurrentUpdates.old.js +++ b/packages/react-reconciler/src/ReactFiberConcurrentUpdates.old.js @@ -233,7 +233,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 6d244630867b2..95381d2558428 100644 --- a/packages/react-reconciler/src/ReactFiberWorkLoop.new.js +++ b/packages/react-reconciler/src/ReactFiberWorkLoop.new.js @@ -3150,7 +3150,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 36799208dc69b..ac07138054a0c 100644 --- a/packages/react-reconciler/src/ReactFiberWorkLoop.old.js +++ b/packages/react-reconciler/src/ReactFiberWorkLoop.old.js @@ -3150,7 +3150,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,