Repository: facebook/react
Pull request: Implement enableSuspenseLayoutEffectSemantics in Fabric

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.

Changed files:
filename: packages/react-reconciler/src/ReactFiberCommitWork.new.js
patch: @@ -980,12 +980,6 @@ function reappearLayoutEffectsOnFiber(node: Fiber) {
 }
 
 function hideOrUnhideAllChildren(finishedWork, isHidden) {
-  // Suspense layout effects semantics don't change for legacy roots.
-  const isModernRoot = (finishedWork.mode & ConcurrentMode) !== NoMode;
-
-  const current = finishedWork.alternate;
-  const wasHidden = current !== null && current.memoizedState !== null;
-
   // Only hide or unhide the top-most host nodes.
   let hostSubtreeRoot = null;
 
@@ -1005,22 +999,6 @@ function hideOrUnhideAllChildren(finishedWork, isHidden) {
             unhideInstance(node.stateNode, node.memoizedProps);
           }
         }
-
-        if (enableSuspenseLayoutEffectSemantics && isModernRoot) {
-          // This method is called during mutation; it should detach refs within a hidden subtree.
-          // Attaching refs should be done elsewhere though (during layout).
-          // TODO (Offscreen) Also check: flags & RefStatic
-          if (isHidden) {
-            safelyDetachRef(node, finishedWork);
-          }
-
-          // TODO (Offscreen) Also check: subtreeFlags & (RefStatic | LayoutStatic)
-          if (node.child !== null) {
-            node.child.return = node;
-            node = node.child;
-            continue;
-          }
-        }
       } else if (node.tag === HostText) {
         if (hostSubtreeRoot === null) {
           const instance = node.stateNode;
@@ -1038,52 +1016,6 @@ function hideOrUnhideAllChildren(finishedWork, isHidden) {
       ) {
         // Found a nested Offscreen component that is hidden.
         // Don't search any deeper. This tree should remain hidden.
-      } else if (enableSuspenseLayoutEffectSemantics && isModernRoot) {
-        // When a mounted Suspense subtree gets hidden again, destroy any nested layout effects.
-        // TODO (Offscreen) Check: flags & (RefStatic | LayoutStatic)
-        switch (node.tag) {
-          case FunctionComponent:
-          case ForwardRef:
-          case MemoComponent:
-          case SimpleMemoComponent: {
-            // Note that refs are attached by the useImperativeHandle() hook, not by commitAttachRef()
-            if (isHidden && !wasHidden) {
-              if (
-                enableProfilerTimer &&
-                enableProfilerCommitHooks &&
-                node.mode & ProfileMode
-              ) {
-                try {
-                  startLayoutEffectTimer();
-                  commitHookEffectListUnmount(HookLayout, node, finishedWork);
-                } finally {
-                  recordLayoutEffectDuration(node);
-                }
-              } else {
-                commitHookEffectListUnmount(HookLayout, node, finishedWork);
-              }
-            }
-            break;
-          }
-          case ClassComponent: {
-            if (isHidden && !wasHidden) {
-              // TODO (Offscreen) Check: flags & RefStatic
-              safelyDetachRef(node, finishedWork);
-
-              const instance = node.stateNode;
-              if (typeof instance.componentWillUnmount === 'function') {
-                safelyCallComponentWillUnmount(node, finishedWork, instance);
-              }
-            }
-            break;
-          }
-        }
-
-        if (node.child !== null) {
-          node.child.return = node;
-          node = node.child;
-          continue;
-        }
       } else if (node.child !== null) {
         node.child.return = node;
         node = node.child;
@@ -1801,6 +1733,11 @@ function commitWork(current: Fiber | null, finishedWork: Fiber): void {
         // This prevents sibling component effects from interfering with each other,
         // e.g. a destroy function in one component should never override a ref set
         // by a create function in another component during the same commit.
+        // TODO: Check if we're inside an Offscreen subtree that disappeared
+        // during this commit. If so, we would have already unmounted its
+        // layout hooks. (However, since we null out the `destroy` function
+        // right before calling it, the behavior is already correct, so this
+        // would mostly be for modeling purposes.)
         if (
           enableProfilerTimer &&
           enableProfilerCommitHooks &&
@@ -2183,28 +2120,73 @@ function commitMutationEffectsOnFiber(finishedWork: Fiber, root: FiberRoot) {
     switch (finishedWork.tag) {
       case SuspenseComponent: {
         const newState: OffscreenState | null = finishedWork.memoizedState;
-        if (newState !== null) {
-          markCommitTimeOfFallback();
-          // Hide the Offscreen component that contains the primary children.
-          // TODO: Ideally, this effect would have been scheduled on the
-          // Offscreen fiber itself. That's how unhiding works: the Offscreen
-          // component schedules an effect on itself. However, in this case, the
-          // component didn't complete, so the fiber was never added to the
-          // effect list in the normal path. We could have appended it to the
-          // effect list in the Suspense component's second pass, but doing it
-          // this way is less complicated. This would be simpler if we got rid
-          // of the effect list and traversed the tree, like we're planning to
-          // do.
-          const primaryChildParent: Fiber = (finishedWork.child: any);
-          hideOrUnhideAllChildren(primaryChildParent, true);
+        const isHidden = newState !== null;
+        const current = finishedWork.alternate;
+        const wasHidden = current !== null && current.memoizedState !== null;
+        const offscreenBoundary: Fiber = (finishedWork.child: any);
+
+        if (isHidden) {
+          if (!wasHidden) {
+            markCommitTimeOfFallback();
+            if (supportsMutation) {
+              hideOrUnhideAllChildren(offscreenBoundary, true);
+            }
+            if (
+              enableSuspenseLayoutEffectSemantics &&
+              (offscreenBoundary.mode & ConcurrentMode) !== NoMode
+            ) {
+              let offscreenChild = offscreenBoundary.child;
+              while (offscreenChild !== null) {
+                nextEffect = offscreenChild;
+                disappearLayoutEffects_begin(offscreenChild);
+                offscreenChild = offscreenChild.sibling;
+              }
+            }
+          }
+        } else {
+          if (wasHidden) {
+            if (supportsMutation) {
+              hideOrUnhideAllChildren(offscreenBoundary, false);
+            }
+            // TODO: Move re-appear call here for symmetry?
+          }
         }
         break;
       }
       case OffscreenComponent:
       case LegacyHiddenComponent: {
         const newState: OffscreenState | null = finishedWork.memoizedState;
         const isHidden = newState !== null;
-        hideOrUnhideAllChildren(finishedWork, isHidden);
+        const current = finishedWork.alternate;
+        const wasHidden = current !== null && current.memoizedState !== null;
+        const offscreenBoundary: Fiber = finishedWork;
+
+        if (supportsMutation) {
+          // TODO: This needs to run whenever there's an insertion or update
+          // inside a hidden Offscreen tree.
+          hideOrUnhideAllChildren(offscreenBoundary, isHidden);
+        }
+
+        if (isHidden) {
+          if (!wasHidden) {
+            if (
+              enableSuspenseLayoutEffectSemantics &&
+              (offscreenBoundary.mode & ConcurrentMode) !== NoMode
+            ) {
+              nextEffect = offscreenBoundary;
+              let offscreenChild = offscreenBoundary.child;
+              while (offscreenChild !== null) {
+                nextEffect = offscreenChild;
+                disappearLayoutEffects_begin(offscreenChild);
+                offscreenChild = offscreenChild.sibling;
+              }
+            }
+          }
+        } else {
+          if (wasHidden) {
+            // TODO: Move re-appear call here for symmetry?
+          }
+        }
         break;
       }
     }
@@ -2381,6 +2363,90 @@ function commitLayoutMountEffects_complete(
   }
 }
 
+function disappearLayoutEffects_begin(subtreeRoot: Fiber) {
+  while (nextEffect !== null) {
+    const fiber = nextEffect;
+    const firstChild = fiber.child;
+
+    // TODO (Offscreen) Check: flags & (RefStatic | LayoutStatic)
+    switch (fiber.tag) {
+      case FunctionComponent:
+      case ForwardRef:
+      case MemoComponent:
+      case SimpleMemoComponent: {
+        if (
+          enableProfilerTimer &&
+          enableProfilerCommitHooks &&
+          fiber.mode & ProfileMode
+        ) {
+          try {
+            startLayoutEffectTimer();
+            commitHookEffectListUnmount(HookLayout, fiber, fiber.return);
+          } finally {
+            recordLayoutEffectDuration(fiber);
+          }
+        } else {
+          commitHookEffectListUnmount(HookLayout, fiber, fiber.return);
+        }
+        break;
+      }
+      case ClassComponent: {
+        // TODO (Offscreen) Check: flags & RefStatic
+        safelyDetachRef(fiber, fiber.return);
+
+        const instance = fiber.stateNode;
+        if (typeof instance.componentWillUnmount === 'function') {
+          safelyCallComponentWillUnmount(fiber, fiber.return, instance);
+        }
+        break;
+      }
+      case HostComponent: {
+        safelyDetachRef(fiber, fiber.return);
+        break;
+      }
+      case OffscreenComponent: {
+        // Check if this is a
+        const isHidden = fiber.memoizedState !== null;
+        if (isHidden) {
+          // Nested Offscreen tree is already hidden. Don't disappear
+          // its effects.
+          disappearLayoutEffects_complete(subtreeRoot);
+          continue;
+        }
+        break;
+      }
+    }
+
+    // TODO (Offscreen) Check: subtreeFlags & LayoutStatic
+    if (firstChild !== null) {
+      firstChild.return = fiber;
+      nextEffect = firstChild;
+    } else {
+      disappearLayoutEffects_complete(subtreeRoot);
+    }
+  }
+}
+
+function disappearLayoutEffects_complete(subtreeRoot: Fiber) {
+  while (nextEffect !== null) {
+    const fiber = nextEffect;
+
+    if (fiber === subtreeRoot) {
+      nextEffect = null;
+      return;
+    }
+
+    const sibling = fiber.sibling;
+    if (sibling !== null) {
+      sibling.return = fiber.return;
+      nextEffect = sibling;
+      return;
+    }
+
+    nextEffect = fiber.return;
+  }
+}
+
 function reappearLayoutEffects_begin(subtreeRoot: Fiber) {
   while (nextEffect !== null) {
     const fiber = nextEffect;
@@ -2397,7 +2463,9 @@ function reappearLayoutEffects_begin(subtreeRoot: Fiber) {
 
     // TODO (Offscreen) Check: subtreeFlags & LayoutStatic
     if (firstChild !== null) {
-      ensureCorrectReturnPointer(firstChild, fiber);
+      // This node may have been reused from a previous render, so we can't
+      // assume its return pointer is correct.
+      firstChild.return = fiber;
       nextEffect = firstChild;
     } else {
       reappearLayoutEffects_complete(subtreeRoot);
@@ -2426,7 +2494,9 @@ function reappearLayoutEffects_complete(subtreeRoot: Fiber) {
 
     const sibling = fiber.sibling;
     if (sibling !== null) {
-      ensureCorrectReturnPointer(sibling, fiber.return);
+      // This node may have been reused from a previous render, so we can't
+      // assume its return pointer is correct.
+      sibling.return = fiber.return;
       nextEffect = sibling;
       return;
     }

filename: packages/react-reconciler/src/ReactFiberCommitWork.old.js
patch: @@ -980,12 +980,6 @@ function reappearLayoutEffectsOnFiber(node: Fiber) {
 }
 
 function hideOrUnhideAllChildren(finishedWork, isHidden) {
-  // Suspense layout effects semantics don't change for legacy roots.
-  const isModernRoot = (finishedWork.mode & ConcurrentMode) !== NoMode;
-
-  const current = finishedWork.alternate;
-  const wasHidden = current !== null && current.memoizedState !== null;
-
   // Only hide or unhide the top-most host nodes.
   let hostSubtreeRoot = null;
 
@@ -1005,22 +999,6 @@ function hideOrUnhideAllChildren(finishedWork, isHidden) {
             unhideInstance(node.stateNode, node.memoizedProps);
           }
         }
-
-        if (enableSuspenseLayoutEffectSemantics && isModernRoot) {
-          // This method is called during mutation; it should detach refs within a hidden subtree.
-          // Attaching refs should be done elsewhere though (during layout).
-          // TODO (Offscreen) Also check: flags & RefStatic
-          if (isHidden) {
-            safelyDetachRef(node, finishedWork);
-          }
-
-          // TODO (Offscreen) Also check: subtreeFlags & (RefStatic | LayoutStatic)
-          if (node.child !== null) {
-            node.child.return = node;
-            node = node.child;
-            continue;
-          }
-        }
       } else if (node.tag === HostText) {
         if (hostSubtreeRoot === null) {
           const instance = node.stateNode;
@@ -1038,52 +1016,6 @@ function hideOrUnhideAllChildren(finishedWork, isHidden) {
       ) {
         // Found a nested Offscreen component that is hidden.
         // Don't search any deeper. This tree should remain hidden.
-      } else if (enableSuspenseLayoutEffectSemantics && isModernRoot) {
-        // When a mounted Suspense subtree gets hidden again, destroy any nested layout effects.
-        // TODO (Offscreen) Check: flags & (RefStatic | LayoutStatic)
-        switch (node.tag) {
-          case FunctionComponent:
-          case ForwardRef:
-          case MemoComponent:
-          case SimpleMemoComponent: {
-            // Note that refs are attached by the useImperativeHandle() hook, not by commitAttachRef()
-            if (isHidden && !wasHidden) {
-              if (
-                enableProfilerTimer &&
-                enableProfilerCommitHooks &&
-                node.mode & ProfileMode
-              ) {
-                try {
-                  startLayoutEffectTimer();
-                  commitHookEffectListUnmount(HookLayout, node, finishedWork);
-                } finally {
-                  recordLayoutEffectDuration(node);
-                }
-              } else {
-                commitHookEffectListUnmount(HookLayout, node, finishedWork);
-              }
-            }
-            break;
-          }
-          case ClassComponent: {
-            if (isHidden && !wasHidden) {
-              // TODO (Offscreen) Check: flags & RefStatic
-              safelyDetachRef(node, finishedWork);
-
-              const instance = node.stateNode;
-              if (typeof instance.componentWillUnmount === 'function') {
-                safelyCallComponentWillUnmount(node, finishedWork, instance);
-              }
-            }
-            break;
-          }
-        }
-
-        if (node.child !== null) {
-          node.child.return = node;
-          node = node.child;
-          continue;
-        }
       } else if (node.child !== null) {
         node.child.return = node;
         node = node.child;
@@ -1801,6 +1733,11 @@ function commitWork(current: Fiber | null, finishedWork: Fiber): void {
         // This prevents sibling component effects from interfering with each other,
         // e.g. a destroy function in one component should never override a ref set
         // by a create function in another component during the same commit.
+        // TODO: Check if we're inside an Offscreen subtree that disappeared
+        // during this commit. If so, we would have already unmounted its
+        // layout hooks. (However, since we null out the `destroy` function
+        // right before calling it, the behavior is already correct, so this
+        // would mostly be for modeling purposes.)
         if (
           enableProfilerTimer &&
           enableProfilerCommitHooks &&
@@ -2183,28 +2120,73 @@ function commitMutationEffectsOnFiber(finishedWork: Fiber, root: FiberRoot) {
     switch (finishedWork.tag) {
       case SuspenseComponent: {
         const newState: OffscreenState | null = finishedWork.memoizedState;
-        if (newState !== null) {
-          markCommitTimeOfFallback();
-          // Hide the Offscreen component that contains the primary children.
-          // TODO: Ideally, this effect would have been scheduled on the
-          // Offscreen fiber itself. That's how unhiding works: the Offscreen
-          // component schedules an effect on itself. However, in this case, the
-          // component didn't complete, so the fiber was never added to the
-          // effect list in the normal path. We could have appended it to the
-          // effect list in the Suspense component's second pass, but doing it
-          // this way is less complicated. This would be simpler if we got rid
-          // of the effect list and traversed the tree, like we're planning to
-          // do.
-          const primaryChildParent: Fiber = (finishedWork.child: any);
-          hideOrUnhideAllChildren(primaryChildParent, true);
+        const isHidden = newState !== null;
+        const current = finishedWork.alternate;
+        const wasHidden = current !== null && current.memoizedState !== null;
+        const offscreenBoundary: Fiber = (finishedWork.child: any);
+
+        if (isHidden) {
+          if (!wasHidden) {
+            markCommitTimeOfFallback();
+            if (supportsMutation) {
+              hideOrUnhideAllChildren(offscreenBoundary, true);
+            }
+            if (
+              enableSuspenseLayoutEffectSemantics &&
+              (offscreenBoundary.mode & ConcurrentMode) !== NoMode
+            ) {
+              let offscreenChild = offscreenBoundary.child;
+              while (offscreenChild !== null) {
+                nextEffect = offscreenChild;
+                disappearLayoutEffects_begin(offscreenChild);
+                offscreenChild = offscreenChild.sibling;
+              }
+            }
+          }
+        } else {
+          if (wasHidden) {
+            if (supportsMutation) {
+              hideOrUnhideAllChildren(offscreenBoundary, false);
+            }
+            // TODO: Move re-appear call here for symmetry?
+          }
         }
         break;
       }
       case OffscreenComponent:
       case LegacyHiddenComponent: {
         const newState: OffscreenState | null = finishedWork.memoizedState;
         const isHidden = newState !== null;
-        hideOrUnhideAllChildren(finishedWork, isHidden);
+        const current = finishedWork.alternate;
+        const wasHidden = current !== null && current.memoizedState !== null;
+        const offscreenBoundary: Fiber = finishedWork;
+
+        if (supportsMutation) {
+          // TODO: This needs to run whenever there's an insertion or update
+          // inside a hidden Offscreen tree.
+          hideOrUnhideAllChildren(offscreenBoundary, isHidden);
+        }
+
+        if (isHidden) {
+          if (!wasHidden) {
+            if (
+              enableSuspenseLayoutEffectSemantics &&
+              (offscreenBoundary.mode & ConcurrentMode) !== NoMode
+            ) {
+              nextEffect = offscreenBoundary;
+              let offscreenChild = offscreenBoundary.child;
+              while (offscreenChild !== null) {
+                nextEffect = offscreenChild;
+                disappearLayoutEffects_begin(offscreenChild);
+                offscreenChild = offscreenChild.sibling;
+              }
+            }
+          }
+        } else {
+          if (wasHidden) {
+            // TODO: Move re-appear call here for symmetry?
+          }
+        }
         break;
       }
     }
@@ -2381,6 +2363,90 @@ function commitLayoutMountEffects_complete(
   }
 }
 
+function disappearLayoutEffects_begin(subtreeRoot: Fiber) {
+  while (nextEffect !== null) {
+    const fiber = nextEffect;
+    const firstChild = fiber.child;
+
+    // TODO (Offscreen) Check: flags & (RefStatic | LayoutStatic)
+    switch (fiber.tag) {
+      case FunctionComponent:
+      case ForwardRef:
+      case MemoComponent:
+      case SimpleMemoComponent: {
+        if (
+          enableProfilerTimer &&
+          enableProfilerCommitHooks &&
+          fiber.mode & ProfileMode
+        ) {
+          try {
+            startLayoutEffectTimer();
+            commitHookEffectListUnmount(HookLayout, fiber, fiber.return);
+          } finally {
+            recordLayoutEffectDuration(fiber);
+          }
+        } else {
+          commitHookEffectListUnmount(HookLayout, fiber, fiber.return);
+        }
+        break;
+      }
+      case ClassComponent: {
+        // TODO (Offscreen) Check: flags & RefStatic
+        safelyDetachRef(fiber, fiber.return);
+
+        const instance = fiber.stateNode;
+        if (typeof instance.componentWillUnmount === 'function') {
+          safelyCallComponentWillUnmount(fiber, fiber.return, instance);
+        }
+        break;
+      }
+      case HostComponent: {
+        safelyDetachRef(fiber, fiber.return);
+        break;
+      }
+      case OffscreenComponent: {
+        // Check if this is a
+        const isHidden = fiber.memoizedState !== null;
+        if (isHidden) {
+          // Nested Offscreen tree is already hidden. Don't disappear
+          // its effects.
+          disappearLayoutEffects_complete(subtreeRoot);
+          continue;
+        }
+        break;
+      }
+    }
+
+    // TODO (Offscreen) Check: subtreeFlags & LayoutStatic
+    if (firstChild !== null) {
+      firstChild.return = fiber;
+      nextEffect = firstChild;
+    } else {
+      disappearLayoutEffects_complete(subtreeRoot);
+    }
+  }
+}
+
+function disappearLayoutEffects_complete(subtreeRoot: Fiber) {
+  while (nextEffect !== null) {
+    const fiber = nextEffect;
+
+    if (fiber === subtreeRoot) {
+      nextEffect = null;
+      return;
+    }
+
+    const sibling = fiber.sibling;
+    if (sibling !== null) {
+      sibling.return = fiber.return;
+      nextEffect = sibling;
+      return;
+    }
+
+    nextEffect = fiber.return;
+  }
+}
+
 function reappearLayoutEffects_begin(subtreeRoot: Fiber) {
   while (nextEffect !== null) {
     const fiber = nextEffect;
@@ -2397,7 +2463,9 @@ function reappearLayoutEffects_begin(subtreeRoot: Fiber) {
 
     // TODO (Offscreen) Check: subtreeFlags & LayoutStatic
     if (firstChild !== null) {
-      ensureCorrectReturnPointer(firstChild, fiber);
+      // This node may have been reused from a previous render, so we can't
+      // assume its return pointer is correct.
+      firstChild.return = fiber;
       nextEffect = firstChild;
     } else {
       reappearLayoutEffects_complete(subtreeRoot);
@@ -2426,7 +2494,9 @@ function reappearLayoutEffects_complete(subtreeRoot: Fiber) {
 
     const sibling = fiber.sibling;
     if (sibling !== null) {
-      ensureCorrectReturnPointer(sibling, fiber.return);
+      // This node may have been reused from a previous render, so we can't
+      // assume its return pointer is correct.
+      sibling.return = fiber.return;
       nextEffect = sibling;
       return;
     }

filename: packages/react-reconciler/src/__tests__/ReactOffscreen-test.js
patch: @@ -220,7 +220,6 @@ describe('ReactOffscreen', () => {
 
   // @gate experimental || www
   // @gate enableSuspenseLayoutEffectSemantics
-  // @gate !persistent
   it('mounts/unmounts layout effects when visibility changes (starting visible)', async () => {
     function Child({text}) {
       useLayoutEffect(() => {
@@ -271,7 +270,6 @@ describe('ReactOffscreen', () => {
 
   // @gate experimental || www
   // @gate enableSuspenseLayoutEffectSemantics
-  // @gate !persistent
   it('mounts/unmounts layout effects when visibility changes (starting hidden)', async () => {
     function Child({text}) {
       useLayoutEffect(() => {

filename: packages/react-reconciler/src/__tests__/ReactSuspenseEffectsSemantics-test.js
patch: @@ -567,7 +567,6 @@ describe('ReactSuspenseEffectsSemantics', () => {
 
     // @gate enableSuspenseLayoutEffectSemantics
     // @gate enableCache
-    // @gate !persistent
     it('should be destroyed and recreated for function components', async () => {
       function App({children = null}) {
         Scheduler.unstable_yieldValue('App render');
@@ -697,7 +696,6 @@ describe('ReactSuspenseEffectsSemantics', () => {
 
     // @gate enableSuspenseLayoutEffectSemantics
     // @gate enableCache
-    // @gate !persistent
     it('should be destroyed and recreated for class components', async () => {
       class ClassText extends React.Component {
         componentDidMount() {
@@ -843,7 +841,6 @@ describe('ReactSuspenseEffectsSemantics', () => {
 
     // @gate enableSuspenseLayoutEffectSemantics
     // @gate enableCache
-    // @gate !persistent
     it('should be destroyed and recreated when nested below host components', async () => {
       function App({children = null}) {
         Scheduler.unstable_yieldValue('App render');
@@ -954,7 +951,6 @@ describe('ReactSuspenseEffectsSemantics', () => {
 
     // @gate enableSuspenseLayoutEffectSemantics
     // @gate enableCache
-    // @gate !persistent
     it('should be destroyed and recreated even if there is a bailout because of memoization', async () => {
       const MemoizedText = React.memo(Text, () => true);
 
@@ -1071,7 +1067,6 @@ describe('ReactSuspenseEffectsSemantics', () => {
 
     // @gate enableSuspenseLayoutEffectSemantics
     // @gate enableCache
-    // @gate !persistent
     it('should respect nested suspense boundaries', async () => {
       function App({innerChildren = null, outerChildren = null}) {
         return (
@@ -1295,7 +1290,6 @@ describe('ReactSuspenseEffectsSemantics', () => {
 
     // @gate enableSuspenseLayoutEffectSemantics
     // @gate enableCache
-    // @gate !persistent
     it('should show nested host nodes if multiple boundaries resolve at the same time', async () => {
       function App({innerChildren = null, outerChildren = null}) {
         return (
@@ -1406,7 +1400,6 @@ describe('ReactSuspenseEffectsSemantics', () => {
 
     // @gate enableSuspenseLayoutEffectSemantics
     // @gate enableCache
-    // @gate !persistent
     it('should be cleaned up inside of a fallback that suspends', async () => {
       function App({fallbackChildren = null, outerChildren = null}) {
         return (
@@ -1550,7 +1543,6 @@ describe('ReactSuspenseEffectsSemantics', () => {
 
     // @gate enableSuspenseLayoutEffectSemantics
     // @gate enableCache
-    // @gate !persistent
     it('should be cleaned up inside of a fallback that suspends (alternate)', async () => {
       function App({fallbackChildren = null, outerChildren = null}) {
         return (
@@ -1671,7 +1663,6 @@ describe('ReactSuspenseEffectsSemantics', () => {
 
     // @gate enableSuspenseLayoutEffectSemantics
     // @gate enableCache
-    // @gate !persistent
     it('should be cleaned up deeper inside of a subtree that suspends', async () => {
       function ConditionalSuspense({shouldSuspend}) {
         if (shouldSuspend) {
@@ -1755,7 +1746,6 @@ describe('ReactSuspenseEffectsSemantics', () => {
     describe('that throw errors', () => {
       // @gate enableSuspenseLayoutEffectSemantics
       // @gate enableCache
-      // @gate !persistent
       it('are properly handled for componentDidMount', async () => {
         let componentDidMountShouldThrow = false;
 
@@ -1895,7 +1885,6 @@ describe('ReactSuspenseEffectsSemantics', () => {
 
       // @gate enableSuspenseLayoutEffectSemantics
       // @gate enableCache
-      // @gate !persistent
       it('are properly handled for componentWillUnmount', async () => {
         class ThrowsInWillUnmount extends React.Component {
           componentDidMount() {
@@ -2009,7 +1998,6 @@ describe('ReactSuspenseEffectsSemantics', () => {
 
       // @gate enableSuspenseLayoutEffectSemantics
       // @gate enableCache
-      // @gate !persistent
       // @gate replayFailedUnitOfWorkWithInvokeGuardedCallback
       it('are properly handled for layout effect creation', async () => {
         let useLayoutEffectShouldThrow = false;
@@ -2150,7 +2138,6 @@ describe('ReactSuspenseEffectsSemantics', () => {
 
       // @gate enableSuspenseLayoutEffectSemantics
       // @gate enableCache
-      // @gate !persistent
       // @gate replayFailedUnitOfWorkWithInvokeGuardedCallback
       it('are properly handled for layout effect descruction', async () => {
         function ThrowsInLayoutEffectDestroy() {
@@ -2263,7 +2250,6 @@ describe('ReactSuspenseEffectsSemantics', () => {
 
     // @gate enableSuspenseLayoutEffectSemantics
     // @gate enableCache
-    // @gate !persistent
     it('should be only destroy layout effects once if a tree suspends in multiple places', async () => {
       class ClassText extends React.Component {
         componentDidMount() {
@@ -2403,7 +2389,6 @@ describe('ReactSuspenseEffectsSemantics', () => {
 
     // @gate enableSuspenseLayoutEffectSemantics
     // @gate enableCache
-    // @gate !persistent
     it('should be only destroy layout effects once if a component suspends multiple times', async () => {
       class ClassText extends React.Component {
         componentDidMount() {
@@ -2688,7 +2673,6 @@ describe('ReactSuspenseEffectsSemantics', () => {
 
     // @gate enableSuspenseLayoutEffectSemantics
     // @gate enableCache
-    // @gate !persistent
     it('should be cleared and reset for host components', async () => {
       function App({children}) {
         Scheduler.unstable_yieldValue(`App render`);
@@ -2786,7 +2770,6 @@ describe('ReactSuspenseEffectsSemantics', () => {
 
     // @gate enableSuspenseLayoutEffectSemantics
     // @gate enableCache
-    // @gate !persistent
     it('should be cleared and reset for class components', async () => {
       class ClassComponent extends React.Component {
         render() {
@@ -2888,7 +2871,6 @@ describe('ReactSuspenseEffectsSemantics', () => {
 
     // @gate enableSuspenseLayoutEffectSemantics
     // @gate enableCache
-    // @gate !persistent
     it('should be cleared and reset for function components with useImperativeHandle', async () => {
       const FunctionComponent = React.forwardRef((props, ref) => {
         Scheduler.unstable_yieldValue('FunctionComponent render');
@@ -2994,7 +2976,6 @@ describe('ReactSuspenseEffectsSemantics', () => {
 
     // @gate enableSuspenseLayoutEffectSemantics
     // @gate enableCache
-    // @gate !persistent
     it('should not reset for user-managed values', async () => {
       function RefChecker({forwardedRef}) {
         Scheduler.unstable_yieldValue(`RefChecker render`);
@@ -3093,7 +3074,6 @@ describe('ReactSuspenseEffectsSemantics', () => {
     describe('that throw errors', () => {
       // @gate enableSuspenseLayoutEffectSemantics
       // @gate enableCache
-      // @gate !persistent
       // @gate replayFailedUnitOfWorkWithInvokeGuardedCallback
       it('are properly handled in ref callbacks', async () => {
         let useRefCallbackShouldThrow = false;