@samitouri / QOS-React-2 / commits / c1d414d758

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 <git@andrewclark.io>

Samuel Susla committed Sep 23, 2022 at 16:31 UTC c1d414d75851aee7f25f69c1b6fda6a14198ba24
13 files changed +256 -85
packages/react-reconciler/src/ReactFiber.new.js
+8 -8
@@ -722,10 +722,10 @@ export function createFiberFromOffscreen(
722 fiber.elementType = REACT_OFFSCREEN_TYPE;
723 fiber.lanes = lanes;
724 const primaryChildInstance: OffscreenInstance = {
725 - visibility: OffscreenVisible,
726 - pendingMarkers: null,
727 - retryCache: null,
728 - transitions: null,
725 + _visibility: OffscreenVisible,
726 + _pendingMarkers: null,
727 + _retryCache: null,
728 + _transitions: null,
729 };
730 fiber.stateNode = primaryChildInstance;
731 return fiber;
@@ -743,10 +743,10 @@ export function createFiberFromLegacyHidden(
743 // Adding a stateNode for legacy hidden because it's currently using
744 // the offscreen implementation, which depends on a state node
745 const instance: OffscreenInstance = {
746 - visibility: OffscreenVisible,
747 - pendingMarkers: null,
748 - transitions: null,
749 - retryCache: null,
746 + _visibility: OffscreenVisible,
747 + _pendingMarkers: null,
748 + _transitions: null,
749 + _retryCache: null,
750 };
751 fiber.stateNode = instance;
752 return fiber;
packages/react-reconciler/src/ReactFiber.old.js
+8 -8
@@ -722,10 +722,10 @@ export function createFiberFromOffscreen(
722 fiber.elementType = REACT_OFFSCREEN_TYPE;
723 fiber.lanes = lanes;
724 const primaryChildInstance: OffscreenInstance = {
725 - visibility: OffscreenVisible,
726 - pendingMarkers: null,
727 - retryCache: null,
728 - transitions: null,
725 + _visibility: OffscreenVisible,
726 + _pendingMarkers: null,
727 + _retryCache: null,
728 + _transitions: null,
729 };
730 fiber.stateNode = primaryChildInstance;
731 return fiber;
@@ -743,10 +743,10 @@ export function createFiberFromLegacyHidden(
743 // Adding a stateNode for legacy hidden because it's currently using
744 // the offscreen implementation, which depends on a state node
745 const instance: OffscreenInstance = {
746 - visibility: OffscreenVisible,
747 - pendingMarkers: null,
748 - transitions: null,
749 - retryCache: null,
746 + _visibility: OffscreenVisible,
747 + _pendingMarkers: null,
748 + _transitions: null,
749 + _retryCache: null,
750 };
751 fiber.stateNode = instance;
752 return fiber;
packages/react-reconciler/src/ReactFiberBeginWork.new.js
+4 -2
@@ -677,6 +677,8 @@ function updateOffscreenComponent(
677 const prevState: OffscreenState | null =
678 current !== null ? current.memoizedState : null;
679
680 + markRef(current, workInProgress);
681 +
682 if (
683 nextProps.mode === 'hidden' ||
684 (enableLegacyHidden && nextProps.mode === 'unstable-defer-without-hiding')
@@ -811,8 +813,8 @@ function updateOffscreenComponent(
813 // We have now gone from hidden to visible, so any transitions should
814 // be added to the stack to get added to any Offscreen/suspense children
815 const instance: OffscreenInstance | null = workInProgress.stateNode;
814 - if (instance !== null && instance.transitions != null) {
815 - transitions = Array.from(instance.transitions);
816 + if (instance !== null && instance._transitions != null) {
817 + transitions = Array.from(instance._transitions);
818 }
819 }
820
packages/react-reconciler/src/ReactFiberBeginWork.old.js
+4 -2
@@ -677,6 +677,8 @@ function updateOffscreenComponent(
677 const prevState: OffscreenState | null =
678 current !== null ? current.memoizedState : null;
679
680 + markRef(current, workInProgress);
681 +
682 if (
683 nextProps.mode === 'hidden' ||
684 (enableLegacyHidden && nextProps.mode === 'unstable-defer-without-hiding')
@@ -811,8 +813,8 @@ function updateOffscreenComponent(
813 // We have now gone from hidden to visible, so any transitions should
814 // be added to the stack to get added to any Offscreen/suspense children
815 const instance: OffscreenInstance | null = workInProgress.stateNode;
814 - if (instance !== null && instance.transitions != null) {
815 - transitions = Array.from(instance.transitions);
816 + if (instance !== null && instance._transitions != null) {
817 + transitions = Array.from(instance._transitions);
818 }
819 }
820
packages/react-reconciler/src/ReactFiberCommitWork.new.js
+49 -28
@@ -26,6 +26,7 @@ import type {
26 OffscreenState,
27 OffscreenInstance,
28 OffscreenQueue,
29 + OffscreenProps,
30 } from './ReactFiberOffscreenComponent';
31 import type {HookFlags} from './ReactHookEffectTags';
32 import type {Cache} from './ReactFiberCacheComponent.new';
@@ -1141,6 +1142,14 @@ function commitLayoutEffectOnFiber(
1142 committedLanes,
1143 );
1144 }
1145 + if (flags & Ref) {
1146 + const props: OffscreenProps = finishedWork.memoizedProps;
1147 + if (props.mode === 'manual') {
1148 + safelyAttachRef(finishedWork, finishedWork.return);
1149 + } else {
1150 + safelyDetachRef(finishedWork, finishedWork.return);
1151 + }
1152 + }
1153 break;
1154 }
1155 default: {
@@ -1314,7 +1323,7 @@ function commitTransitionProgress(offscreenFiber: Fiber) {
1323 const wasHidden = prevState !== null;
1324 const isHidden = nextState !== null;
1325
1317 - const pendingMarkers = offscreenInstance.pendingMarkers;
1326 + const pendingMarkers = offscreenInstance._pendingMarkers;
1327 // If there is a name on the suspense boundary, store that in
1328 // the pending boundaries.
1329 let name = null;
@@ -2144,6 +2153,7 @@ function commitDeletionEffectsOnFiber(
2153 return;
2154 }
2155 case OffscreenComponent: {
2156 + safelyDetachRef(deletedFiber, nearestMountedAncestor);
2157 if (deletedFiber.mode & ConcurrentMode) {
2158 // If this offscreen component is hidden, we already unmounted it. Before
2159 // deleting the children, track that it's already unmounted so that we
@@ -2250,9 +2260,9 @@ function getRetryCache(finishedWork) {
2260 }
2261 case OffscreenComponent: {
2262 const instance: OffscreenInstance = finishedWork.stateNode;
2253 - let retryCache = instance.retryCache;
2263 + let retryCache = instance._retryCache;
2264 if (retryCache === null) {
2255 - retryCache = instance.retryCache = new PossiblyWeakSet();
2265 + retryCache = instance._retryCache = new PossiblyWeakSet();
2266 }
2267 return retryCache;
2268 }
@@ -2623,6 +2633,12 @@ function commitMutationEffectsOnFiber(
2633 return;
2634 }
2635 case OffscreenComponent: {
2636 + if (flags & Ref) {
2637 + if (current !== null) {
2638 + safelyDetachRef(current, current.return);
2639 + }
2640 + }
2641 +
2642 const newState: OffscreenState | null = finishedWork.memoizedState;
2643 const isHidden = newState !== null;
2644 const wasHidden = current !== null && current.memoizedState !== null;
@@ -2651,9 +2667,9 @@ function commitMutationEffectsOnFiber(
2667 // Track the current state on the Offscreen instance so we can
2668 // read it during an event
2669 if (isHidden) {
2654 - offscreenInstance.visibility &= ~OffscreenVisible;
2670 + offscreenInstance._visibility &= ~OffscreenVisible;
2671 } else {
2656 - offscreenInstance.visibility |= OffscreenVisible;
2672 + offscreenInstance._visibility |= OffscreenVisible;
2673 }
2674
2675 if (isHidden) {
@@ -2838,6 +2854,9 @@ export function disappearLayoutEffects(finishedWork: Fiber) {
2854 break;
2855 }
2856 case OffscreenComponent: {
2857 + // TODO (Offscreen) Check: flags & RefStatic
2858 + safelyDetachRef(finishedWork, finishedWork.return);
2859 +
2860 const isHidden = finishedWork.memoizedState !== null;
2861 if (isHidden) {
2862 // Nested Offscreen tree is already hidden. Don't disappear
@@ -2985,6 +3004,8 @@ export function reappearLayoutEffects(
3004 includeWorkInProgressEffects,
3005 );
3006 }
3007 + // TODO: Check flags & Ref
3008 + safelyAttachRef(finishedWork, finishedWork.return);
3009 break;
3010 }
3011 default: {
@@ -3098,10 +3119,10 @@ function commitOffscreenPassiveMountEffects(
3119 // Add all the transitions saved in the update queue during
3120 // the render phase (ie the transitions associated with this boundary)
3121 // into the transitions set.
3101 - if (instance.transitions === null) {
3102 - instance.transitions = new Set();
3122 + if (instance._transitions === null) {
3123 + instance._transitions = new Set();
3124 }
3104 - instance.transitions.add(transition);
3125 + instance._transitions.add(transition);
3126 });
3127 }
3128
@@ -3114,17 +3135,17 @@ function commitOffscreenPassiveMountEffects(
3135 // caused them
3136 if (markerTransitions !== null) {
3137 markerTransitions.forEach(transition => {
3117 - if (instance.transitions === null) {
3118 - instance.transitions = new Set();
3119 - } else if (instance.transitions.has(transition)) {
3138 + if (instance._transitions === null) {
3139 + instance._transitions = new Set();
3140 + } else if (instance._transitions.has(transition)) {
3141 if (markerInstance.pendingBoundaries === null) {
3142 markerInstance.pendingBoundaries = new Map();
3143 }
3123 - if (instance.pendingMarkers === null) {
3124 - instance.pendingMarkers = new Set();
3144 + if (instance._pendingMarkers === null) {
3145 + instance._pendingMarkers = new Set();
3146 }
3147
3127 - instance.pendingMarkers.add(markerInstance);
3148 + instance._pendingMarkers.add(markerInstance);
3149 }
3150 });
3151 }
@@ -3139,8 +3160,8 @@ function commitOffscreenPassiveMountEffects(
3160
3161 // TODO: Refactor this into an if/else branch
3162 if (!isHidden) {
3142 - instance.transitions = null;
3143 - instance.pendingMarkers = null;
3163 + instance._transitions = null;
3164 + instance._pendingMarkers = null;
3165 }
3166 }
3167 }
@@ -3320,7 +3341,7 @@ function commitPassiveMountOnFiber(
3341 const isHidden = nextState !== null;
3342
3343 if (isHidden) {
3323 - if (instance.visibility & OffscreenPassiveEffectsConnected) {
3344 + if (instance._visibility & OffscreenPassiveEffectsConnected) {
3345 // The effects are currently connected. Update them.
3346 recursivelyTraversePassiveMountEffects(
3347 finishedRoot,
@@ -3345,7 +3366,7 @@ function commitPassiveMountOnFiber(
3366 }
3367 } else {
3368 // Legacy Mode: Fire the effects even if the tree is hidden.
3348 - instance.visibility |= OffscreenPassiveEffectsConnected;
3369 + instance._visibility |= OffscreenPassiveEffectsConnected;
3370 recursivelyTraversePassiveMountEffects(
3371 finishedRoot,
3372 finishedWork,
@@ -3356,7 +3377,7 @@ function commitPassiveMountOnFiber(
3377 }
3378 } else {
3379 // Tree is visible
3359 - if (instance.visibility & OffscreenPassiveEffectsConnected) {
3380 + if (instance._visibility & OffscreenPassiveEffectsConnected) {
3381 // The effects are currently connected. Update them.
3382 recursivelyTraversePassiveMountEffects(
3383 finishedRoot,
@@ -3368,7 +3389,7 @@ function commitPassiveMountOnFiber(
3389 // The effects are currently disconnected. Reconnect them, while also
3390 // firing effects inside newly mounted trees. This also applies to
3391 // the initial render.
3371 - instance.visibility |= OffscreenPassiveEffectsConnected;
3392 + instance._visibility |= OffscreenPassiveEffectsConnected;
3393
3394 const includeWorkInProgressEffects =
3395 (finishedWork.subtreeFlags & PassiveMask) !== NoFlags;
@@ -3500,7 +3521,7 @@ export function reconnectPassiveEffects(
3521 const isHidden = nextState !== null;
3522
3523 if (isHidden) {
3503 - if (instance.visibility & OffscreenPassiveEffectsConnected) {
3524 + if (instance._visibility & OffscreenPassiveEffectsConnected) {
3525 // The effects are currently connected. Update them.
3526 recursivelyTraverseReconnectPassiveEffects(
3527 finishedRoot,
@@ -3526,7 +3547,7 @@ export function reconnectPassiveEffects(
3547 }
3548 } else {
3549 // Legacy Mode: Fire the effects even if the tree is hidden.
3529 - instance.visibility |= OffscreenPassiveEffectsConnected;
3550 + instance._visibility |= OffscreenPassiveEffectsConnected;
3551 recursivelyTraverseReconnectPassiveEffects(
3552 finishedRoot,
3553 finishedWork,
@@ -3544,7 +3565,7 @@ export function reconnectPassiveEffects(
3565 // continue traversing the tree and firing all the effects.
3566 //
3567 // We do need to set the "connected" flag on the instance, though.
3547 - instance.visibility |= OffscreenPassiveEffectsConnected;
3568 + instance._visibility |= OffscreenPassiveEffectsConnected;
3569
3570 recursivelyTraverseReconnectPassiveEffects(
3571 finishedRoot,
@@ -3799,7 +3820,7 @@ function commitPassiveUnmountOnFiber(finishedWork: Fiber): void {
3820
3821 if (
3822 isHidden &&
3802 - instance.visibility & OffscreenPassiveEffectsConnected &&
3823 + instance._visibility & OffscreenPassiveEffectsConnected &&
3824 // For backwards compatibility, don't unmount when a tree suspends. In
3825 // the future we may change this to unmount after a delay.
3826 (finishedWork.return === null ||
@@ -3809,7 +3830,7 @@ function commitPassiveUnmountOnFiber(finishedWork: Fiber): void {
3830 // TODO: Add option or heuristic to delay before disconnecting the
3831 // effects. Then if the tree reappears before the delay has elapsed, we
3832 // can skip toggling the effects entirely.
3812 - instance.visibility &= ~OffscreenPassiveEffectsConnected;
3833 + instance._visibility &= ~OffscreenPassiveEffectsConnected;
3834 recursivelyTraverseDisconnectPassiveEffects(finishedWork);
3835 } else {
3836 recursivelyTraversePassiveUnmountEffects(finishedWork);
@@ -3873,8 +3894,8 @@ export function disconnectPassiveEffect(finishedWork: Fiber): void {
3894 }
3895 case OffscreenComponent: {
3896 const instance: OffscreenInstance = finishedWork.stateNode;
3876 - if (instance.visibility & OffscreenPassiveEffectsConnected) {
3877 - instance.visibility &= ~OffscreenPassiveEffectsConnected;
3897 + if (instance._visibility & OffscreenPassiveEffectsConnected) {
3898 + instance._visibility &= ~OffscreenPassiveEffectsConnected;
3899 recursivelyTraverseDisconnectPassiveEffects(finishedWork);
3900 } else {
3901 // The effects are already disconnected.
@@ -4002,7 +4023,7 @@ function commitPassiveUnmountInsideDeletedTreeOnFiber(
4023 // We need to mark this fiber's parents as deleted
4024 const offscreenFiber: Fiber = (current.child: any);
4025 const instance: OffscreenInstance = offscreenFiber.stateNode;
4005 - const transitions = instance.transitions;
4026 + const transitions = instance._transitions;
4027 if (transitions !== null) {
4028 const abortReason = {
4029 reason: 'suspense',
packages/react-reconciler/src/ReactFiberCommitWork.old.js
+49 -28
@@ -26,6 +26,7 @@ import type {
26 OffscreenState,
27 OffscreenInstance,
28 OffscreenQueue,
29 + OffscreenProps,
30 } from './ReactFiberOffscreenComponent';
31 import type {HookFlags} from './ReactHookEffectTags';
32 import type {Cache} from './ReactFiberCacheComponent.old';
@@ -1141,6 +1142,14 @@ function commitLayoutEffectOnFiber(
1142 committedLanes,
1143 );
1144 }
1145 + if (flags & Ref) {
1146 + const props: OffscreenProps = finishedWork.memoizedProps;
1147 + if (props.mode === 'manual') {
1148 + safelyAttachRef(finishedWork, finishedWork.return);
1149 + } else {
1150 + safelyDetachRef(finishedWork, finishedWork.return);
1151 + }
1152 + }
1153 break;
1154 }
1155 default: {
@@ -1314,7 +1323,7 @@ function commitTransitionProgress(offscreenFiber: Fiber) {
1323 const wasHidden = prevState !== null;
1324 const isHidden = nextState !== null;
1325
1317 - const pendingMarkers = offscreenInstance.pendingMarkers;
1326 + const pendingMarkers = offscreenInstance._pendingMarkers;
1327 // If there is a name on the suspense boundary, store that in
1328 // the pending boundaries.
1329 let name = null;
@@ -2144,6 +2153,7 @@ function commitDeletionEffectsOnFiber(
2153 return;
2154 }
2155 case OffscreenComponent: {
2156 + safelyDetachRef(deletedFiber, nearestMountedAncestor);
2157 if (deletedFiber.mode & ConcurrentMode) {
2158 // If this offscreen component is hidden, we already unmounted it. Before
2159 // deleting the children, track that it's already unmounted so that we
@@ -2250,9 +2260,9 @@ function getRetryCache(finishedWork) {
2260 }
2261 case OffscreenComponent: {
2262 const instance: OffscreenInstance = finishedWork.stateNode;
2253 - let retryCache = instance.retryCache;
2263 + let retryCache = instance._retryCache;
2264 if (retryCache === null) {
2255 - retryCache = instance.retryCache = new PossiblyWeakSet();
2265 + retryCache = instance._retryCache = new PossiblyWeakSet();
2266 }
2267 return retryCache;
2268 }
@@ -2623,6 +2633,12 @@ function commitMutationEffectsOnFiber(
2633 return;
2634 }
2635 case OffscreenComponent: {
2636 + if (flags & Ref) {
2637 + if (current !== null) {
2638 + safelyDetachRef(current, current.return);
2639 + }
2640 + }
2641 +
2642 const newState: OffscreenState | null = finishedWork.memoizedState;
2643 const isHidden = newState !== null;
2644 const wasHidden = current !== null && current.memoizedState !== null;
@@ -2651,9 +2667,9 @@ function commitMutationEffectsOnFiber(
2667 // Track the current state on the Offscreen instance so we can
2668 // read it during an event
2669 if (isHidden) {
2654 - offscreenInstance.visibility &= ~OffscreenVisible;
2670 + offscreenInstance._visibility &= ~OffscreenVisible;
2671 } else {
2656 - offscreenInstance.visibility |= OffscreenVisible;
2672 + offscreenInstance._visibility |= OffscreenVisible;
2673 }
2674
2675 if (isHidden) {
@@ -2838,6 +2854,9 @@ export function disappearLayoutEffects(finishedWork: Fiber) {
2854 break;
2855 }
2856 case OffscreenComponent: {
2857 + // TODO (Offscreen) Check: flags & RefStatic
2858 + safelyDetachRef(finishedWork, finishedWork.return);
2859 +
2860 const isHidden = finishedWork.memoizedState !== null;
2861 if (isHidden) {
2862 // Nested Offscreen tree is already hidden. Don't disappear
@@ -2985,6 +3004,8 @@ export function reappearLayoutEffects(
3004 includeWorkInProgressEffects,
3005 );
3006 }
3007 + // TODO: Check flags & Ref
3008 + safelyAttachRef(finishedWork, finishedWork.return);
3009 break;
3010 }
3011 default: {
@@ -3098,10 +3119,10 @@ function commitOffscreenPassiveMountEffects(
3119 // Add all the transitions saved in the update queue during
3120 // the render phase (ie the transitions associated with this boundary)
3121 // into the transitions set.
3101 - if (instance.transitions === null) {
3102 - instance.transitions = new Set();
3122 + if (instance._transitions === null) {
3123 + instance._transitions = new Set();
3124 }
3104 - instance.transitions.add(transition);
3125 + instance._transitions.add(transition);
3126 });
3127 }
3128
@@ -3114,17 +3135,17 @@ function commitOffscreenPassiveMountEffects(
3135 // caused them
3136 if (markerTransitions !== null) {
3137 markerTransitions.forEach(transition => {
3117 - if (instance.transitions === null) {
3118 - instance.transitions = new Set();
3119 - } else if (instance.transitions.has(transition)) {
3138 + if (instance._transitions === null) {
3139 + instance._transitions = new Set();
3140 + } else if (instance._transitions.has(transition)) {
3141 if (markerInstance.pendingBoundaries === null) {
3142 markerInstance.pendingBoundaries = new Map();
3143 }
3123 - if (instance.pendingMarkers === null) {
3124 - instance.pendingMarkers = new Set();
3144 + if (instance._pendingMarkers === null) {
3145 + instance._pendingMarkers = new Set();
3146 }
3147
3127 - instance.pendingMarkers.add(markerInstance);
3148 + instance._pendingMarkers.add(markerInstance);
3149 }
3150 });
3151 }
@@ -3139,8 +3160,8 @@ function commitOffscreenPassiveMountEffects(
3160
3161 // TODO: Refactor this into an if/else branch
3162 if (!isHidden) {
3142 - instance.transitions = null;
3143 - instance.pendingMarkers = null;
3163 + instance._transitions = null;
3164 + instance._pendingMarkers = null;
3165 }
3166 }
3167 }
@@ -3320,7 +3341,7 @@ function commitPassiveMountOnFiber(
3341 const isHidden = nextState !== null;
3342
3343 if (isHidden) {
3323 - if (instance.visibility & OffscreenPassiveEffectsConnected) {
3344 + if (instance._visibility & OffscreenPassiveEffectsConnected) {
3345 // The effects are currently connected. Update them.
3346 recursivelyTraversePassiveMountEffects(
3347 finishedRoot,
@@ -3345,7 +3366,7 @@ function commitPassiveMountOnFiber(
3366 }
3367 } else {
3368 // Legacy Mode: Fire the effects even if the tree is hidden.
3348 - instance.visibility |= OffscreenPassiveEffectsConnected;
3369 + instance._visibility |= OffscreenPassiveEffectsConnected;
3370 recursivelyTraversePassiveMountEffects(
3371 finishedRoot,
3372 finishedWork,
@@ -3356,7 +3377,7 @@ function commitPassiveMountOnFiber(
3377 }
3378 } else {
3379 // Tree is visible
3359 - if (instance.visibility & OffscreenPassiveEffectsConnected) {
3380 + if (instance._visibility & OffscreenPassiveEffectsConnected) {
3381 // The effects are currently connected. Update them.
3382 recursivelyTraversePassiveMountEffects(
3383 finishedRoot,
@@ -3368,7 +3389,7 @@ function commitPassiveMountOnFiber(
3389 // The effects are currently disconnected. Reconnect them, while also
3390 // firing effects inside newly mounted trees. This also applies to
3391 // the initial render.
3371 - instance.visibility |= OffscreenPassiveEffectsConnected;
3392 + instance._visibility |= OffscreenPassiveEffectsConnected;
3393
3394 const includeWorkInProgressEffects =
3395 (finishedWork.subtreeFlags & PassiveMask) !== NoFlags;
@@ -3500,7 +3521,7 @@ export function reconnectPassiveEffects(
3521 const isHidden = nextState !== null;
3522
3523 if (isHidden) {
3503 - if (instance.visibility & OffscreenPassiveEffectsConnected) {
3524 + if (instance._visibility & OffscreenPassiveEffectsConnected) {
3525 // The effects are currently connected. Update them.
3526 recursivelyTraverseReconnectPassiveEffects(
3527 finishedRoot,
@@ -3526,7 +3547,7 @@ export function reconnectPassiveEffects(
3547 }
3548 } else {
3549 // Legacy Mode: Fire the effects even if the tree is hidden.
3529 - instance.visibility |= OffscreenPassiveEffectsConnected;
3550 + instance._visibility |= OffscreenPassiveEffectsConnected;
3551 recursivelyTraverseReconnectPassiveEffects(
3552 finishedRoot,
3553 finishedWork,
@@ -3544,7 +3565,7 @@ export function reconnectPassiveEffects(
3565 // continue traversing the tree and firing all the effects.
3566 //
3567 // We do need to set the "connected" flag on the instance, though.
3547 - instance.visibility |= OffscreenPassiveEffectsConnected;
3568 + instance._visibility |= OffscreenPassiveEffectsConnected;
3569
3570 recursivelyTraverseReconnectPassiveEffects(
3571 finishedRoot,
@@ -3799,7 +3820,7 @@ function commitPassiveUnmountOnFiber(finishedWork: Fiber): void {
3820
3821 if (
3822 isHidden &&
3802 - instance.visibility & OffscreenPassiveEffectsConnected &&
3823 + instance._visibility & OffscreenPassiveEffectsConnected &&
3824 // For backwards compatibility, don't unmount when a tree suspends. In
3825 // the future we may change this to unmount after a delay.
3826 (finishedWork.return === null ||
@@ -3809,7 +3830,7 @@ function commitPassiveUnmountOnFiber(finishedWork: Fiber): void {
3830 // TODO: Add option or heuristic to delay before disconnecting the
3831 // effects. Then if the tree reappears before the delay has elapsed, we
3832 // can skip toggling the effects entirely.
3812 - instance.visibility &= ~OffscreenPassiveEffectsConnected;
3833 + instance._visibility &= ~OffscreenPassiveEffectsConnected;
3834 recursivelyTraverseDisconnectPassiveEffects(finishedWork);
3835 } else {
3836 recursivelyTraversePassiveUnmountEffects(finishedWork);
@@ -3873,8 +3894,8 @@ export function disconnectPassiveEffect(finishedWork: Fiber): void {
3894 }
3895 case OffscreenComponent: {
3896 const instance: OffscreenInstance = finishedWork.stateNode;
3876 - if (instance.visibility & OffscreenPassiveEffectsConnected) {
3877 - instance.visibility &= ~OffscreenPassiveEffectsConnected;
3897 + if (instance._visibility & OffscreenPassiveEffectsConnected) {
3898 + instance._visibility &= ~OffscreenPassiveEffectsConnected;
3899 recursivelyTraverseDisconnectPassiveEffects(finishedWork);
3900 } else {
3901 // The effects are already disconnected.
@@ -4002,7 +4023,7 @@ function commitPassiveUnmountInsideDeletedTreeOnFiber(
4023 // We need to mark this fiber's parents as deleted
4024 const offscreenFiber: Fiber = (current.child: any);
4025 const instance: OffscreenInstance = offscreenFiber.stateNode;
4005 - const transitions = instance.transitions;
4026 + const transitions = instance._transitions;
4027 if (transitions !== null) {
4028 const abortReason = {
4029 reason: 'suspense',
packages/react-reconciler/src/ReactFiberConcurrentUpdates.new.js
+1 -1
@@ -233,7 +233,7 @@ function markUpdateLaneFromFiberToRoot(
233 const offscreenInstance: OffscreenInstance | null = parent.stateNode;
234 if (
235 offscreenInstance !== null &&
236 - !(offscreenInstance.visibility & OffscreenVisible)
236 + !(offscreenInstance._visibility & OffscreenVisible)
237 ) {
238 isHidden = true;
239 }
packages/react-reconciler/src/ReactFiberConcurrentUpdates.old.js
+1 -1
@@ -233,7 +233,7 @@ function markUpdateLaneFromFiberToRoot(
233 const offscreenInstance: OffscreenInstance | null = parent.stateNode;
234 if (
235 offscreenInstance !== null &&
236 - !(offscreenInstance.visibility & OffscreenVisible)
236 + !(offscreenInstance._visibility & OffscreenVisible)
237 ) {
238 isHidden = true;
239 }
packages/react-reconciler/src/ReactFiberOffscreenComponent.js
+4 -4
@@ -48,8 +48,8 @@ export const OffscreenVisible = /* */ 0b01;
48 export const OffscreenPassiveEffectsConnected = /* */ 0b10;
49
50 export type OffscreenInstance = {
51 - visibility: OffscreenVisibility,
52 - pendingMarkers: Set<TracingMarkerInstance> | null,
53 - transitions: Set<Transition> | null,
54 - retryCache: WeakSet<Wakeable> | Set<Wakeable> | null,
51 + _visibility: OffscreenVisibility,
52 + _pendingMarkers: Set<TracingMarkerInstance> | null,
53 + _transitions: Set<Transition> | null,
54 + _retryCache: WeakSet<Wakeable> | Set<Wakeable> | null,
55 };
packages/react-reconciler/src/ReactFiberWorkLoop.new.js
+1 -1
@@ -3150,7 +3150,7 @@ export function resolveRetryWakeable(boundaryFiber: Fiber, wakeable: Wakeable) {
3150 break;
3151 case OffscreenComponent: {
3152 const instance: OffscreenInstance = boundaryFiber.stateNode;
3153 - retryCache = instance.retryCache;
3153 + retryCache = instance._retryCache;
3154 break;
3155 }
3156 default:
packages/react-reconciler/src/ReactFiberWorkLoop.old.js
+1 -1
@@ -3150,7 +3150,7 @@ export function resolveRetryWakeable(boundaryFiber: Fiber, wakeable: Wakeable) {
3150 break;
3151 case OffscreenComponent: {
3152 const instance: OffscreenInstance = boundaryFiber.stateNode;
3153 - retryCache = instance.retryCache;
3153 + retryCache = instance._retryCache;
3154 break;
3155 }
3156 default:
packages/react-reconciler/src/__tests__/ReactOffscreen-test.js
+124
@@ -8,6 +8,7 @@ let useState;
8 let useLayoutEffect;
9 let useEffect;
10 let useMemo;
11 +let useRef;
12 let startTransition;
13
14 describe('ReactOffscreen', () => {
@@ -24,6 +25,7 @@ describe('ReactOffscreen', () => {
25 useLayoutEffect = React.useLayoutEffect;
26 useEffect = React.useEffect;
27 useMemo = React.useMemo;
28 + useRef = React.useRef;
29 startTransition = React.startTransition;
30 });
31
@@ -1259,4 +1261,126 @@ describe('ReactOffscreen', () => {
1261 </div>,
1262 );
1263 });
1264 +
1265 + describe('manual interactivity', () => {
1266 + // @gate enableOffscreen
1267 + it('should attach ref only for mode null', async () => {
1268 + let offscreenRef;
1269 +
1270 + function App({mode}) {
1271 + offscreenRef = useRef(null);
1272 + return (
1273 + <Offscreen
1274 + mode={mode}
1275 + ref={ref => {
1276 + offscreenRef.current = ref;
1277 + }}>
1278 + <div />
1279 + </Offscreen>
1280 + );
1281 + }
1282 +
1283 + const root = ReactNoop.createRoot();
1284 +
1285 + await act(async () => {
1286 + root.render(<App mode={'manual'} />);
1287 + });
1288 +
1289 + expect(offscreenRef.current).not.toBeNull();
1290 +
1291 + await act(async () => {
1292 + root.render(<App mode={'visible'} />);
1293 + });
1294 +
1295 + expect(offscreenRef.current).toBeNull();
1296 +
1297 + await act(async () => {
1298 + root.render(<App mode={'hidden'} />);
1299 + });
1300 +
1301 + expect(offscreenRef.current).toBeNull();
1302 +
1303 + await act(async () => {
1304 + root.render(<App mode={'manual'} />);
1305 + });
1306 +
1307 + expect(offscreenRef.current).not.toBeNull();
1308 + });
1309 + });
1310 +
1311 + // @gate enableOffscreen
1312 + it('should detach ref if Offscreen is unmounted', async () => {
1313 + let offscreenRef;
1314 +
1315 + function App({showOffscreen}) {
1316 + offscreenRef = useRef(null);
1317 + return showOffscreen ? (
1318 + <Offscreen
1319 + mode={'manual'}
1320 + ref={ref => {
1321 + offscreenRef.current = ref;
1322 + }}>
1323 + <div />
1324 + </Offscreen>
1325 + ) : null;
1326 + }
1327 +
1328 + const root = ReactNoop.createRoot();
1329 +
1330 + await act(async () => {
1331 + root.render(<App showOffscreen={true} />);
1332 + });
1333 +
1334 + expect(offscreenRef.current).not.toBeNull();
1335 +
1336 + await act(async () => {
1337 + root.render(<App showOffscreen={false} />);
1338 + });
1339 +
1340 + expect(offscreenRef.current).toBeNull();
1341 +
1342 + await act(async () => {
1343 + root.render(<App showOffscreen={true} />);
1344 + });
1345 +
1346 + expect(offscreenRef.current).not.toBeNull();
1347 + });
1348 +
1349 + // @gate enableOffscreen
1350 + it('should detach ref when parent Offscreen is hidden', async () => {
1351 + let offscreenRef;
1352 +
1353 + function App({mode}) {
1354 + offscreenRef = useRef(null);
1355 + return (
1356 + <Offscreen mode={mode}>
1357 + <Offscreen mode={'manual'} ref={offscreenRef}>
1358 + <div />
1359 + </Offscreen>
1360 + </Offscreen>
1361 + );
1362 + }
1363 +
1364 + const root = ReactNoop.createRoot();
1365 +
1366 + await act(async () => {
1367 + root.render(<App mode={'hidden'} />);
1368 + });
1369 +
1370 + expect(offscreenRef.current).toBeNull();
1371 +
1372 + await act(async () => {
1373 + root.render(<App mode={'visible'} />);
1374 + });
1375 +
1376 + expect(offscreenRef.current).not.toBeNull();
1377 +
1378 + await act(async () => {
1379 + root.render(<App mode={'hidden'} />);
1380 + });
1381 +
1382 + expect(offscreenRef.current).toBeNull();
1383 + });
1384 +
1385 + // TODO: When attach/detach methods are implemented. Add tests for nested Offscreen case.
1386 });
packages/shared/ReactTypes.js
+2 -1
@@ -209,7 +209,8 @@ export type Thenable<T> =
209 export type OffscreenMode =
210 | 'hidden'
211 | 'unstable-defer-without-hiding'
212 - | 'visible';
212 + | 'visible'
213 + | 'manual';
214
215 export type StartTransitionOptions = {
216 name?: string,