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,