Skip to content

Commit

Permalink
Add ref to Offscreen component (#25254)
Browse files Browse the repository at this point in the history
* 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>
  • Loading branch information
sammy-SC and acdlite committed Sep 23, 2022
1 parent 135e33c commit c1d414d
Show file tree
Hide file tree
Showing 13 changed files with 256 additions and 85 deletions.
16 changes: 8 additions & 8 deletions packages/react-reconciler/src/ReactFiber.new.js
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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;
Expand Down
16 changes: 8 additions & 8 deletions packages/react-reconciler/src/ReactFiber.old.js
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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;
Expand Down
6 changes: 4 additions & 2 deletions packages/react-reconciler/src/ReactFiberBeginWork.new.js
Original file line number Diff line number Diff line change
Expand Up @@ -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')
Expand Down Expand Up @@ -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);
}
}

Expand Down
6 changes: 4 additions & 2 deletions packages/react-reconciler/src/ReactFiberBeginWork.old.js
Original file line number Diff line number Diff line change
Expand Up @@ -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')
Expand Down Expand Up @@ -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);
}
}

Expand Down
77 changes: 49 additions & 28 deletions packages/react-reconciler/src/ReactFiberCommitWork.new.js
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ import type {
OffscreenState,
OffscreenInstance,
OffscreenQueue,
OffscreenProps,
} from './ReactFiberOffscreenComponent';
import type {HookFlags} from './ReactHookEffectTags';
import type {Cache} from './ReactFiberCacheComponent.new';
Expand Down Expand Up @@ -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: {
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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;
}
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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) {
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -2985,6 +3004,8 @@ export function reappearLayoutEffects(
includeWorkInProgressEffects,
);
}
// TODO: Check flags & Ref
safelyAttachRef(finishedWork, finishedWork.return);
break;
}
default: {
Expand Down Expand Up @@ -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);
});
}

Expand All @@ -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);
}
});
}
Expand All @@ -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;
}
}
}
Expand Down Expand Up @@ -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,
Expand All @@ -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,
Expand All @@ -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,
Expand All @@ -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;
Expand Down Expand Up @@ -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,
Expand All @@ -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,
Expand All @@ -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,
Expand Down Expand Up @@ -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 ||
Expand All @@ -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);
Expand Down Expand Up @@ -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.
Expand Down Expand Up @@ -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',
Expand Down
Loading

0 comments on commit c1d414d

Please sign in to comment.