Skip to content

Commit bbe4813

Browse files
committed
Mount/unmount passive effects on hide/show
This changes the behavior of Offscreen so that passive effects are unmounted when the tree is hidden, and re-mounted when the tree is revealed again. This is already how layout effects worked. In the future we will likely add an option or heuristic to only unmount the effects of a hidden tree after a delay. That way if the tree quickly switches back to visible, we can skip toggling the effects entirely. This change does not apply to suspended trees, which happen to use the Offscreen fiber type as an implementation detail. Passive effects remain mounted while the tree is suspended, for the reason described above — it's likely that the suspended tree will resolve and switch back to visible within a short time span. At a high level, what this capability enables is a feature we refer to as "resuable state". The real value proposition here isn't so much the behavior of effects — it's that you can switch back to a previously rendered tree without losing the state of the UI.
1 parent 6a6dbe4 commit bbe4813

File tree

3 files changed

+320
-33
lines changed

3 files changed

+320
-33
lines changed

packages/react-reconciler/src/ReactFiberCommitWork.new.js

+101-16
Original file line numberDiff line numberDiff line change
@@ -3555,6 +3555,17 @@ export function commitPassiveUnmountEffects(finishedWork: Fiber): void {
35553555

35563556
function detachAlternateSiblings(parentFiber: Fiber) {
35573557
if (deletedTreeCleanUpLevel >= 1) {
3558+
// A fiber was deleted from this parent fiber, but it's still part of the
3559+
// previous (alternate) parent fiber's list of children. Because children
3560+
// are a linked list, an earlier sibling that's still alive will be
3561+
// connected to the deleted fiber via its `alternate`:
3562+
//
3563+
// live fiber --alternate--> previous live fiber --sibling--> deleted
3564+
// fiber
3565+
//
3566+
// We can't disconnect `alternate` on nodes that haven't been deleted yet,
3567+
// but we can disconnect the `sibling` and `child` pointers.
3568+
35583569
const previousFiber = parentFiber.alternate;
35593570
if (previousFiber !== null) {
35603571
let detachedChild = previousFiber.child;
@@ -3613,17 +3624,6 @@ function recursivelyTraversePassiveUnmountEffects(parentFiber: Fiber): void {
36133624
);
36143625
}
36153626
}
3616-
// A fiber was deleted from this parent fiber, but it's still part of
3617-
// the previous (alternate) parent fiber's list of children. Because
3618-
// children are a linked list, an earlier sibling that's still alive
3619-
// will be connected to the deleted fiber via its `alternate`:
3620-
//
3621-
// live fiber
3622-
// --alternate--> previous live fiber
3623-
// --sibling--> deleted fiber
3624-
//
3625-
// We can't disconnect `alternate` on nodes that haven't been deleted
3626-
// yet, but we can disconnect the `sibling` and `child` pointers.
36273627
detachAlternateSiblings(parentFiber);
36283628
}
36293629

@@ -3655,18 +3655,103 @@ function commitPassiveUnmountOnFiber(finishedWork: Fiber): void {
36553655
}
36563656
break;
36573657
}
3658-
// TODO: Disconnect passive effects when a tree is hidden, perhaps after
3659-
// a delay.
3660-
// case OffscreenComponent: {
3661-
// ...
3662-
// }
3658+
case OffscreenComponent: {
3659+
const instance: OffscreenInstance = finishedWork.stateNode;
3660+
const nextState: OffscreenState | null = finishedWork.memoizedState;
3661+
3662+
const isHidden = nextState !== null;
3663+
3664+
if (
3665+
isHidden &&
3666+
instance.visibility & OffscreenPassiveEffectsConnected &&
3667+
// For backwards compatibility, don't unmount when a tree suspends. In
3668+
// the future we may change this to unmount after a delay.
3669+
(finishedWork.return === null ||
3670+
finishedWork.return.tag !== SuspenseComponent)
3671+
) {
3672+
// The effects are currently connected. Disconnect them.
3673+
// TODO: Add option or heuristic to delay before disconnecting the
3674+
// effects. Then if the tree reappears before the delay has elapsed, we
3675+
// can skip toggling the effects entirely.
3676+
instance.visibility &= ~OffscreenPassiveEffectsConnected;
3677+
recursivelyTraverseDisconnectPassiveEffects(finishedWork);
3678+
} else {
3679+
recursivelyTraversePassiveUnmountEffects(finishedWork);
3680+
}
3681+
3682+
break;
3683+
}
36633684
default: {
36643685
recursivelyTraversePassiveUnmountEffects(finishedWork);
36653686
break;
36663687
}
36673688
}
36683689
}
36693690

