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;