Skip to content

Commit 5f0dc50

Browse files
committed
Refactor "disappear" logic into its own traversal
Similar to #21898, but for "disappear" logic. Previously this lived inside `hideOrUnhideAllChildren`, the function that mutates the nearest DOM nodes to override their `display` style. This makes the feature work in persistent mode (Fabric); it didn't before because `hideOrUnhideAllChildren` only runs in mutation mode.
1 parent 34600f4 commit 5f0dc50

File tree

4 files changed

+310
-172
lines changed

4 files changed

+310
-172
lines changed

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

Lines changed: 155 additions & 75 deletions
Original file line numberDiff line numberDiff line change
@@ -980,12 +980,6 @@ function reappearLayoutEffectsOnFiber(node: Fiber) {
980980
}
981981

982982
function hideOrUnhideAllChildren(finishedWork, isHidden) {
983-
// Suspense layout effects semantics don't change for legacy roots.
984-
const isModernRoot = (finishedWork.mode & ConcurrentMode) !== NoMode;
985-
986-
const current = finishedWork.alternate;
987-
const wasHidden = current !== null && current.memoizedState !== null;
988-
989983
// Only hide or unhide the top-most host nodes.
990984
let hostSubtreeRoot = null;
991985

@@ -1005,22 +999,6 @@ function hideOrUnhideAllChildren(finishedWork, isHidden) {
1005999
unhideInstance(node.stateNode, node.memoizedProps);
10061000
}
10071001
}
1008-
1009-
if (enableSuspenseLayoutEffectSemantics && isModernRoot) {
1010-
// This method is called during mutation; it should detach refs within a hidden subtree.
1011-
// Attaching refs should be done elsewhere though (during layout).
1012-
// TODO (Offscreen) Also check: flags & RefStatic
1013-
if (isHidden) {
1014-
safelyDetachRef(node, finishedWork);
1015-
}
1016-
1017-
// TODO (Offscreen) Also check: subtreeFlags & (RefStatic | LayoutStatic)
1018-
if (node.child !== null) {
1019-
node.child.return = node;
1020-
node = node.child;
1021-
continue;
1022-
}
1023-
}
10241002
} else if (node.tag === HostText) {
10251003
if (hostSubtreeRoot === null) {
10261004
const instance = node.stateNode;
@@ -1038,52 +1016,6 @@ function hideOrUnhideAllChildren(finishedWork, isHidden) {
10381016
) {
10391017
// Found a nested Offscreen component that is hidden.
10401018
// Don't search any deeper. This tree should remain hidden.
1041-
} else if (enableSuspenseLayoutEffectSemantics && isModernRoot) {
1042-
// When a mounted Suspense subtree gets hidden again, destroy any nested layout effects.
1043-
// TODO (Offscreen) Check: flags & (RefStatic | LayoutStatic)
1044-
switch (node.tag) {
1045-
case FunctionComponent:
1046-
case ForwardRef:
1047-
case MemoComponent:
1048-
case SimpleMemoComponent: {
1049-
// Note that refs are attached by the useImperativeHandle() hook, not by commitAttachRef()
1050-
if (isHidden && !wasHidden) {
1051-
if (
1052-
enableProfilerTimer &&
1053-
enableProfilerCommitHooks &&
1054-
node.mode & ProfileMode
1055-
) {
1056-
try {
1057-
startLayoutEffectTimer();
1058-
commitHookEffectListUnmount(HookLayout, node, finishedWork);
1059-
} finally {
1060-
recordLayoutEffectDuration(node);
1061-
}
1062-
} else {
1063-
commitHookEffectListUnmount(HookLayout, node, finishedWork);
1064-
}
1065-
}
1066-
break;
1067-
}
1068-
case ClassComponent: {
1069-
if (isHidden && !wasHidden) {
1070-
// TODO (Offscreen) Check: flags & RefStatic
1071-
safelyDetachRef(node, finishedWork);
1072-
1073-
const instance = node.stateNode;
1074-
if (typeof instance.componentWillUnmount === 'function') {
1075-
safelyCallComponentWillUnmount(node, finishedWork, instance);
1076-
}
1077-
}
1078-
break;
1079-
}
1080-
}
1081-
1082-
if (node.child !== null) {
1083-
node.child.return = node;
1084-
node = node.child;
1085-
continue;
1086-
}
10871019
} else if (node.child !== null) {
10881020
node.child.return = node;
10891021
node = node.child;
@@ -1801,6 +1733,11 @@ function commitWork(current: Fiber | null, finishedWork: Fiber): void {
18011733
// This prevents sibling component effects from interfering with each other,
18021734
// e.g. a destroy function in one component should never override a ref set
18031735
// by a create function in another component during the same commit.
1736+
// TODO: Check if we're inside an Offscreen subtree that disappeared
1737+
// during this commit. If so, we would have already unmounted its
1738+
// layout hooks. (However, since we null out the `destroy` function
1739+
// right before calling it, the behavior is already correct, so this
1740+
// would mostly be for modeling purposes.)
18041741
if (
18051742
enableProfilerTimer &&
18061743
enableProfilerCommitHooks &&
@@ -2183,8 +2120,12 @@ function commitMutationEffectsOnFiber(finishedWork: Fiber, root: FiberRoot) {
21832120
switch (finishedWork.tag) {
21842121
case SuspenseComponent: {
21852122
const newState: OffscreenState | null = finishedWork.memoizedState;
2186-
if (newState !== null) {
2187-
markCommitTimeOfFallback();
2123+
const isHidden = newState !== null;
2124+
const current = finishedWork.alternate;
2125+
const wasHidden = current !== null && current.memoizedState !== null;
2126+
const offscreenBoundary: Fiber = (finishedWork.child: any);
2127+
2128+
if (isHidden) {
21882129
// Hide the Offscreen component that contains the primary children.
21892130
// TODO: Ideally, this effect would have been scheduled on the
21902131
// Offscreen fiber itself. That's how unhiding works: the Offscreen
@@ -2195,16 +2136,67 @@ function commitMutationEffectsOnFiber(finishedWork: Fiber, root: FiberRoot) {
21952136
// this way is less complicated. This would be simpler if we got rid
21962137
// of the effect list and traversed the tree, like we're planning to
21972138
// do.
2198-
const primaryChildParent: Fiber = (finishedWork.child: any);
2199-
hideOrUnhideAllChildren(primaryChildParent, true);
2139+
if (!wasHidden) {
2140+
markCommitTimeOfFallback();
2141+
if (supportsMutation) {
2142+
hideOrUnhideAllChildren(offscreenBoundary, true);
2143+
}
2144+
if (
2145+
enableSuspenseLayoutEffectSemantics &&
2146+
(offscreenBoundary.mode & ConcurrentMode) !== NoMode
2147+
) {
2148+
let offscreenChild = offscreenBoundary.child;
2149+
while (offscreenChild !== null) {
2150+
nextEffect = offscreenChild;
2151+
disappearLayoutEffects_begin(offscreenChild);
2152+
offscreenChild = offscreenChild.sibling;
2153+
}
2154+
}
2155+
}
2156+
} else {
2157+
if (wasHidden) {
2158+
if (supportsMutation) {
2159+
hideOrUnhideAllChildren(offscreenBoundary, false);
2160+
}
2161+
// TODO: Move re-appear call here for symmetry?
2162+
}
22002163
}
22012164
break;
22022165
}
22032166
case OffscreenComponent:
22042167
case LegacyHiddenComponent: {
22052168
const newState: OffscreenState | null = finishedWork.memoizedState;
22062169
const isHidden = newState !== null;
2207-
hideOrUnhideAllChildren(finishedWork, isHidden);
2170+
const current = finishedWork.alternate;
2171+
const wasHidden = current !== null && current.memoizedState !== null;
2172+
const offscreenBoundary: Fiber = finishedWork;
2173+
2174+
if (supportsMutation) {
2175+
// TODO: This needs to run whenever there's an insertion or update
2176+
// inside a hidden Offscreen tree.
2177+
hideOrUnhideAllChildren(offscreenBoundary, isHidden);
2178+
}
2179+
2180+
if (isHidden) {
2181+
if (!wasHidden) {
2182+
if (
2183+
enableSuspenseLayoutEffectSemantics &&
2184+
(offscreenBoundary.mode & ConcurrentMode) !== NoMode
2185+
) {
2186+
nextEffect = offscreenBoundary;
2187+
let offscreenChild = offscreenBoundary.child;
2188+
while (offscreenChild !== null) {
2189+
nextEffect = offscreenChild;
2190+
disappearLayoutEffects_begin(offscreenChild);
2191+
offscreenChild = offscreenChild.sibling;
2192+
}
2193+
}
2194+
}
2195+
} else {
2196+
if (wasHidden) {
2197+
// TODO: Move re-appear call here for symmetry?
2198+
}
2199+
}
22082200
break;
22092201
}
22102202
}
@@ -2381,6 +2373,90 @@ function commitLayoutMountEffects_complete(
23812373
}
23822374
}
23832375

2376+
function disappearLayoutEffects_begin(subtreeRoot: Fiber) {
2377+
while (nextEffect !== null) {
2378+
const fiber = nextEffect;
2379+
const firstChild = fiber.child;
2380+
2381+
// TODO (Offscreen) Check: flags & (RefStatic | LayoutStatic)
2382+
switch (fiber.tag) {
2383+
case FunctionComponent:
2384+
case ForwardRef:
2385+
case MemoComponent:
2386+
case SimpleMemoComponent: {
2387+
if (
2388+
enableProfilerTimer &&
2389+
enableProfilerCommitHooks &&
2390+
fiber.mode & ProfileMode
2391+
) {
2392+
try {
2393+
startLayoutEffectTimer();
2394+
commitHookEffectListUnmount(HookLayout, fiber, fiber.return);
2395+
} finally {
2396+
recordLayoutEffectDuration(fiber);
2397+
}
2398+
} else {
2399+
commitHookEffectListUnmount(HookLayout, fiber, fiber.return);
2400+
}
2401+
break;
2402+
}
2403+
case ClassComponent: {
2404+
// TODO (Offscreen) Check: flags & RefStatic
2405+
safelyDetachRef(fiber, fiber.return);
2406+
2407+
const instance = fiber.stateNode;
2408+
if (typeof instance.componentWillUnmount === 'function') {
2409+
safelyCallComponentWillUnmount(fiber, fiber.return, instance);
2410+
}
2411+
break;
2412+
}
2413+
case HostComponent: {
2414+
safelyDetachRef(fiber, fiber.return);
2415+
break;
2416+
}
2417+
case OffscreenComponent: {
2418+
// Check if this is a
2419+
const isHidden = fiber.memoizedState !== null;
2420+
if (isHidden) {
2421+
// Nested Offscreen tree is already hidden. Don't disappear
2422+
// its effects.
2423+
disappearLayoutEffects_complete(subtreeRoot);
2424+
continue;
2425+
}
2426+
break;
2427+
}
2428+
}
2429+
2430+
// TODO (Offscreen) Check: subtreeFlags & LayoutStatic
2431+
if (firstChild !== null) {
2432+
firstChild.return = fiber;
2433+
nextEffect = firstChild;
2434+
} else {
2435+
disappearLayoutEffects_complete(subtreeRoot);
2436+
}
2437+
}
2438+
}
2439+
2440+
function disappearLayoutEffects_complete(subtreeRoot: Fiber) {
2441+
while (nextEffect !== null) {
2442+
const fiber = nextEffect;
2443+
2444+
if (fiber === subtreeRoot) {
2445+
nextEffect = null;
2446+
return;
2447+
}
2448+
2449+
const sibling = fiber.sibling;
2450+
if (sibling !== null) {
2451+
sibling.return = fiber.return;
2452+
nextEffect = sibling;
2453+
return;
2454+
}
2455+
2456+
nextEffect = fiber.return;
2457+
}
2458+
}
2459+
23842460
function reappearLayoutEffects_begin(subtreeRoot: Fiber) {
23852461
while (nextEffect !== null) {
23862462
const fiber = nextEffect;
@@ -2397,7 +2473,9 @@ function reappearLayoutEffects_begin(subtreeRoot: Fiber) {
23972473

23982474
// TODO (Offscreen) Check: subtreeFlags & LayoutStatic
23992475
if (firstChild !== null) {
2400-
ensureCorrectReturnPointer(firstChild, fiber);
2476+
// This node may have been reused from a previous render, so we can't
2477+
// assume its return pointer is correct.
2478+
firstChild.return = fiber;
24012479
nextEffect = firstChild;
24022480
} else {
24032481
reappearLayoutEffects_complete(subtreeRoot);
@@ -2426,7 +2504,9 @@ function reappearLayoutEffects_complete(subtreeRoot: Fiber) {
24262504

24272505
const sibling = fiber.sibling;
24282506
if (sibling !== null) {
2429-
ensureCorrectReturnPointer(sibling, fiber.return);
2507+
// This node may have been reused from a previous render, so we can't
2508+
// assume its return pointer is correct.
2509+
sibling.return = fiber.return;
24302510
nextEffect = sibling;
24312511
return;
24322512
}

0 commit comments

Comments
 (0)