@samitouri / QOS-React-2 / commits / 262ff7ad2c

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.

Andrew Clark committed Jul 16, 2021 at 18:58 UTC 262ff7ad2c4a018de9992731fcf8a8f9c91083d8
4 files changed +310 -192
packages/react-reconciler/src/ReactFiberCommitWork.new.js
+155 -85
@@ -980,12 +980,6 @@ function reappearLayoutEffectsOnFiber(node: Fiber) {
980 }
981
982 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 -
983 // Only hide or unhide the top-most host nodes.
984 let hostSubtreeRoot = null;
985
@@ -1005,22 +999,6 @@ function hideOrUnhideAllChildren(finishedWork, isHidden) {
999 unhideInstance(node.stateNode, node.memoizedProps);
1000 }
1001 }
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 - }
1002 } else if (node.tag === HostText) {
1003 if (hostSubtreeRoot === null) {
1004 const instance = node.stateNode;
@@ -1038,52 +1016,6 @@ function hideOrUnhideAllChildren(finishedWork, isHidden) {
1016 ) {
1017 // Found a nested Offscreen component that is hidden.
1018 // 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 - }
1019 } else if (node.child !== null) {
1020 node.child.return = node;
1021 node = node.child;
@@ -1801,6 +1733,11 @@ function commitWork(current: Fiber | null, finishedWork: Fiber): void {
1733 // This prevents sibling component effects from interfering with each other,
1734 // e.g. a destroy function in one component should never override a ref set
1735 // 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.)
1741 if (
1742 enableProfilerTimer &&
1743 enableProfilerCommitHooks &&
@@ -2183,20 +2120,36 @@ function commitMutationEffectsOnFiber(finishedWork: Fiber, root: FiberRoot) {
2120 switch (finishedWork.tag) {
2121 case SuspenseComponent: {
2122 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 + }
2153 }
2154 break;
2155 }
@@ -2204,7 +2157,36 @@ function commitMutationEffectsOnFiber(finishedWork: Fiber, root: FiberRoot) {
2157 case LegacyHiddenComponent: {
2158 const newState: OffscreenState | null = finishedWork.memoizedState;
2159 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 + }
2190 break;
2191 }
2192 }
@@ -2381,6 +2363,90 @@ function commitLayoutMountEffects_complete(
2363 }
2364 }
2365
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 +
2450 function reappearLayoutEffects_begin(subtreeRoot: Fiber) {
2451 while (nextEffect !== null) {
2452 const fiber = nextEffect;
@@ -2397,7 +2463,9 @@ function reappearLayoutEffects_begin(subtreeRoot: Fiber) {
2463
2464 // TODO (Offscreen) Check: subtreeFlags & LayoutStatic
2465 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;
2469 nextEffect = firstChild;
2470 } else {
2471 reappearLayoutEffects_complete(subtreeRoot);
@@ -2426,7 +2494,9 @@ function reappearLayoutEffects_complete(subtreeRoot: Fiber) {
2494
2495 const sibling = fiber.sibling;
2496 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;
2500 nextEffect = sibling;
2501 return;
2502 }
packages/react-reconciler/src/ReactFiberCommitWork.old.js
+155 -85
@@ -980,12 +980,6 @@ function reappearLayoutEffectsOnFiber(node: Fiber) {
980 }
981
982 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 -
983 // Only hide or unhide the top-most host nodes.
984 let hostSubtreeRoot = null;
985
@@ -1005,22 +999,6 @@ function hideOrUnhideAllChildren(finishedWork, isHidden) {
999 unhideInstance(node.stateNode, node.memoizedProps);
1000 }
1001 }
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 - }
1002 } else if (node.tag === HostText) {
1003 if (hostSubtreeRoot === null) {
1004 const instance = node.stateNode;
@@ -1038,52 +1016,6 @@ function hideOrUnhideAllChildren(finishedWork, isHidden) {
1016 ) {
1017 // Found a nested Offscreen component that is hidden.
1018 // 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 - }
1019 } else if (node.child !== null) {
1020 node.child.return = node;
1021 node = node.child;
@@ -1801,6 +1733,11 @@ function commitWork(current: Fiber | null, finishedWork: Fiber): void {
1733 // This prevents sibling component effects from interfering with each other,
1734 // e.g. a destroy function in one component should never override a ref set
1735 // 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.)
1741 if (
1742 enableProfilerTimer &&
1743 enableProfilerCommitHooks &&
@@ -2183,20 +2120,36 @@ function commitMutationEffectsOnFiber(finishedWork: Fiber, root: FiberRoot) {
2120 switch (finishedWork.tag) {
2121 case SuspenseComponent: {
2122 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 + }
2153 }
2154 break;
2155 }
@@ -2204,7 +2157,36 @@ function commitMutationEffectsOnFiber(finishedWork: Fiber, root: FiberRoot) {
2157 case LegacyHiddenComponent: {
2158 const newState: OffscreenState | null = finishedWork.memoizedState;
2159 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 + }
2190 break;
2191 }
2192 }
@@ -2381,6 +2363,90 @@ function commitLayoutMountEffects_complete(
2363 }
2364 }
2365
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 +
2450 function reappearLayoutEffects_begin(subtreeRoot: Fiber) {
2451 while (nextEffect !== null) {
2452 const fiber = nextEffect;
@@ -2397,7 +2463,9 @@ function reappearLayoutEffects_begin(subtreeRoot: Fiber) {
2463
2464 // TODO (Offscreen) Check: subtreeFlags & LayoutStatic
2465 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;
2469 nextEffect = firstChild;
2470 } else {
2471 reappearLayoutEffects_complete(subtreeRoot);
@@ -2426,7 +2494,9 @@ function reappearLayoutEffects_complete(subtreeRoot: Fiber) {
2494
2495 const sibling = fiber.sibling;
2496 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;
2500 nextEffect = sibling;
2501 return;
2502 }
packages/react-reconciler/src/__tests__/ReactOffscreen-test.js
-2
@@ -220,7 +220,6 @@ describe('ReactOffscreen', () => {
220
221 // @gate experimental || www
222 // @gate enableSuspenseLayoutEffectSemantics
223 - // @gate !persistent
223 it('mounts/unmounts layout effects when visibility changes (starting visible)', async () => {
224 function Child({text}) {
225 useLayoutEffect(() => {
@@ -271,7 +270,6 @@ describe('ReactOffscreen', () => {
270
271 // @gate experimental || www
272 // @gate enableSuspenseLayoutEffectSemantics
274 - // @gate !persistent
273 it('mounts/unmounts layout effects when visibility changes (starting hidden)', async () => {
274 function Child({text}) {
275 useLayoutEffect(() => {
packages/react-reconciler/src/__tests__/ReactSuspenseEffectsSemantics-test.js
-20
@@ -567,7 +567,6 @@ describe('ReactSuspenseEffectsSemantics', () => {
567
568 // @gate enableSuspenseLayoutEffectSemantics
569 // @gate enableCache
570 - // @gate !persistent
570 it('should be destroyed and recreated for function components', async () => {
571 function App({children = null}) {
572 Scheduler.unstable_yieldValue('App render');
@@ -697,7 +696,6 @@ describe('ReactSuspenseEffectsSemantics', () => {
696
697 // @gate enableSuspenseLayoutEffectSemantics
698 // @gate enableCache
700 - // @gate !persistent
699 it('should be destroyed and recreated for class components', async () => {
700 class ClassText extends React.Component {
701 componentDidMount() {
@@ -843,7 +841,6 @@ describe('ReactSuspenseEffectsSemantics', () => {
841
842 // @gate enableSuspenseLayoutEffectSemantics
843 // @gate enableCache
846 - // @gate !persistent
844 it('should be destroyed and recreated when nested below host components', async () => {
845 function App({children = null}) {
846 Scheduler.unstable_yieldValue('App render');
@@ -954,7 +951,6 @@ describe('ReactSuspenseEffectsSemantics', () => {
951
952 // @gate enableSuspenseLayoutEffectSemantics
953 // @gate enableCache
957 - // @gate !persistent
954 it('should be destroyed and recreated even if there is a bailout because of memoization', async () => {
955 const MemoizedText = React.memo(Text, () => true);
956
@@ -1071,7 +1067,6 @@ describe('ReactSuspenseEffectsSemantics', () => {
1067
1068 // @gate enableSuspenseLayoutEffectSemantics
1069 // @gate enableCache
1074 - // @gate !persistent
1070 it('should respect nested suspense boundaries', async () => {
1071 function App({innerChildren = null, outerChildren = null}) {
1072 return (
@@ -1295,7 +1290,6 @@ describe('ReactSuspenseEffectsSemantics', () => {
1290
1291 // @gate enableSuspenseLayoutEffectSemantics
1292 // @gate enableCache
1298 - // @gate !persistent
1293 it('should show nested host nodes if multiple boundaries resolve at the same time', async () => {
1294 function App({innerChildren = null, outerChildren = null}) {
1295 return (
@@ -1406,7 +1400,6 @@ describe('ReactSuspenseEffectsSemantics', () => {
1400
1401 // @gate enableSuspenseLayoutEffectSemantics
1402 // @gate enableCache
1409 - // @gate !persistent
1403 it('should be cleaned up inside of a fallback that suspends', async () => {
1404 function App({fallbackChildren = null, outerChildren = null}) {
1405 return (
@@ -1550,7 +1543,6 @@ describe('ReactSuspenseEffectsSemantics', () => {
1543
1544 // @gate enableSuspenseLayoutEffectSemantics
1545 // @gate enableCache
1553 - // @gate !persistent
1546 it('should be cleaned up inside of a fallback that suspends (alternate)', async () => {
1547 function App({fallbackChildren = null, outerChildren = null}) {
1548 return (
@@ -1671,7 +1663,6 @@ describe('ReactSuspenseEffectsSemantics', () => {
1663
1664 // @gate enableSuspenseLayoutEffectSemantics
1665 // @gate enableCache
1674 - // @gate !persistent
1666 it('should be cleaned up deeper inside of a subtree that suspends', async () => {
1667 function ConditionalSuspense({shouldSuspend}) {
1668 if (shouldSuspend) {
@@ -1755,7 +1746,6 @@ describe('ReactSuspenseEffectsSemantics', () => {
1746 describe('that throw errors', () => {
1747 // @gate enableSuspenseLayoutEffectSemantics
1748 // @gate enableCache
1758 - // @gate !persistent
1749 it('are properly handled for componentDidMount', async () => {
1750 let componentDidMountShouldThrow = false;
1751
@@ -1895,7 +1885,6 @@ describe('ReactSuspenseEffectsSemantics', () => {
1885
1886 // @gate enableSuspenseLayoutEffectSemantics
1887 // @gate enableCache
1898 - // @gate !persistent
1888 it('are properly handled for componentWillUnmount', async () => {
1889 class ThrowsInWillUnmount extends React.Component {
1890 componentDidMount() {
@@ -2009,7 +1998,6 @@ describe('ReactSuspenseEffectsSemantics', () => {
1998
1999 // @gate enableSuspenseLayoutEffectSemantics
2000 // @gate enableCache
2012 - // @gate !persistent
2001 // @gate replayFailedUnitOfWorkWithInvokeGuardedCallback
2002 it('are properly handled for layout effect creation', async () => {
2003 let useLayoutEffectShouldThrow = false;
@@ -2150,7 +2138,6 @@ describe('ReactSuspenseEffectsSemantics', () => {
2138
2139 // @gate enableSuspenseLayoutEffectSemantics
2140 // @gate enableCache
2153 - // @gate !persistent
2141 // @gate replayFailedUnitOfWorkWithInvokeGuardedCallback
2142 it('are properly handled for layout effect descruction', async () => {
2143 function ThrowsInLayoutEffectDestroy() {
@@ -2263,7 +2250,6 @@ describe('ReactSuspenseEffectsSemantics', () => {
2250
2251 // @gate enableSuspenseLayoutEffectSemantics
2252 // @gate enableCache
2266 - // @gate !persistent
2253 it('should be only destroy layout effects once if a tree suspends in multiple places', async () => {
2254 class ClassText extends React.Component {
2255 componentDidMount() {
@@ -2403,7 +2389,6 @@ describe('ReactSuspenseEffectsSemantics', () => {
2389
2390 // @gate enableSuspenseLayoutEffectSemantics
2391 // @gate enableCache
2406 - // @gate !persistent
2392 it('should be only destroy layout effects once if a component suspends multiple times', async () => {
2393 class ClassText extends React.Component {
2394 componentDidMount() {
@@ -2688,7 +2673,6 @@ describe('ReactSuspenseEffectsSemantics', () => {
2673
2674 // @gate enableSuspenseLayoutEffectSemantics
2675 // @gate enableCache
2691 - // @gate !persistent
2676 it('should be cleared and reset for host components', async () => {
2677 function App({children}) {
2678 Scheduler.unstable_yieldValue(`App render`);
@@ -2786,7 +2770,6 @@ describe('ReactSuspenseEffectsSemantics', () => {
2770
2771 // @gate enableSuspenseLayoutEffectSemantics
2772 // @gate enableCache
2789 - // @gate !persistent
2773 it('should be cleared and reset for class components', async () => {
2774 class ClassComponent extends React.Component {
2775 render() {
@@ -2888,7 +2871,6 @@ describe('ReactSuspenseEffectsSemantics', () => {
2871
2872 // @gate enableSuspenseLayoutEffectSemantics
2873 // @gate enableCache
2891 - // @gate !persistent
2874 it('should be cleared and reset for function components with useImperativeHandle', async () => {
2875 const FunctionComponent = React.forwardRef((props, ref) => {
2876 Scheduler.unstable_yieldValue('FunctionComponent render');
@@ -2994,7 +2976,6 @@ describe('ReactSuspenseEffectsSemantics', () => {
2976
2977 // @gate enableSuspenseLayoutEffectSemantics
2978 // @gate enableCache
2997 - // @gate !persistent
2979 it('should not reset for user-managed values', async () => {
2980 function RefChecker({forwardedRef}) {
2981 Scheduler.unstable_yieldValue(`RefChecker render`);
@@ -3093,7 +3074,6 @@ describe('ReactSuspenseEffectsSemantics', () => {
3074 describe('that throw errors', () => {
3075 // @gate enableSuspenseLayoutEffectSemantics
3076 // @gate enableCache
3096 - // @gate !persistent
3077 // @gate replayFailedUnitOfWorkWithInvokeGuardedCallback
3078 it('are properly handled in ref callbacks', async () => {
3079 let useRefCallbackShouldThrow = false;