Skip to content

Commit 262ff7a

Browse files
authored
Refactor "disappear" logic into its own traversal (#21901)
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 262ff7a

File tree

4 files changed

+310
-192
lines changed

4 files changed

+310
-192
lines changed

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

Lines changed: 155 additions & 85 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,28 +2120,73 @@ 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();
2188-
// Hide the Offscreen component that contains the primary children.
2189-
// TODO: Ideally, this effect would have been scheduled on the
2190-
// Offscreen fiber itself. That's how unhiding works: the Offscreen
2191-
// component schedules an effect on itself. However, in this case, the
2192-
// component didn't complete, so the fiber was never added to the
2193-
// effect list in the normal path. We could have appended it to the
2194-
// effect list in the Suspense component's second pass, but doing it
2195-
// this way is less complicated. This would be simpler if we got rid
2196-
// of the effect list and traversed the tree, like we're planning to
2197-
// do.
2198-
const primaryChildParent: Fiber = (finishedWork.child: any);
2199-
hideOrUnhideAllChildren(primaryChildParent, true);
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) {
2129+
if (!wasHidden) {
2130+
markCommitTimeOfFallback();
2131+
if (supportsMutation) {
2132+
hideOrUnhideAllChildren(offscreenBoundary, true);
2133+
}
2134+
if (
2135+
enableSuspenseLayoutEffectSemantics &&
2136+
(offscreenBoundary.mode & ConcurrentMode) !== NoMode
2137+
) {
2138+
let offscreenChild = offscreenBoundary.child;
2139+
while (offscreenChild !== null) {
2140+
nextEffect = offscreenChild;
2141+
disappearLayoutEffects_begin(offscreenChild);
2142+
offscreenChild = offscreenChild.sibling;
2143+
}
2144+
}
2145+
}
2146+
} else {
2147+
if (wasHidden) {
2148+
if (supportsMutation) {
2149+
hideOrUnhideAllChildren(offscreenBoundary, false);
2150+
}
2151+
// TODO: Move re-appear call here for symmetry?
2152+
}
22002153
}
22012154
break;
22022155
}
22032156
case OffscreenComponent:
22042157
case LegacyHiddenComponent: {
22052158
const newState: OffscreenState | null = finishedWork.memoizedState;
22062159
const isHidden = newState !== null;
2207-
hideOrUnhideAllChildren(finishedWork, isHidden);
2160+
const current = finishedWork.alternate;
2161+
const wasHidden = current !== null && current.memoizedState !== null;
2162+
const offscreenBoundary: Fiber = finishedWork;
2163+
2164+
if (supportsMutation) {
2165+
// TODO: This needs to run whenever there's an insertion or update
2166+
// inside a hidden Offscreen tree.
2167+
hideOrUnhideAllChildren(offscreenBoundary, isHidden);
2168+
}
2169+
2170+
if (isHidden) {
2171+
if (!wasHidden) {
2172+
if (
2173+
enableSuspenseLayoutEffectSemantics &&
2174+
(offscreenBoundary.mode & ConcurrentMode) !== NoMode
2175+
) {
2176+
nextEffect = offscreenBoundary;
2177+
let offscreenChild = offscreenBoundary.child;
2178+
while (offscreenChild !== null) {
2179+
nextEffect = offscreenChild;
2180+
disappearLayoutEffects_begin(offscreenChild);
2181+
offscreenChild = offscreenChild.sibling;
2182+
}
2183+
}
2184+
}
2185+
} else {
2186+
if (wasHidden) {
2187+
// TODO: Move re-appear call here for symmetry?
2188+
}
2189+
}
22082190
break;
22092191
}
22102192
}
@@ -2381,6 +2363,90 @@ function commitLayoutMountEffects_complete(
23812363
}
23822364
}
23832365

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

23982464
// TODO (Offscreen) Check: subtreeFlags & LayoutStatic
23992465
if (firstChild !== null) {
2400-
ensureCorrectReturnPointer(firstChild, fiber);
2466+
// This node may have been reused from a previous render, so we can't
2467+
// assume its return pointer is correct.
2468+
firstChild.return = fiber;
24012469
nextEffect = firstChild;
24022470
} else {
24032471
reappearLayoutEffects_complete(subtreeRoot);
@@ -2426,7 +2494,9 @@ function reappearLayoutEffects_complete(subtreeRoot: Fiber) {
24262494

24272495
const sibling = fiber.sibling;
24282496
if (sibling !== null) {
2429-
ensureCorrectReturnPointer(sibling, fiber.return);
2497+
// This node may have been reused from a previous render, so we can't
2498+
// assume its return pointer is correct.
2499+
sibling.return = fiber.return;
24302500
nextEffect = sibling;
24312501
return;
24322502
}

0 commit comments

Comments
 (0)