3691+
function recursivelyTraverseDisconnectPassiveEffects(parentFiber: Fiber): void {
3692+
// Deletions effects can be scheduled on any fiber type. They need to happen
3693+
// before the children effects have fired.
3694+
const deletions = parentFiber.deletions;
3695+
3696+
if ((parentFiber.flags & ChildDeletion) !== NoFlags) {
3697+
if (deletions !== null) {
3698+
for (let i = 0; i < deletions.length; i++) {
3699+
const childToDelete = deletions[i];
3700+
// TODO: Convert this to use recursion
3701+
nextEffect = childToDelete;
3702+
commitPassiveUnmountEffectsInsideOfDeletedTree_begin(
3703+
childToDelete,
3704+
parentFiber,
3705+
);
3706+
}
3707+
}
3708+
detachAlternateSiblings(parentFiber);
3709+
}
3710+
3711+
const prevDebugFiber = getCurrentDebugFiberInDEV();
3712+
// TODO: Check PassiveStatic flag
3713+
let child = parentFiber.child;
3714+
while (child !== null) {
3715+
setCurrentDebugFiberInDEV(child);
3716+
disconnectPassiveEffect(child);
3717+
child = child.sibling;
3718+
}
3719+
setCurrentDebugFiberInDEV(prevDebugFiber);
3720+
}
3721+
3722+
function disconnectPassiveEffect(finishedWork: Fiber): void {
3723+
switch (finishedWork.tag) {
3724+
case FunctionComponent:
3725+
case ForwardRef:
3726+
case SimpleMemoComponent: {
3727+
// TODO: Check PassiveStatic flag
3728+
commitHookPassiveUnmountEffects(
3729+
finishedWork,
3730+
finishedWork.return,
3731+
HookPassive,
3732+
);
3733+
// When disconnecting passive effects, we fire the effects in the same
3734+
// order as during a deletiong: parent before child
3735+
recursivelyTraverseDisconnectPassiveEffects(finishedWork);
3736+
break;
3737+
}
3738+
case OffscreenComponent: {
3739+
const instance: OffscreenInstance = finishedWork.stateNode;
3740+
if (instance.visibility & OffscreenPassiveEffectsConnected) {
3741+
instance.visibility &= ~OffscreenPassiveEffectsConnected;
3742+
recursivelyTraverseDisconnectPassiveEffects(finishedWork);
3743+
} else {
3744+
// The effects are already disconnected.
3745+
}
3746+
break;
3747+
}
3748+
default: {
3749+
recursivelyTraverseDisconnectPassiveEffects(finishedWork);
3750+
break;
3751+
}
3752+
}
3753+
}
3754+
36703755
function commitPassiveUnmountEffectsInsideOfDeletedTree_begin(
36713756
deletedSubtreeRoot: Fiber,
36723757
nearestMountedAncestor: Fiber | null,

packages/react-reconciler/src/ReactFiberCommitWork.old.js

+101-16
Original file line numberDiff line numberDiff line change
@@ -3555,6 +3555,17 @@ export function commitPassiveUnmountEffects(finishedWork: Fiber): void {
35553555

35563556
function detachAlternateSiblings(parentFiber: Fiber) {
35573557
if (deletedTreeCleanUpLevel >= 1) {
3558+
// A fiber was deleted from this parent fiber, but it's still part of the
3559+
// previous (alternate) parent fiber's list of children. Because children
3560+
// are a linked list, an earlier sibling that's still alive will be
3561+
// connected to the deleted fiber via its `alternate`:
3562+
//
3563+
// live fiber --alternate--> previous live fiber --sibling--> deleted
3564+
// fiber
3565+
//
3566+
// We can't disconnect `alternate` on nodes that haven't been deleted yet,
3567+
// but we can disconnect the `sibling` and `child` pointers.
3568+
35583569
const previousFiber = parentFiber.alternate;
35593570
if (previousFiber !== null) {
35603571
let detachedChild = previousFiber.child;
@@ -3613,17 +3624,6 @@ function recursivelyTraversePassiveUnmountEffects(parentFiber: Fiber): void {
36133624
);
36143625
}
36153626
}
3616-
// A fiber was deleted from this parent fiber, but it's still part of
3617-
// the previous (alternate) parent fiber's list of children. Because
3618-
// children are a linked list, an earlier sibling that's still alive
3619-
// will be connected to the deleted fiber via its `alternate`:
3620-
//
3621-
// live fiber
3622-
// --alternate--> previous live fiber
3623-
// --sibling--> deleted fiber
3624-
//
3625-
// We can't disconnect `alternate` on nodes that haven't been deleted
3626-
// yet, but we can disconnect the `sibling` and `child` pointers.
36273627
detachAlternateSiblings(parentFiber);
36283628
}
36293629

@@ -3655,18 +3655,103 @@ function commitPassiveUnmountOnFiber(finishedWork: Fiber): void {
36553655
}
36563656
break;
36573657
}
3658-
// TODO: Disconnect passive effects when a tree is hidden, perhaps after
3659-
// a delay.
3660-
// case OffscreenComponent: {
3661-
// ...
3662-
// }
3658+
case OffscreenComponent: {
3659+
const instance: OffscreenInstance = finishedWork.stateNode;
3660+
const nextState: OffscreenState | null = finishedWork.memoizedState;
3661+
3662+
const isHidden = nextState !== null;
3663+
3664+
if (
3665+
isHidden &&
3666+
instance.visibility & OffscreenPassiveEffectsConnected &&
3667+
// For backwards compatibility, don't unmount when a tree suspends. In
3668+
// the future we may change this to unmount after a delay.
3669+
(finishedWork.return === null ||
3670+
finishedWork.return.tag !== SuspenseComponent)
3671+
) {
3672+
// The effects are currently connected. Disconnect them.
3673+
// TODO: Add option or heuristic to delay before disconnecting the
3674+
// effects. Then if the tree reappears before the delay has elapsed, we
3675+
// can skip toggling the effects entirely.
3676+
instance.visibility &= ~OffscreenPassiveEffectsConnected;
3677+
recursivelyTraverseDisconnectPassiveEffects(finishedWork);
3678+
} else {
3679+
recursivelyTraversePassiveUnmountEffects(finishedWork);
3680+
}
3681+
3682+
break;
3683+
}
36633684
default: {
36643685
recursivelyTraversePassiveUnmountEffects(finishedWork);
36653686
break;
36663687
}
36673688
}
36683689
}
36693690

3691+
function recursivelyTraverseDisconnectPassiveEffects(parentFiber: Fiber): void {
3692+
// Deletions effects can be scheduled on any fiber type. They need to happen
3693+
// before the children effects have fired.
3694+
const deletions = parentFiber.deletions;
3695+
3696+
if ((parentFiber.flags & ChildDeletion) !== NoFlags) {
3697+
if (deletions !== null) {
3698+
for (let i = 0; i < deletions.length; i++) {
3699+
const childToDelete = deletions[i];
3700+
// TODO: Convert this to use recursion
3701+
nextEffect = childToDelete;
3702+
commitPassiveUnmountEffectsInsideOfDeletedTree_begin(
3703+
childToDelete,
3704+
parentFiber,
3705+
);
3706+
}
3707+
}
3708+
detachAlternateSiblings(parentFiber);
3709+
}
3710+
3711+
const prevDebugFiber = getCurrentDebugFiberInDEV();
3712+
// TODO: Check PassiveStatic flag
3713+
let child = parentFiber.child;
3714+
while (child !== null) {
3715+
setCurrentDebugFiberInDEV(child);
3716+
disconnectPassiveEffect(child);
3717+
child = child.sibling;
3718+
}
3719+
setCurrentDebugFiberInDEV(prevDebugFiber);
3720+
}
3721+
3722+
function disconnectPassiveEffect(finishedWork: Fiber): void {
3723+
switch (finishedWork.tag) {
3724+
case FunctionComponent:
3725+
case ForwardRef:
3726+
case SimpleMemoComponent: {
3727+
// TODO: Check PassiveStatic flag
3728+
commitHookPassiveUnmountEffects(
3729+
finishedWork,
3730+
finishedWork.return,
3731+
HookPassive,
3732+
);
3733+
// When disconnecting passive effects, we fire the effects in the same
3734+
// order as during a deletiong: parent before child
3735+
recursivelyTraverseDisconnectPassiveEffects(finishedWork);
3736+
break;
3737+
}
3738+
case OffscreenComponent: {
3739+
const instance: OffscreenInstance = finishedWork.stateNode;
3740+
if (instance.visibility & OffscreenPassiveEffectsConnected) {
3741+
instance.visibility &= ~OffscreenPassiveEffectsConnected;
3742+
recursivelyTraverseDisconnectPassiveEffects(finishedWork);
3743+
} else {
3744+
// The effects are already disconnected.
3745+
}
3746+
break;
3747+
}
3748+
default: {
3749+
recursivelyTraverseDisconnectPassiveEffects(finishedWork);
3750+
break;
3751+
}
3752+
}
3753+
}
3754+
36703755
function commitPassiveUnmountEffectsInsideOfDeletedTree_begin(
36713756
deletedSubtreeRoot: Fiber,
36723757
nearestMountedAncestor: Fiber | null,

0 commit comments

Comments
 (0)