@samitouri / QOS-React-2 / commits / 7c1ba2b57d

Proposed new Suspense layout effect semantics (#21079)

This commit contains a proposed change to layout effect semantics within Suspense subtrees: If a component mounts within a Suspense boundary and is later hidden (because of something else suspending) React will cleanup that component’s layout effects (including React-managed refs). This change will hopefully fix existing bugs that occur because of things like reading layout in a hidden tree and will also enable a point at which to e.g. pause videos and hide user-managed portals. After the suspended boundary resolves, React will setup the component’s layout effects again (including React-managed refs). The scenario described above is not common. The useTransition API should ensure that Suspense does not revert to its fallback state after being mounted. Note that these changes are primarily written in terms of the (as of yet internal) Offscreen API as we intend to provide similar effects semantics within recently shown/hidden Offscreen trees in the future. (More to follow.) (Note that all changes in this PR are behind a new feature flag, enableSuspenseLayoutEffectSemantics, which is disabled for now.)

Brian Vaughn committed Apr 6, 2021 at 09:21 UTC 7c1ba2b57d37df165da0f19c65ccc174866b5af7
25 files changed +3878 -140
packages/react-reconciler/src/ReactFiberBeginWork.new.js
+5
@@ -66,6 +66,7 @@ import {
66 DidCapture,
67 Update,
68 Ref,
69 + RefStatic,
70 ChildDeletion,
71 ForceUpdateForLegacySuspense,
72 StaticMask,
@@ -83,6 +84,7 @@ import {
84 enableScopeAPI,
85 enableCache,
86 enableLazyContextPropagation,
87 + enableSuspenseLayoutEffectSemantics,
88 } from 'shared/ReactFeatureFlags';
89 import invariant from 'shared/invariant';
90 import shallowEqual from 'shared/shallowEqual';
@@ -854,6 +856,9 @@ function markRef(current: Fiber | null, workInProgress: Fiber) {
856 ) {
857 // Schedule a Ref effect
858 workInProgress.flags |= Ref;
859 + if (enableSuspenseLayoutEffectSemantics) {
860 + workInProgress.flags |= RefStatic;
861 + }
862 }
863 }
864
packages/react-reconciler/src/ReactFiberBeginWork.old.js
+5
@@ -66,6 +66,7 @@ import {
66 DidCapture,
67 Update,
68 Ref,
69 + RefStatic,
70 ChildDeletion,
71 ForceUpdateForLegacySuspense,
72 StaticMask,
@@ -83,6 +84,7 @@ import {
84 enableScopeAPI,
85 enableCache,
86 enableLazyContextPropagation,
87 + enableSuspenseLayoutEffectSemantics,
88 } from 'shared/ReactFeatureFlags';
89 import invariant from 'shared/invariant';
90 import shallowEqual from 'shared/shallowEqual';
@@ -854,6 +856,9 @@ function markRef(current: Fiber | null, workInProgress: Fiber) {
856 ) {
857 // Schedule a Ref effect
858 workInProgress.flags |= Ref;
859 + if (enableSuspenseLayoutEffectSemantics) {
860 + workInProgress.flags |= RefStatic;
861 + }
862 }
863 }
864
packages/react-reconciler/src/ReactFiberClassComponent.new.js
+32 -13
@@ -10,9 +10,15 @@
10 import type {Fiber} from './ReactInternalTypes';
11 import type {Lanes} from './ReactFiberLane.new';
12 import type {UpdateQueue} from './ReactUpdateQueue.new';
13 +import type {Flags} from './ReactFiberFlags';
14
15 import * as React from 'react';
15 -import {MountLayoutDev, Update, Snapshot} from './ReactFiberFlags';
16 +import {
17 + LayoutStatic,
18 + MountLayoutDev,
19 + Update,
20 + Snapshot,
21 +} from './ReactFiberFlags';
22 import {
23 debugRenderPhaseSideEffectsForStrictMode,
24 disableLegacyContext,
@@ -21,6 +27,7 @@ import {
27 warnAboutDeprecatedLifecycles,
28 enableStrictEffects,
29 enableLazyContextPropagation,
30 + enableSuspenseLayoutEffectSemantics,
31 } from 'shared/ReactFeatureFlags';
32 import ReactStrictModeWarnings from './ReactStrictModeWarnings.new';
33 import {isMounted} from './ReactFiberTreeReflection';
@@ -908,16 +915,19 @@ function mountClassInstance(
915 }
916
917 if (typeof instance.componentDidMount === 'function') {
918 + let fiberFlags: Flags = Update;
919 + if (enableSuspenseLayoutEffectSemantics) {
920 + fiberFlags |= LayoutStatic;
921 + }
922 if (
923 __DEV__ &&
924 enableStrictEffects &&
925 (workInProgress.mode & StrictEffectsMode) !== NoMode
926 ) {
927 // Never double-invoke effects for legacy roots.
917 - workInProgress.flags |= MountLayoutDev | Update;
918 - } else {
919 - workInProgress.flags |= Update;
928 + fiberFlags |= MountLayoutDev;
929 }
930 + workInProgress.flags |= fiberFlags;
931 }
932 }
933
@@ -987,16 +997,19 @@ function resumeMountClassInstance(
997 // If an update was already in progress, we should schedule an Update
998 // effect even though we're bailing out, so that cWU/cDU are called.
999 if (typeof instance.componentDidMount === 'function') {
1000 + let fiberFlags: Flags = Update;
1001 + if (enableSuspenseLayoutEffectSemantics) {
1002 + fiberFlags |= LayoutStatic;
1003 + }
1004 if (
1005 __DEV__ &&
1006 enableStrictEffects &&
1007 (workInProgress.mode & StrictEffectsMode) !== NoMode
1008 ) {
1009 // Never double-invoke effects for legacy roots.
996 - workInProgress.flags |= MountLayoutDev | Update;
997 - } else {
998 - workInProgress.flags |= Update;
1010 + fiberFlags |= MountLayoutDev;
1011 }
1012 + workInProgress.flags |= fiberFlags;
1013 }
1014 return false;
1015 }
@@ -1039,31 +1052,37 @@ function resumeMountClassInstance(
1052 }
1053 }
1054 if (typeof instance.componentDidMount === 'function') {
1055 + let fiberFlags: Flags = Update;
1056 + if (enableSuspenseLayoutEffectSemantics) {
1057 + fiberFlags |= LayoutStatic;
1058 + }
1059 if (
1060 __DEV__ &&
1061 enableStrictEffects &&
1062 (workInProgress.mode & StrictEffectsMode) !== NoMode
1063 ) {
1064 // Never double-invoke effects for legacy roots.
1048 - workInProgress.flags |= MountLayoutDev | Update;
1049 - } else {
1050 - workInProgress.flags |= Update;
1065 + fiberFlags |= MountLayoutDev;
1066 }
1067 + workInProgress.flags |= fiberFlags;
1068 }
1069 } else {
1070 // If an update was already in progress, we should schedule an Update
1071 // effect even though we're bailing out, so that cWU/cDU are called.
1072 if (typeof instance.componentDidMount === 'function') {
1073 + let fiberFlags: Flags = Update;
1074 + if (enableSuspenseLayoutEffectSemantics) {
1075 + fiberFlags |= LayoutStatic;
1076 + }
1077 if (
1078 __DEV__ &&
1079 enableStrictEffects &&
1080 (workInProgress.mode & StrictEffectsMode) !== NoMode
1081 ) {
1082 // Never double-invoke effects for legacy roots.
1063 - workInProgress.flags |= MountLayoutDev | Update;
1064 - } else {
1065 - workInProgress.flags |= Update;
1083 + fiberFlags |= MountLayoutDev;
1084 }
1085 + workInProgress.flags |= fiberFlags;
1086 }
1087
1088 // If shouldComponentUpdate returned false, we should still update the
packages/react-reconciler/src/ReactFiberClassComponent.old.js
+32 -13
@@ -10,9 +10,15 @@
10 import type {Fiber} from './ReactInternalTypes';
11 import type {Lanes} from './ReactFiberLane.old';
12 import type {UpdateQueue} from './ReactUpdateQueue.old';
13 +import type {Flags} from './ReactFiberFlags';
14
15 import * as React from 'react';
15 -import {MountLayoutDev, Update, Snapshot} from './ReactFiberFlags';
16 +import {
17 + LayoutStatic,
18 + MountLayoutDev,
19 + Update,
20 + Snapshot,
21 +} from './ReactFiberFlags';
22 import {
23 debugRenderPhaseSideEffectsForStrictMode,
24 disableLegacyContext,
@@ -21,6 +27,7 @@ import {
27 warnAboutDeprecatedLifecycles,
28 enableStrictEffects,
29 enableLazyContextPropagation,
30 + enableSuspenseLayoutEffectSemantics,
31 } from 'shared/ReactFeatureFlags';
32 import ReactStrictModeWarnings from './ReactStrictModeWarnings.old';
33 import {isMounted} from './ReactFiberTreeReflection';
@@ -908,16 +915,19 @@ function mountClassInstance(
915 }
916
917 if (typeof instance.componentDidMount === 'function') {
918 + let fiberFlags: Flags = Update;
919 + if (enableSuspenseLayoutEffectSemantics) {
920 + fiberFlags |= LayoutStatic;
921 + }
922 if (
923 __DEV__ &&
924 enableStrictEffects &&
925 (workInProgress.mode & StrictEffectsMode) !== NoMode
926 ) {
927 // Never double-invoke effects for legacy roots.
917 - workInProgress.flags |= MountLayoutDev | Update;
918 - } else {
919 - workInProgress.flags |= Update;
928 + fiberFlags |= MountLayoutDev;
929 }
930 + workInProgress.flags |= fiberFlags;
931 }
932 }
933
@@ -987,16 +997,19 @@ function resumeMountClassInstance(
997 // If an update was already in progress, we should schedule an Update
998 // effect even though we're bailing out, so that cWU/cDU are called.
999 if (typeof instance.componentDidMount === 'function') {
1000 + let fiberFlags: Flags = Update;
1001 + if (enableSuspenseLayoutEffectSemantics) {
1002 + fiberFlags |= LayoutStatic;
1003 + }
1004 if (
1005 __DEV__ &&
1006 enableStrictEffects &&
1007 (workInProgress.mode & StrictEffectsMode) !== NoMode
1008 ) {
1009 // Never double-invoke effects for legacy roots.
996 - workInProgress.flags |= MountLayoutDev | Update;
997 - } else {
998 - workInProgress.flags |= Update;
1010 + fiberFlags |= MountLayoutDev;
1011 }
1012 + workInProgress.flags |= fiberFlags;
1013 }
1014 return false;
1015 }
@@ -1039,31 +1052,37 @@ function resumeMountClassInstance(
1052 }
1053 }
1054 if (typeof instance.componentDidMount === 'function') {
1055 + let fiberFlags: Flags = Update;
1056 + if (enableSuspenseLayoutEffectSemantics) {
1057 + fiberFlags |= LayoutStatic;
1058 + }
1059 if (
1060 __DEV__ &&
1061 enableStrictEffects &&
1062 (workInProgress.mode & StrictEffectsMode) !== NoMode
1063 ) {
1064 // Never double-invoke effects for legacy roots.
1048 - workInProgress.flags |= MountLayoutDev | Update;
1049 - } else {
1050 - workInProgress.flags |= Update;
1065 + fiberFlags |= MountLayoutDev;
1066 }
1067 + workInProgress.flags |= fiberFlags;
1068 }
1069 } else {
1070 // If an update was already in progress, we should schedule an Update
1071 // effect even though we're bailing out, so that cWU/cDU are called.
1072 if (typeof instance.componentDidMount === 'function') {
1073 + let fiberFlags: Flags = Update;
1074 + if (enableSuspenseLayoutEffectSemantics) {
1075 + fiberFlags |= LayoutStatic;
1076 + }
1077 if (
1078 __DEV__ &&
1079 enableStrictEffects &&
1080 (workInProgress.mode & StrictEffectsMode) !== NoMode
1081 ) {
1082 // Never double-invoke effects for legacy roots.
1063 - workInProgress.flags |= MountLayoutDev | Update;
1064 - } else {
1065 - workInProgress.flags |= Update;
1083 + fiberFlags |= MountLayoutDev;
1084 }
1085 + workInProgress.flags |= fiberFlags;
1086 }
1087
1088 // If shouldComponentUpdate returned false, we should still update the
packages/react-reconciler/src/ReactFiberCommitWork.new.js
+255 -4
@@ -36,6 +36,7 @@ import {
36 enableScopeAPI,
37 enableStrictEffects,
38 deletedTreeCleanUpLevel,
39 + enableSuspenseLayoutEffectSemantics,
40 } from 'shared/ReactFeatureFlags';
41 import {
42 FunctionComponent,
@@ -79,6 +80,8 @@ import {
80 MutationMask,
81 LayoutMask,
82 PassiveMask,
83 + LayoutStatic,
84 + RefStatic,
85 } from './ReactFiberFlags';
86 import getComponentNameFromFiber from 'react-reconciler/src/getComponentNameFromFiber';
87 import invariant from 'shared/invariant';
@@ -97,7 +100,7 @@ import {
100 recordPassiveEffectDuration,
101 startPassiveEffectTimer,
102 } from './ReactProfilerTimer.new';
100 -import {ProfileMode} from './ReactTypeOfMode';
103 +import {ConcurrentMode, NoMode, ProfileMode} from './ReactTypeOfMode';
104 import {commitUpdateQueue} from './ReactUpdateQueue.new';
105 import {
106 getPublicInstance,
@@ -149,6 +152,14 @@ if (__DEV__) {
152 didWarnAboutUndefinedSnapshotBeforeUpdate = new Set();
153 }
154
155 +// Used during the commit phase to track the state of the Offscreen component stack.
156 +// Allows us to avoid traversing the return path to find the nearest Offscreen ancestor.
157 +// Only used when enableSuspenseLayoutEffectSemantics is enabled.
158 +let offscreenSubtreeIsHidden: boolean = false;
159 +const offscreenSubtreeIsHiddenStack: Array<boolean> = [];
160 +let offscreenSubtreeWasHidden: boolean = false;
161 +const offscreenSubtreeWasHiddenStack: Array<boolean> = [];
162 +
163 const PossiblyWeakSet = typeof WeakSet === 'function' ? WeakSet : Set;
164
165 let nextEffect: Fiber | null = null;
@@ -172,6 +183,32 @@ const callComponentWillUnmountWithTimer = function(current, instance) {
183 }
184 };
185
186 +// Capture errors so they don't interrupt mounting.
187 +function safelyCallCommitHookLayoutEffectListMount(
188 + current: Fiber,
189 + nearestMountedAncestor: Fiber | null,
190 +) {
191 + if (__DEV__) {
192 + invokeGuardedCallback(
193 + null,
194 + commitHookEffectListMount,
195 + null,
196 + HookLayout,
197 + current,
198 + );
199 + if (hasCaughtError()) {
200 + const unmountError = clearCaughtError();
201 + captureCommitPhaseError(current, nearestMountedAncestor, unmountError);
202 + }
203 + } else {
204 + try {
205 + commitHookEffectListMount(HookLayout, current);
206 + } catch (unmountError) {
207 + captureCommitPhaseError(current, nearestMountedAncestor, unmountError);
208 + }
209 + }
210 +}
211 +
212 // Capture errors so they don't interrupt unmounting.
213 function safelyCallComponentWillUnmount(
214 current: Fiber,
@@ -199,6 +236,44 @@ function safelyCallComponentWillUnmount(
236 }
237 }
238
239 +// Capture errors so they don't interrupt mounting.
240 +function safelyCallComponentDidMount(
241 + current: Fiber,
242 + nearestMountedAncestor: Fiber | null,
243 + instance: any,
244 +) {
245 + if (__DEV__) {
246 + invokeGuardedCallback(null, instance.componentDidMount, instance);
247 + if (hasCaughtError()) {
248 + const unmountError = clearCaughtError();
249 + captureCommitPhaseError(current, nearestMountedAncestor, unmountError);
250 + }
251 + } else {
252 + try {
253 + instance.componentDidMount();
254 + } catch (unmountError) {
255 + captureCommitPhaseError(current, nearestMountedAncestor, unmountError);
256 + }
257 + }
258 +}
259 +
260 +// Capture errors so they don't interrupt mounting.
261 +function safelyAttachRef(current: Fiber, nearestMountedAncestor: Fiber | null) {
262 + if (__DEV__) {
263 + invokeGuardedCallback(null, commitAttachRef, null, current);
264 + if (hasCaughtError()) {
265 + const unmountError = clearCaughtError();
266 + captureCommitPhaseError(current, nearestMountedAncestor, unmountError);
267 + }
268 + } else {
269 + try {
270 + commitAttachRef(current);
271 + } catch (unmountError) {
272 + captureCommitPhaseError(current, nearestMountedAncestor, unmountError);
273 + }
274 + }
275 +}
276 +
277 function safelyDetachRef(current: Fiber, nearestMountedAncestor: Fiber | null) {
278 const ref = current.ref;
279 if (ref !== null) {
@@ -942,6 +1017,12 @@ function commitLayoutEffectOnFiber(
1017 }
1018
1019 function hideOrUnhideAllChildren(finishedWork, isHidden) {
1020 + // Suspense layout effects semantics don't change for legacy roots.
1021 + const isModernRoot = (finishedWork.mode & ConcurrentMode) !== NoMode;
1022 +
1023 + const current = finishedWork.alternate;
1024 + const wasHidden = current !== null && current.memoizedState !== null;
1025 +
1026 if (supportsMutation) {
1027 // We only have the top Fiber that was inserted but we need to recurse down its
1028 // children to find all the terminal nodes.
@@ -954,6 +1035,25 @@ function hideOrUnhideAllChildren(finishedWork, isHidden) {
1035 } else {
1036 unhideInstance(node.stateNode, node.memoizedProps);
1037 }
1038 +
1039 + if (enableSuspenseLayoutEffectSemantics && isModernRoot) {
1040 + // This method is called during mutation; it should detach refs within a hidden subtree.
1041 + // Attaching refs should be done elsewhere though (during layout).
1042 + if ((node.flags & RefStatic) !== NoFlags) {
1043 + if (isHidden) {
1044 + safelyDetachRef(node, finishedWork);
1045 + }
1046 + }
1047 +
1048 + if (
1049 + (node.subtreeFlags & (RefStatic | LayoutStatic)) !== NoFlags &&
1050 + node.child !== null
1051 + ) {
1052 + node.child.return = node;
1053 + node = node.child;
1054 + continue;
1055 + }
1056 + }
1057 } else if (node.tag === HostText) {
1058 const instance = node.stateNode;
1059 if (isHidden) {
@@ -967,13 +1067,61 @@ function hideOrUnhideAllChildren(finishedWork, isHidden) {
1067 (node.memoizedState: OffscreenState) !== null &&
1068 node !== finishedWork
1069 ) {
970 - // Found a nested Offscreen component that is hidden. Don't search
971 - // any deeper. This tree should remain hidden.
1070 + // Found a nested Offscreen component that is hidden.
1071 + // Don't search any deeper. This tree should remain hidden.
1072 + } else if (enableSuspenseLayoutEffectSemantics && isModernRoot) {
1073 + // When a mounted Suspense subtree gets hidden again, destroy any nested layout effects.
1074 + if ((node.flags & (RefStatic | LayoutStatic)) !== NoFlags) {
1075 + switch (node.tag) {
1076 + case FunctionComponent:
1077 + case ForwardRef:
1078 + case MemoComponent:
1079 + case SimpleMemoComponent: {
1080 + // Note that refs are attached by the useImperativeHandle() hook, not by commitAttachRef()
1081 + if (isHidden && !wasHidden) {
1082 + if (
1083 + enableProfilerTimer &&
1084 + enableProfilerCommitHooks &&
1085 + node.mode & ProfileMode
1086 + ) {
1087 + try {
1088 + startLayoutEffectTimer();
1089 + commitHookEffectListUnmount(HookLayout, node, finishedWork);
1090 + } finally {
1091 + recordLayoutEffectDuration(node);
1092 + }
1093 + } else {
1094 + commitHookEffectListUnmount(HookLayout, node, finishedWork);
1095 + }
1096 + }
1097 + break;
1098 + }
1099 + case ClassComponent: {
1100 + if (isHidden && !wasHidden) {
1101 + if ((node.flags & RefStatic) !== NoFlags) {
1102 + safelyDetachRef(node, finishedWork);
1103 + }
1104 + const instance = node.stateNode;
1105 + if (typeof instance.componentWillUnmount === 'function') {
1106 + safelyCallComponentWillUnmount(node, finishedWork, instance);
1107 + }
1108 + }
1109 + break;
1110 + }
1111 + }
1112 + }
1113 +
1114 + if (node.child !== null) {
1115 + node.child.return = node;
1116 + node = node.child;
1117 + continue;
1118 + }
1119 } else if (node.child !== null) {
1120 node.child.return = node;
1121 node = node.child;
1122 continue;
1123 }
1124 +
1125 if (node === finishedWork) {
1126 return;
1127 }
@@ -2143,13 +2291,49 @@ function commitLayoutEffects_begin(
2291 root: FiberRoot,
2292 committedLanes: Lanes,
2293 ) {
2294 + // Suspense layout effects semantics don't change for legacy roots.
2295 + const isModernRoot = (subtreeRoot.mode & ConcurrentMode) !== NoMode;
2296 +
2297 while (nextEffect !== null) {
2298 const fiber = nextEffect;
2299 const firstChild = fiber.child;
2300 +
2301 + if (enableSuspenseLayoutEffectSemantics && isModernRoot) {
2302 + // Keep track of the current Offscreen stack's state.
2303 + if (fiber.tag === OffscreenComponent) {
2304 + const current = fiber.alternate;
2305 + const wasHidden = current !== null && current.memoizedState !== null;
2306 + const isHidden = fiber.memoizedState !== null;
2307 +
2308 + offscreenSubtreeWasHidden = wasHidden || offscreenSubtreeWasHidden;
2309 + offscreenSubtreeIsHidden = isHidden || offscreenSubtreeIsHidden;
2310 +
2311 + offscreenSubtreeWasHiddenStack.push(wasHidden);
2312 + offscreenSubtreeIsHiddenStack.push(isHidden);
2313 + }
2314 + }
2315 +
2316 if ((fiber.subtreeFlags & LayoutMask) !== NoFlags && firstChild !== null) {
2317 ensureCorrectReturnPointer(firstChild, fiber);
2318 nextEffect = firstChild;
2319 } else {
2320 + if (enableSuspenseLayoutEffectSemantics && isModernRoot) {
2321 + const visibilityChanged =
2322 + !offscreenSubtreeIsHidden && offscreenSubtreeWasHidden;
2323 + if (
2324 + visibilityChanged &&
2325 + (fiber.subtreeFlags & LayoutStatic) !== NoFlags &&
2326 + firstChild !== null
2327 + ) {
2328 + // We've just shown or hidden a Offscreen tree that contains layout effects.
2329 + // We only enter this code path for subtrees that are updated,
2330 + // because newly mounted ones would pass the LayoutMask check above.
2331 + ensureCorrectReturnPointer(firstChild, fiber);
2332 + nextEffect = firstChild;
2333 + continue;
2334 + }
2335 + }
2336 +
2337 commitLayoutMountEffects_complete(subtreeRoot, root, committedLanes);
2338 }
2339 }
@@ -2160,9 +2344,76 @@ function commitLayoutMountEffects_complete(
2344 root: FiberRoot,
2345 committedLanes: Lanes,
2346 ) {
2347 + // Suspense layout effects semantics don't change for legacy roots.
2348 + const isModernRoot = (subtreeRoot.mode & ConcurrentMode) !== NoMode;
2349 +
2350 while (nextEffect !== null) {
2351 const fiber = nextEffect;
2165 - if ((fiber.flags & LayoutMask) !== NoFlags) {
2352 +
2353 + if (enableSuspenseLayoutEffectSemantics && isModernRoot) {
2354 + if (fiber.tag === OffscreenComponent) {
2355 + offscreenSubtreeWasHiddenStack.pop();
2356 + offscreenSubtreeIsHiddenStack.pop();
2357 + offscreenSubtreeWasHidden =
2358 + offscreenSubtreeWasHiddenStack.length > 0 &&
2359 + offscreenSubtreeWasHiddenStack[
2360 + offscreenSubtreeWasHiddenStack.length - 1
2361 + ];
2362 + offscreenSubtreeIsHidden =
2363 + offscreenSubtreeIsHiddenStack.length > 0 &&
2364 + offscreenSubtreeIsHiddenStack[
2365 + offscreenSubtreeIsHiddenStack.length - 1
2366 + ];
2367 + }
2368 + }
2369 +
2370 + if (
2371 + enableSuspenseLayoutEffectSemantics &&
2372 + isModernRoot &&
2373 + offscreenSubtreeWasHidden &&
2374 + !offscreenSubtreeIsHidden
2375 + ) {
2376 + // Inside of an Offscreen subtree that changed visibility during this commit.
2377 + // If this subtree was hidden, layout effects will have already been destroyed (during mutation phase)
2378 + // but if it was just shown, we need to (re)create the effects now.
2379 + if ((fiber.flags & LayoutStatic) !== NoFlags) {
2380 + switch (fiber.tag) {
2381 + case FunctionComponent:
2382 + case ForwardRef:
2383 + case SimpleMemoComponent: {
2384 + if (
2385 + enableProfilerTimer &&
2386 + enableProfilerCommitHooks &&
2387 + fiber.mode & ProfileMode
2388 + ) {
2389 + try {
2390 + startLayoutEffectTimer();
2391 + safelyCallCommitHookLayoutEffectListMount(fiber, fiber.return);
2392 + } finally {
2393 + recordLayoutEffectDuration(fiber);
2394 + }
2395 + } else {
2396 + safelyCallCommitHookLayoutEffectListMount(fiber, fiber.return);
2397 + }
2398 + break;
2399 + }
2400 + case ClassComponent: {
2401 + const instance = fiber.stateNode;
2402 + safelyCallComponentDidMount(fiber, fiber.return, instance);
2403 + break;
2404 + }
2405 + }
2406 + }
2407 +
2408 + if ((fiber.flags & RefStatic) !== NoFlags) {
2409 + switch (fiber.tag) {
2410 + case ClassComponent:
2411 + case HostComponent:
2412 + safelyAttachRef(fiber, fiber.return);
2413 + break;
2414 + }
2415 + }
2416 + } else if ((fiber.flags & LayoutMask) !== NoFlags) {
2417 const current = fiber.alternate;
2418 if (__DEV__) {
2419 setCurrentDebugFiberInDEV(fiber);
packages/react-reconciler/src/ReactFiberCommitWork.old.js
+255 -4
@@ -36,6 +36,7 @@ import {
36 enableScopeAPI,
37 enableStrictEffects,
38 deletedTreeCleanUpLevel,
39 + enableSuspenseLayoutEffectSemantics,
40 } from 'shared/ReactFeatureFlags';
41 import {
42 FunctionComponent,
@@ -79,6 +80,8 @@ import {
80 MutationMask,
81 LayoutMask,
82 PassiveMask,
83 + LayoutStatic,
84 + RefStatic,
85 } from './ReactFiberFlags';
86 import getComponentNameFromFiber from 'react-reconciler/src/getComponentNameFromFiber';
87 import invariant from 'shared/invariant';
@@ -97,7 +100,7 @@ import {
100 recordPassiveEffectDuration,
101 startPassiveEffectTimer,
102 } from './ReactProfilerTimer.old';
100 -import {ProfileMode} from './ReactTypeOfMode';
103 +import {ConcurrentMode, NoMode, ProfileMode} from './ReactTypeOfMode';
104 import {commitUpdateQueue} from './ReactUpdateQueue.old';
105 import {
106 getPublicInstance,
@@ -149,6 +152,14 @@ if (__DEV__) {
152 didWarnAboutUndefinedSnapshotBeforeUpdate = new Set();
153 }
154
155 +// Used during the commit phase to track the state of the Offscreen component stack.
156 +// Allows us to avoid traversing the return path to find the nearest Offscreen ancestor.
157 +// Only used when enableSuspenseLayoutEffectSemantics is enabled.
158 +let offscreenSubtreeIsHidden: boolean = false;
159 +const offscreenSubtreeIsHiddenStack: Array<boolean> = [];
160 +let offscreenSubtreeWasHidden: boolean = false;
161 +const offscreenSubtreeWasHiddenStack: Array<boolean> = [];
162 +
163 const PossiblyWeakSet = typeof WeakSet === 'function' ? WeakSet : Set;
164
165 let nextEffect: Fiber | null = null;
@@ -172,6 +183,32 @@ const callComponentWillUnmountWithTimer = function(current, instance) {
183 }
184 };
185
186 +// Capture errors so they don't interrupt mounting.
187 +function safelyCallCommitHookLayoutEffectListMount(
188 + current: Fiber,
189 + nearestMountedAncestor: Fiber | null,
190 +) {
191 + if (__DEV__) {
192 + invokeGuardedCallback(
193 + null,
194 + commitHookEffectListMount,
195 + null,
196 + HookLayout,
197 + current,
198 + );
199 + if (hasCaughtError()) {
200 + const unmountError = clearCaughtError();
201 + captureCommitPhaseError(current, nearestMountedAncestor, unmountError);
202 + }
203 + } else {
204 + try {
205 + commitHookEffectListMount(HookLayout, current);
206 + } catch (unmountError) {
207 + captureCommitPhaseError(current, nearestMountedAncestor, unmountError);
208 + }
209 + }
210 +}
211 +
212 // Capture errors so they don't interrupt unmounting.
213 function safelyCallComponentWillUnmount(
214 current: Fiber,
@@ -199,6 +236,44 @@ function safelyCallComponentWillUnmount(
236 }
237 }
238
239 +// Capture errors so they don't interrupt mounting.
240 +function safelyCallComponentDidMount(
241 + current: Fiber,
242 + nearestMountedAncestor: Fiber | null,
243 + instance: any,
244 +) {
245 + if (__DEV__) {
246 + invokeGuardedCallback(null, instance.componentDidMount, instance);
247 + if (hasCaughtError()) {
248 + const unmountError = clearCaughtError();
249 + captureCommitPhaseError(current, nearestMountedAncestor, unmountError);
250 + }
251 + } else {
252 + try {
253 + instance.componentDidMount();
254 + } catch (unmountError) {
255 + captureCommitPhaseError(current, nearestMountedAncestor, unmountError);
256 + }
257 + }
258 +}
259 +
260 +// Capture errors so they don't interrupt mounting.
261 +function safelyAttachRef(current: Fiber, nearestMountedAncestor: Fiber | null) {
262 + if (__DEV__) {
263 + invokeGuardedCallback(null, commitAttachRef, null, current);
264 + if (hasCaughtError()) {
265 + const unmountError = clearCaughtError();
266 + captureCommitPhaseError(current, nearestMountedAncestor, unmountError);
267 + }
268 + } else {
269 + try {
270 + commitAttachRef(current);
271 + } catch (unmountError) {
272 + captureCommitPhaseError(current, nearestMountedAncestor, unmountError);
273 + }
274 + }
275 +}
276 +
277 function safelyDetachRef(current: Fiber, nearestMountedAncestor: Fiber | null) {
278 const ref = current.ref;
279 if (ref !== null) {
@@ -942,6 +1017,12 @@ function commitLayoutEffectOnFiber(
1017 }
1018
1019 function hideOrUnhideAllChildren(finishedWork, isHidden) {
1020 + // Suspense layout effects semantics don't change for legacy roots.
1021 + const isModernRoot = (finishedWork.mode & ConcurrentMode) !== NoMode;
1022 +
1023 + const current = finishedWork.alternate;
1024 + const wasHidden = current !== null && current.memoizedState !== null;
1025 +
1026 if (supportsMutation) {
1027 // We only have the top Fiber that was inserted but we need to recurse down its
1028 // children to find all the terminal nodes.
@@ -954,6 +1035,25 @@ function hideOrUnhideAllChildren(finishedWork, isHidden) {
1035 } else {
1036 unhideInstance(node.stateNode, node.memoizedProps);
1037 }
1038 +
1039 + if (enableSuspenseLayoutEffectSemantics && isModernRoot) {
1040 + // This method is called during mutation; it should detach refs within a hidden subtree.
1041 + // Attaching refs should be done elsewhere though (during layout).
1042 + if ((node.flags & RefStatic) !== NoFlags) {
1043 + if (isHidden) {
1044 + safelyDetachRef(node, finishedWork);
1045 + }
1046 + }
1047 +
1048 + if (
1049 + (node.subtreeFlags & (RefStatic | LayoutStatic)) !== NoFlags &&
1050 + node.child !== null
1051 + ) {
1052 + node.child.return = node;
1053 + node = node.child;
1054 + continue;
1055 + }
1056 + }
1057 } else if (node.tag === HostText) {
1058 const instance = node.stateNode;
1059 if (isHidden) {
@@ -967,13 +1067,61 @@ function hideOrUnhideAllChildren(finishedWork, isHidden) {
1067 (node.memoizedState: OffscreenState) !== null &&
1068 node !== finishedWork
1069 ) {
970 - // Found a nested Offscreen component that is hidden. Don't search
971 - // any deeper. This tree should remain hidden.
1070 + // Found a nested Offscreen component that is hidden.
1071 + // Don't search any deeper. This tree should remain hidden.
1072 + } else if (enableSuspenseLayoutEffectSemantics && isModernRoot) {
1073 + // When a mounted Suspense subtree gets hidden again, destroy any nested layout effects.
1074 + if ((node.flags & (RefStatic | LayoutStatic)) !== NoFlags) {
1075 + switch (node.tag) {
1076 + case FunctionComponent:
1077 + case ForwardRef:
1078 + case MemoComponent:
1079 + case SimpleMemoComponent: {
1080 + // Note that refs are attached by the useImperativeHandle() hook, not by commitAttachRef()
1081 + if (isHidden && !wasHidden) {
1082 + if (
1083 + enableProfilerTimer &&
1084 + enableProfilerCommitHooks &&
1085 + node.mode & ProfileMode
1086 + ) {
1087 + try {
1088 + startLayoutEffectTimer();
1089 + commitHookEffectListUnmount(HookLayout, node, finishedWork);
1090 + } finally {
1091 + recordLayoutEffectDuration(node);
1092 + }
1093 + } else {
1094 + commitHookEffectListUnmount(HookLayout, node, finishedWork);
1095 + }
1096 + }
1097 + break;
1098 + }
1099 + case ClassComponent: {
1100 + if (isHidden && !wasHidden) {
1101 + if ((node.flags & RefStatic) !== NoFlags) {
1102 + safelyDetachRef(node, finishedWork);
1103 + }
1104 + const instance = node.stateNode;
1105 + if (typeof instance.componentWillUnmount === 'function') {
1106 + safelyCallComponentWillUnmount(node, finishedWork, instance);
1107 + }
1108 + }
1109 + break;
1110 + }
1111 + }
1112 + }
1113 +
1114 + if (node.child !== null) {
1115 + node.child.return = node;
1116 + node = node.child;
1117 + continue;
1118 + }
1119 } else if (node.child !== null) {
1120 node.child.return = node;
1121 node = node.child;
1122 continue;
1123 }
1124 +
1125 if (node === finishedWork) {
1126 return;
1127 }
@@ -2143,13 +2291,49 @@ function commitLayoutEffects_begin(
2291 root: FiberRoot,
2292 committedLanes: Lanes,
2293 ) {
2294 + // Suspense layout effects semantics don't change for legacy roots.
2295 + const isModernRoot = (subtreeRoot.mode & ConcurrentMode) !== NoMode;
2296 +
2297 while (nextEffect !== null) {
2298 const fiber = nextEffect;
2299 const firstChild = fiber.child;
2300 +
2301 + if (enableSuspenseLayoutEffectSemantics && isModernRoot) {
2302 + // Keep track of the current Offscreen stack's state.
2303 + if (fiber.tag === OffscreenComponent) {
2304 + const current = fiber.alternate;
2305 + const wasHidden = current !== null && current.memoizedState !== null;
2306 + const isHidden = fiber.memoizedState !== null;
2307 +
2308 + offscreenSubtreeWasHidden = wasHidden || offscreenSubtreeWasHidden;
2309 + offscreenSubtreeIsHidden = isHidden || offscreenSubtreeIsHidden;
2310 +
2311 + offscreenSubtreeWasHiddenStack.push(wasHidden);
2312 + offscreenSubtreeIsHiddenStack.push(isHidden);
2313 + }
2314 + }
2315 +
2316 if ((fiber.subtreeFlags & LayoutMask) !== NoFlags && firstChild !== null) {
2317 ensureCorrectReturnPointer(firstChild, fiber);
2318 nextEffect = firstChild;
2319 } else {
2320 + if (enableSuspenseLayoutEffectSemantics && isModernRoot) {
2321 + const visibilityChanged =
2322 + !offscreenSubtreeIsHidden && offscreenSubtreeWasHidden;
2323 + if (
2324 + visibilityChanged &&
2325 + (fiber.subtreeFlags & LayoutStatic) !== NoFlags &&
2326 + firstChild !== null
2327 + ) {
2328 + // We've just shown or hidden a Offscreen tree that contains layout effects.
2329 + // We only enter this code path for subtrees that are updated,
2330 + // because newly mounted ones would pass the LayoutMask check above.
2331 + ensureCorrectReturnPointer(firstChild, fiber);
2332 + nextEffect = firstChild;
2333 + continue;
2334 + }
2335 + }
2336 +
2337 commitLayoutMountEffects_complete(subtreeRoot, root, committedLanes);
2338 }
2339 }
@@ -2160,9 +2344,76 @@ function commitLayoutMountEffects_complete(
2344 root: FiberRoot,
2345 committedLanes: Lanes,
2346 ) {
2347 + // Suspense layout effects semantics don't change for legacy roots.
2348 + const isModernRoot = (subtreeRoot.mode & ConcurrentMode) !== NoMode;
2349 +
2350 while (nextEffect !== null) {
2351 const fiber = nextEffect;
2165 - if ((fiber.flags & LayoutMask) !== NoFlags) {
2352 +
2353 + if (enableSuspenseLayoutEffectSemantics && isModernRoot) {
2354 + if (fiber.tag === OffscreenComponent) {
2355 + offscreenSubtreeWasHiddenStack.pop();
2356 + offscreenSubtreeIsHiddenStack.pop();
2357 + offscreenSubtreeWasHidden =
2358 + offscreenSubtreeWasHiddenStack.length > 0 &&
2359 + offscreenSubtreeWasHiddenStack[
2360 + offscreenSubtreeWasHiddenStack.length - 1
2361 + ];
2362 + offscreenSubtreeIsHidden =
2363 + offscreenSubtreeIsHiddenStack.length > 0 &&
2364 + offscreenSubtreeIsHiddenStack[
2365 + offscreenSubtreeIsHiddenStack.length - 1
2366 + ];
2367 + }
2368 + }
2369 +
2370 + if (
2371 + enableSuspenseLayoutEffectSemantics &&
2372 + isModernRoot &&
2373 + offscreenSubtreeWasHidden &&
2374 + !offscreenSubtreeIsHidden
2375 + ) {
2376 + // Inside of an Offscreen subtree that changed visibility during this commit.
2377 + // If this subtree was hidden, layout effects will have already been destroyed (during mutation phase)
2378 + // but if it was just shown, we need to (re)create the effects now.
2379 + if ((fiber.flags & LayoutStatic) !== NoFlags) {
2380 + switch (fiber.tag) {
2381 + case FunctionComponent:
2382 + case ForwardRef:
2383 + case SimpleMemoComponent: {
2384 + if (
2385 + enableProfilerTimer &&
2386 + enableProfilerCommitHooks &&
2387 + fiber.mode & ProfileMode
2388 + ) {
2389 + try {
2390 + startLayoutEffectTimer();
2391 + safelyCallCommitHookLayoutEffectListMount(fiber, fiber.return);
2392 + } finally {
2393 + recordLayoutEffectDuration(fiber);
2394 + }
2395 + } else {
2396 + safelyCallCommitHookLayoutEffectListMount(fiber, fiber.return);
2397 + }
2398 + break;
2399 + }
2400 + case ClassComponent: {
2401 + const instance = fiber.stateNode;
2402 + safelyCallComponentDidMount(fiber, fiber.return, instance);
2403 + break;
2404 + }
2405 + }
2406 + }
2407 +
2408 + if ((fiber.flags & RefStatic) !== NoFlags) {
2409 + switch (fiber.tag) {
2410 + case ClassComponent:
2411 + case HostComponent:
2412 + safelyAttachRef(fiber, fiber.return);
2413 + break;
2414 + }
2415 + }
2416 + } else if ((fiber.flags & LayoutMask) !== NoFlags) {
2417 const current = fiber.alternate;
2418 if (__DEV__) {
2419 setCurrentDebugFiberInDEV(fiber);
packages/react-reconciler/src/ReactFiberCompleteWork.new.js
+5
@@ -58,6 +58,7 @@ import {
58 import {NoMode, ConcurrentMode, ProfileMode} from './ReactTypeOfMode';
59 import {
60 Ref,
61 + RefStatic,
62 Update,
63 NoFlags,
64 DidCapture,
@@ -123,6 +124,7 @@ import {
124 enableScopeAPI,
125 enableProfilerTimer,
126 enableCache,
127 + enableSuspenseLayoutEffectSemantics,
128 } from 'shared/ReactFeatureFlags';
129 import {
130 markSpawnedWork,
@@ -157,6 +159,9 @@ function markUpdate(workInProgress: Fiber) {
159
160 function markRef(workInProgress: Fiber) {
161 workInProgress.flags |= Ref;
162 + if (enableSuspenseLayoutEffectSemantics) {
163 + workInProgress.flags |= RefStatic;
164 + }
165 }
166
167 function hadNoMutationsEffects(current: null | Fiber, completedWork: Fiber) {
packages/react-reconciler/src/ReactFiberCompleteWork.old.js
+5
@@ -58,6 +58,7 @@ import {
58 import {NoMode, ConcurrentMode, ProfileMode} from './ReactTypeOfMode';
59 import {
60 Ref,
61 + RefStatic,
62 Update,
63 NoFlags,
64 DidCapture,
@@ -123,6 +124,7 @@ import {
124 enableScopeAPI,
125 enableProfilerTimer,
126 enableCache,
127 + enableSuspenseLayoutEffectSemantics,
128 } from 'shared/ReactFeatureFlags';
129 import {
130 markSpawnedWork,
@@ -157,6 +159,9 @@ function markUpdate(workInProgress: Fiber) {
159
160 function markRef(workInProgress: Fiber) {
161 workInProgress.flags |= Ref;
162 + if (enableSuspenseLayoutEffectSemantics) {
163 + workInProgress.flags |= RefStatic;
164 + }
165 }
166
167 function hadNoMutationsEffects(current: null | Fiber, completedWork: Fiber) {
packages/react-reconciler/src/ReactFiberFlags.js
+26 -24
@@ -12,49 +12,51 @@ import {enableCreateEventHandleAPI} from 'shared/ReactFeatureFlags';
12 export type Flags = number;
13
14 // Don't change these two values. They're used by React Dev Tools.
15 -export const NoFlags = /* */ 0b000000000000000000000;
16 -export const PerformedWork = /* */ 0b000000000000000000001;
15 +export const NoFlags = /* */ 0b00000000000000000000000;
16 +export const PerformedWork = /* */ 0b00000000000000000000001;
17
18 // You can change the rest (and add more).
19 -export const Placement = /* */ 0b000000000000000000010;
20 -export const Update = /* */ 0b000000000000000000100;
19 +export const Placement = /* */ 0b00000000000000000000010;
20 +export const Update = /* */ 0b00000000000000000000100;
21 export const PlacementAndUpdate = /* */ Placement | Update;
22 -export const Deletion = /* */ 0b000000000000000001000;
23 -export const ChildDeletion = /* */ 0b000000000000000010000;
24 -export const ContentReset = /* */ 0b000000000000000100000;
25 -export const Callback = /* */ 0b000000000000001000000;
26 -export const DidCapture = /* */ 0b000000000000010000000;
27 -export const Ref = /* */ 0b000000000000100000000;
28 -export const Snapshot = /* */ 0b000000000001000000000;
29 -export const Passive = /* */ 0b000000000010000000000;
30 -export const Hydrating = /* */ 0b000000000100000000000;
22 +export const Deletion = /* */ 0b00000000000000000001000;
23 +export const ChildDeletion = /* */ 0b00000000000000000010000;
24 +export const ContentReset = /* */ 0b00000000000000000100000;
25 +export const Callback = /* */ 0b00000000000000001000000;
26 +export const DidCapture = /* */ 0b00000000000000010000000;
27 +export const Ref = /* */ 0b00000000000000100000000;
28 +export const Snapshot = /* */ 0b00000000000001000000000;
29 +export const Passive = /* */ 0b00000000000010000000000;
30 +export const Hydrating = /* */ 0b00000000000100000000000;
31 export const HydratingAndUpdate = /* */ Hydrating | Update;
32 -export const Visibility = /* */ 0b000000001000000000000;
32 +export const Visibility = /* */ 0b00000000001000000000000;
33
34 export const LifecycleEffectMask = Passive | Update | Callback | Ref | Snapshot;
35
36 // Union of all commit flags (flags with the lifetime of a particular commit)
37 -export const HostEffectMask = /* */ 0b000000001111111111111;
37 +export const HostEffectMask = /* */ 0b00000000001111111111111;
38
39 // These are not really side effects, but we still reuse this field.
40 -export const Incomplete = /* */ 0b000000010000000000000;
41 -export const ShouldCapture = /* */ 0b000000100000000000000;
42 -export const ForceUpdateForLegacySuspense = /* */ 0b000001000000000000000;
43 -export const DidPropagateContext = /* */ 0b000010000000000000000;
44 -export const NeedsPropagation = /* */ 0b000100000000000000000;
40 +export const Incomplete = /* */ 0b00000000010000000000000;
41 +export const ShouldCapture = /* */ 0b00000000100000000000000;
42 +export const ForceUpdateForLegacySuspense = /* */ 0b00000001000000000000000;
43 +export const DidPropagateContext = /* */ 0b00000010000000000000000;
44 +export const NeedsPropagation = /* */ 0b00000100000000000000000;
45
46 // Static tags describe aspects of a fiber that are not specific to a render,
47 // e.g. a fiber uses a passive effect (even if there are no updates on this particular render).
48 // This enables us to defer more work in the unmount case,
49 // since we can defer traversing the tree during layout to look for Passive effects,
50 // and instead rely on the static flag as a signal that there may be cleanup work.
51 -export const PassiveStatic = /* */ 0b001000000000000000000;
51 +export const RefStatic = /* */ 0b00001000000000000000000;
52 +export const LayoutStatic = /* */ 0b00010000000000000000000;
53 +export const PassiveStatic = /* */ 0b00100000000000000000000;
54
55 // These flags allow us to traverse to fibers that have effects on mount
56 // without traversing the entire tree after every commit for
57 // double invoking
56 -export const MountLayoutDev = /* */ 0b010000000000000000000;
57 -export const MountPassiveDev = /* */ 0b100000000000000000000;
58 +export const MountLayoutDev = /* */ 0b01000000000000000000000;
59 +export const MountPassiveDev = /* */ 0b10000000000000000000000;
60
61 // Groups of flags that are used in the commit phase to skip over trees that
62 // don't contain effects, by checking subtreeFlags.
@@ -88,4 +90,4 @@ export const PassiveMask = Passive | ChildDeletion;
90 // Union of tags that don't get reset on clones.
91 // This allows certain concepts to persist without recalculting them,
92 // e.g. whether a subtree contains passive effects or portals.
91 -export const StaticMask = PassiveStatic;
93 +export const StaticMask = LayoutStatic | PassiveStatic | RefStatic;
packages/react-reconciler/src/ReactFiberHooks.new.js
+26 -26
@@ -19,6 +19,7 @@ import type {HookFlags} from './ReactHookEffectTags';
19 import type {FiberRoot} from './ReactInternalTypes';
20 import type {OpaqueIDType} from './ReactFiberHostConfig';
21 import type {Cache} from './ReactFiberCacheComponent.new';
22 +import type {Flags} from './ReactFiberFlags';
23
24 import ReactSharedInternals from 'shared/ReactSharedInternals';
25 import {
@@ -29,6 +30,7 @@ import {
30 enableUseRefAccessWarning,
31 enableStrictEffects,
32 enableLazyContextPropagation,
33 + enableSuspenseLayoutEffectSemantics,
34 } from 'shared/ReactFeatureFlags';
35
36 import {
@@ -57,11 +59,13 @@ import {
59 import {readContext, checkIfContextChanged} from './ReactFiberNewContext.new';
60 import {HostRoot, CacheComponent} from './ReactWorkTags';
61 import {
60 - Update as UpdateEffect,
61 - Passive as PassiveEffect,
62 - PassiveStatic as PassiveStaticEffect,
62 + LayoutStatic as LayoutStaticEffect,
63 MountLayoutDev as MountLayoutDevEffect,
64 MountPassiveDev as MountPassiveDevEffect,
65 + Passive as PassiveEffect,
66 + PassiveStatic as PassiveStaticEffect,
67 + StaticMask as StaticMaskEffect,
68 + Update as UpdateEffect,
69 } from './ReactFiberFlags';
70 import {
71 HasEffect as HookHasEffect,
@@ -474,8 +478,8 @@ export function renderWithHooks<Props, SecondArg>(
478 // example, in the SuspenseList implementation.
479 if (
480 current !== null &&
477 - (current.flags & PassiveStaticEffect) !==
478 - (workInProgress.flags & PassiveStaticEffect)
481 + (current.flags & StaticMaskEffect) !==
482 + (workInProgress.flags & StaticMaskEffect)
483 ) {
484 console.error(
485 'Internal React error: Expected static flag was missing. Please ' +
@@ -1478,20 +1482,18 @@ function mountLayoutEffect(
1482 create: () => (() => void) | void,
1483 deps: Array<mixed> | void | null,
1484 ): void {
1485 + let fiberFlags: Flags = UpdateEffect;
1486 + if (enableSuspenseLayoutEffectSemantics) {
1487 + fiberFlags |= LayoutStaticEffect;
1488 + }
1489 if (
1490 __DEV__ &&
1491 enableStrictEffects &&
1492 (currentlyRenderingFiber.mode & StrictEffectsMode) !== NoMode
1493 ) {
1486 - return mountEffectImpl(
1487 - MountLayoutDevEffect | UpdateEffect,
1488 - HookLayout,
1489 - create,
1490 - deps,
1491 - );
1492 - } else {
1493 - return mountEffectImpl(UpdateEffect, HookLayout, create, deps);
1494 + fiberFlags |= MountLayoutDevEffect;
1495 }
1496 + return mountEffectImpl(fiberFlags, HookLayout, create, deps);
1497 }
1498
1499 function updateLayoutEffect(
@@ -1550,25 +1552,23 @@ function mountImperativeHandle<T>(
1552 const effectDeps =
1553 deps !== null && deps !== undefined ? deps.concat([ref]) : null;
1554
1555 + let fiberFlags: Flags = UpdateEffect;
1556 + if (enableSuspenseLayoutEffectSemantics) {
1557 + fiberFlags |= LayoutStaticEffect;
1558 + }
1559 if (
1560 __DEV__ &&
1561 enableStrictEffects &&
1562 (currentlyRenderingFiber.mode & StrictEffectsMode) !== NoMode
1563 ) {
1558 - return mountEffectImpl(
1559 - MountLayoutDevEffect | UpdateEffect,
1560 - HookLayout,
1561 - imperativeHandleEffect.bind(null, create, ref),
1562 - effectDeps,
1563 - );
1564 - } else {
1565 - return mountEffectImpl(
1566 - UpdateEffect,
1567 - HookLayout,
1568 - imperativeHandleEffect.bind(null, create, ref),
1569 - effectDeps,
1570 - );
1564 + fiberFlags |= MountLayoutDevEffect;
1565 }
1566 + return mountEffectImpl(
1567 + fiberFlags,
1568 + HookLayout,
1569 + imperativeHandleEffect.bind(null, create, ref),
1570 + effectDeps,
1571 + );
1572 }
1573
1574 function updateImperativeHandle<T>(
packages/react-reconciler/src/ReactFiberHooks.old.js
+26 -26
@@ -19,6 +19,7 @@ import type {HookFlags} from './ReactHookEffectTags';
19 import type {FiberRoot} from './ReactInternalTypes';
20 import type {OpaqueIDType} from './ReactFiberHostConfig';
21 import type {Cache} from './ReactFiberCacheComponent.old';
22 +import type {Flags} from './ReactFiberFlags';
23
24 import ReactSharedInternals from 'shared/ReactSharedInternals';
25 import {
@@ -29,6 +30,7 @@ import {
30 enableUseRefAccessWarning,
31 enableStrictEffects,
32 enableLazyContextPropagation,
33 + enableSuspenseLayoutEffectSemantics,
34 } from 'shared/ReactFeatureFlags';
35
36 import {
@@ -57,11 +59,13 @@ import {
59 import {readContext, checkIfContextChanged} from './ReactFiberNewContext.old';
60 import {HostRoot, CacheComponent} from './ReactWorkTags';
61 import {
60 - Update as UpdateEffect,
61 - Passive as PassiveEffect,
62 - PassiveStatic as PassiveStaticEffect,
62 + LayoutStatic as LayoutStaticEffect,
63 MountLayoutDev as MountLayoutDevEffect,
64 MountPassiveDev as MountPassiveDevEffect,
65 + Passive as PassiveEffect,
66 + PassiveStatic as PassiveStaticEffect,
67 + StaticMask as StaticMaskEffect,
68 + Update as UpdateEffect,
69 } from './ReactFiberFlags';
70 import {
71 HasEffect as HookHasEffect,
@@ -474,8 +478,8 @@ export function renderWithHooks<Props, SecondArg>(
478 // example, in the SuspenseList implementation.
479 if (
480 current !== null &&
477 - (current.flags & PassiveStaticEffect) !==
478 - (workInProgress.flags & PassiveStaticEffect)
481 + (current.flags & StaticMaskEffect) !==
482 + (workInProgress.flags & StaticMaskEffect)
483 ) {
484 console.error(
485 'Internal React error: Expected static flag was missing. Please ' +
@@ -1478,20 +1482,18 @@ function mountLayoutEffect(
1482 create: () => (() => void) | void,
1483 deps: Array<mixed> | void | null,
1484 ): void {
1485 + let fiberFlags: Flags = UpdateEffect;
1486 + if (enableSuspenseLayoutEffectSemantics) {
1487 + fiberFlags |= LayoutStaticEffect;
1488 + }
1489 if (
1490 __DEV__ &&
1491 enableStrictEffects &&
1492 (currentlyRenderingFiber.mode & StrictEffectsMode) !== NoMode
1493 ) {
1486 - return mountEffectImpl(
1487 - MountLayoutDevEffect | UpdateEffect,
1488 - HookLayout,
1489 - create,
1490 - deps,
1491 - );
1492 - } else {
1493 - return mountEffectImpl(UpdateEffect, HookLayout, create, deps);
1494 + fiberFlags |= MountLayoutDevEffect;
1495 }
1496 + return mountEffectImpl(fiberFlags, HookLayout, create, deps);
1497 }
1498
1499 function updateLayoutEffect(
@@ -1550,25 +1552,23 @@ function mountImperativeHandle<T>(
1552 const effectDeps =
1553 deps !== null && deps !== undefined ? deps.concat([ref]) : null;
1554
1555 + let fiberFlags: Flags = UpdateEffect;
1556 + if (enableSuspenseLayoutEffectSemantics) {
1557 + fiberFlags |= LayoutStaticEffect;
1558 + }
1559 if (
1560 __DEV__ &&
1561 enableStrictEffects &&
1562 (currentlyRenderingFiber.mode & StrictEffectsMode) !== NoMode
1563 ) {
1558 - return mountEffectImpl(
1559 - MountLayoutDevEffect | UpdateEffect,
1560 - HookLayout,
1561 - imperativeHandleEffect.bind(null, create, ref),
1562 - effectDeps,
1563 - );
1564 - } else {
1565 - return mountEffectImpl(
1566 - UpdateEffect,
1567 - HookLayout,
1568 - imperativeHandleEffect.bind(null, create, ref),
1569 - effectDeps,
1570 - );
1564 + fiberFlags |= MountLayoutDevEffect;
1565 }
1566 + return mountEffectImpl(
1567 + fiberFlags,
1568 + HookLayout,
1569 + imperativeHandleEffect.bind(null, create, ref),
1570 + effectDeps,
1571 + );
1572 }
1573
1574 function updateImperativeHandle<T>(
packages/react-reconciler/src/__tests__/ReactLazy-test.internal.js
+26 -6
@@ -1271,6 +1271,9 @@ describe('ReactLazy', () => {
1271 // @gate enableLazyElements
1272 it('mount and reorder lazy types', async () => {
1273 class Child extends React.Component {
1274 + componentWillUnmount() {
1275 + Scheduler.unstable_yieldValue('Did unmount: ' + this.props.label);
1276 + }
1277 componentDidMount() {
1278 Scheduler.unstable_yieldValue('Did mount: ' + this.props.label);
1279 }
@@ -1348,6 +1351,12 @@ describe('ReactLazy', () => {
1351 expect(Scheduler).toFlushAndYield(['Init B2', 'Loading...']);
1352 jest.runAllTimers();
1353
1354 + gate(flags => {
1355 + if (flags.enableSuspenseLayoutEffectSemantics) {
1356 + expect(Scheduler).toHaveYielded(['Did unmount: A', 'Did unmount: B']);
1357 + }
1358 + });
1359 +
1360 // The suspense boundary should've triggered now.
1361 expect(root).toMatchRenderedOutput('Loading...');
1362 await resolveB2({default: ChildB});
@@ -1356,12 +1365,23 @@ describe('ReactLazy', () => {
1365 expect(Scheduler).toFlushAndYield(['Init A2']);
1366 await LazyChildA2;
1367
1359 - expect(Scheduler).toFlushAndYield([
1360 - 'b',
1361 - 'a',
1362 - 'Did update: b',
1363 - 'Did update: a',
1364 - ]);
1368 + gate(flags => {
1369 + if (flags.enableSuspenseLayoutEffectSemantics) {
1370 + expect(Scheduler).toFlushAndYield([
1371 + 'b',
1372 + 'a',
1373 + 'Did mount: b',
1374 + 'Did mount: a',
1375 + ]);
1376 + } else {
1377 + expect(Scheduler).toFlushAndYield([
1378 + 'b',
1379 + 'a',
1380 + 'Did update: b',
1381 + 'Did update: a',
1382 + ]);
1383 + }
1384 + });
1385 expect(root).toMatchRenderedOutput('ba');
1386 });
1387
packages/react-reconciler/src/__tests__/ReactSuspenseEffectsSemantics-test.js new
+3097
@@ -0,0 +1,3097 @@
1 +/**
2 + * Copyright (c) Facebook, Inc. and its affiliates.
3 + *
4 + * This source code is licensed under the MIT license found in the
5 + * LICENSE file in the root directory of this source tree.
6 + *
7 + * @flow
8 + */
9 +
10 +let React;
11 +let ReactNoop;
12 +let Scheduler;
13 +let Suspense;
14 +let getCacheForType;
15 +let caches;
16 +let seededCache;
17 +let ErrorBoundary;
18 +
19 +describe('ReactSuspenseEffectsSemantics', () => {
20 + beforeEach(() => {
21 + jest.resetModules();
22 +
23 + React = require('react');
24 + ReactNoop = require('react-noop-renderer');
25 + Scheduler = require('scheduler');
26 + Suspense = React.Suspense;
27 +
28 + getCacheForType = React.unstable_getCacheForType;
29 +
30 + caches = [];
31 + seededCache = null;
32 +
33 + ErrorBoundary = class extends React.Component {
34 + state = {error: null};
35 + componentDidCatch(error) {
36 + this.setState({error});
37 + }
38 + render() {
39 + if (this.state.error) {
40 + Scheduler.unstable_yieldValue('ErrorBoundary render: catch');
41 + return this.props.fallback;
42 + }
43 + Scheduler.unstable_yieldValue('ErrorBoundary render: try');
44 + return this.props.children;
45 + }
46 + };
47 + });
48 +
49 + function createTextCache() {
50 + if (seededCache !== null) {
51 + // Trick to seed a cache before it exists.
52 + // TODO: Need a built-in API to seed data before the initial render (i.e.
53 + // not a refresh because nothing has mounted yet).
54 + const cache = seededCache;
55 + seededCache = null;
56 + return cache;
57 + }
58 +
59 + const data = new Map();
60 + const version = caches.length + 1;
61 + const cache = {
62 + version,
63 + data,
64 + resolve(text) {
65 + const record = data.get(text);
66 + if (record === undefined) {
67 + const newRecord = {
68 + status: 'resolved',
69 + value: text,
70 + };
71 + data.set(text, newRecord);
72 + } else if (record.status === 'pending') {
73 + const thenable = record.value;
74 + record.status = 'resolved';
75 + record.value = text;
76 + thenable.pings.forEach(t => t());
77 + }
78 + },
79 + reject(text, error) {
80 + const record = data.get(text);
81 + if (record === undefined) {
82 + const newRecord = {
83 + status: 'rejected',
84 + value: error,
85 + };
86 + data.set(text, newRecord);
87 + } else if (record.status === 'pending') {
88 + const thenable = record.value;
89 + record.status = 'rejected';
90 + record.value = error;
91 + thenable.pings.forEach(t => t());
92 + }
93 + },
94 + };
95 + caches.push(cache);
96 + return cache;
97 + }
98 +
99 + function readText(text) {
100 + const textCache = getCacheForType(createTextCache);
101 + const record = textCache.data.get(text);
102 + if (record !== undefined) {
103 + switch (record.status) {
104 + case 'pending':
105 + Scheduler.unstable_yieldValue(`Suspend:${text}`);
106 + throw record.value;
107 + case 'rejected':
108 + Scheduler.unstable_yieldValue(`Error:${text}`);
109 + throw record.value;
110 + case 'resolved':
111 + return textCache.version;
112 + }
113 + } else {
114 + Scheduler.unstable_yieldValue(`Suspend:${text}`);
115 +
116 + const thenable = {
117 + pings: [],
118 + then(resolve) {
119 + if (newRecord.status === 'pending') {
120 + thenable.pings.push(resolve);
121 + } else {
122 + Promise.resolve().then(() => resolve(newRecord.value));
123 + }
124 + },
125 + };
126 +
127 + const newRecord = {
128 + status: 'pending',
129 + value: thenable,
130 + };
131 + textCache.data.set(text, newRecord);
132 +
133 + throw thenable;
134 + }
135 + }
136 +
137 + function Text({children = null, text}) {
138 + Scheduler.unstable_yieldValue(`Text:${text} render`);
139 + React.useLayoutEffect(() => {
140 + Scheduler.unstable_yieldValue(`Text:${text} create layout`);
141 + return () => {
142 + Scheduler.unstable_yieldValue(`Text:${text} destroy layout`);
143 + };
144 + }, []);
145 + React.useEffect(() => {
146 + Scheduler.unstable_yieldValue(`Text:${text} create passive`);
147 + return () => {
148 + Scheduler.unstable_yieldValue(`Text:${text} destroy passive`);
149 + };
150 + }, []);
151 + return <span prop={text}>{children}</span>;
152 + }
153 +
154 + function AsyncText({children = null, text}) {
155 + readText(text);
156 + Scheduler.unstable_yieldValue(`AsyncText:${text} render`);
157 + React.useLayoutEffect(() => {
158 + Scheduler.unstable_yieldValue(`AsyncText:${text} create layout`);
159 + return () => {
160 + Scheduler.unstable_yieldValue(`AsyncText:${text} destroy layout`);
161 + };
162 + }, []);
163 + React.useEffect(() => {
164 + Scheduler.unstable_yieldValue(`AsyncText:${text} create passive`);
165 + return () => {
166 + Scheduler.unstable_yieldValue(`AsyncText:${text} destroy passive`);
167 + };
168 + }, []);
169 + return <span prop={text}>{children}</span>;
170 + }
171 +
172 + function resolveMostRecentTextCache(text) {
173 + if (caches.length === 0) {
174 + throw Error('Cache does not exist.');
175 + } else {
176 + // Resolve the most recently created cache. An older cache can by
177 + // resolved with `caches[index].resolve(text)`.
178 + caches[caches.length - 1].resolve(text);
179 + }
180 + }
181 +
182 + const resolveText = resolveMostRecentTextCache;
183 +
184 + function span(prop, children = []) {
185 + return {type: 'span', children, prop, hidden: false};
186 + }
187 +
188 + function spanHidden(prop, children = []) {
189 + return {type: 'span', children, prop, hidden: true};
190 + }
191 +
192 + function advanceTimers(ms) {
193 + // Note: This advances Jest's virtual time but not React's. Use
194 + // ReactNoop.expire for that.
195 + if (typeof ms !== 'number') {
196 + throw new Error('Must specify ms');
197 + }
198 + jest.advanceTimersByTime(ms);
199 + // Wait until the end of the current tick
200 + // We cannot use a timer since we're faking them
201 + return Promise.resolve().then(() => {});
202 + }
203 +
204 + describe('when a component suspends during initial mount', () => {
205 + // @gate enableCache
206 + it('should not change behavior in concurrent mode', async () => {
207 + class ClassText extends React.Component {
208 + componentDidMount() {
209 + const {text} = this.props;
210 + Scheduler.unstable_yieldValue(`ClassText:${text} componentDidMount`);
211 + }
212 + componentDidUpdate() {
213 + const {text} = this.props;
214 + Scheduler.unstable_yieldValue(`ClassText:${text} componentDidUpdate`);
215 + }
216 + componentWillUnmount() {
217 + const {text} = this.props;
218 + Scheduler.unstable_yieldValue(
219 + `ClassText:${text} componentWillUnmount`,
220 + );
221 + }
222 + render() {
223 + const {children, text} = this.props;
224 + Scheduler.unstable_yieldValue(`ClassText:${text} render`);
225 + return <span prop={text}>{children}</span>;
226 + }
227 + }
228 +
229 + function App({children = null}) {
230 + Scheduler.unstable_yieldValue('App render');
231 + React.useLayoutEffect(() => {
232 + Scheduler.unstable_yieldValue('App create layout');
233 + return () => {
234 + Scheduler.unstable_yieldValue('App destroy layout');
235 + };
236 + }, []);
237 + React.useEffect(() => {
238 + Scheduler.unstable_yieldValue('App create passive');
239 + return () => {
240 + Scheduler.unstable_yieldValue('App destroy passive');
241 + };
242 + }, []);
243 + return (
244 + <>
245 + <Suspense fallback={<Text text="Fallback" />}>
246 + <Text text="Inside:Before" />
247 + {children}
248 + <ClassText text="Inside:After" />
249 + </Suspense>
250 + <Text text="Outside" />
251 + </>
252 + );
253 + }
254 +
255 + // Mount and suspend.
256 + await ReactNoop.act(async () => {
257 + ReactNoop.render(
258 + <App>
259 + <AsyncText text="Async" ms={1000} />
260 + </App>,
261 + );
262 + });
263 + expect(Scheduler).toHaveYielded([
264 + 'App render',
265 + 'Text:Inside:Before render',
266 + 'Suspend:Async',
267 + 'ClassText:Inside:After render',
268 + 'Text:Fallback render',
269 + 'Text:Outside render',
270 + 'Text:Fallback create layout',
271 + 'Text:Outside create layout',
272 + 'App create layout',
273 + 'Text:Fallback create passive',
274 + 'Text:Outside create passive',
275 + 'App create passive',
276 + ]);
277 + expect(ReactNoop.getChildren()).toEqual([
278 + span('Fallback'),
279 + span('Outside'),
280 + ]);
281 +
282 + // Resolving the suspended resource should
283 + await ReactNoop.act(async () => {
284 + await resolveText('Async');
285 + });
286 + expect(Scheduler).toHaveYielded([
287 + 'Text:Inside:Before render',
288 + 'AsyncText:Async render',
289 + 'ClassText:Inside:After render',
290 + 'Text:Fallback destroy layout',
291 + 'Text:Inside:Before create layout',
292 + 'AsyncText:Async create layout',
293 + 'ClassText:Inside:After componentDidMount',
294 + 'Text:Fallback destroy passive',
295 + 'Text:Inside:Before create passive',
296 + 'AsyncText:Async create passive',
297 + ]);
298 + expect(ReactNoop.getChildren()).toEqual([
299 + span('Inside:Before'),
300 + span('Async'),
301 + span('Inside:After'),
302 + span('Outside'),
303 + ]);
304 +
305 + await ReactNoop.act(async () => {
306 + ReactNoop.render(null);
307 + });
308 + expect(Scheduler).toHaveYielded([
309 + 'App destroy layout',
310 + 'Text:Inside:Before destroy layout',
311 + 'AsyncText:Async destroy layout',
312 + 'ClassText:Inside:After componentWillUnmount',
313 + 'Text:Outside destroy layout',
314 + 'App destroy passive',
315 + 'Text:Inside:Before destroy passive',
316 + 'AsyncText:Async destroy passive',
317 + 'Text:Outside destroy passive',
318 + ]);
319 + expect(ReactNoop.getChildren()).toEqual([]);
320 + });
321 +
322 + // @gate enableCache
323 + it('should not change behavior in sync', async () => {
324 + class ClassText extends React.Component {
325 + componentDidMount() {
326 + const {text} = this.props;
327 + Scheduler.unstable_yieldValue(`ClassText:${text} componentDidMount`);
328 + }
329 + componentDidUpdate() {
330 + const {text} = this.props;
331 + Scheduler.unstable_yieldValue(`ClassText:${text} componentDidUpdate`);
332 + }
333 + componentWillUnmount() {
334 + const {text} = this.props;
335 + Scheduler.unstable_yieldValue(
336 + `ClassText:${text} componentWillUnmount`,
337 + );
338 + }
339 + render() {
340 + const {children, text} = this.props;
341 + Scheduler.unstable_yieldValue(`ClassText:${text} render`);
342 + return <span prop={text}>{children}</span>;
343 + }
344 + }
345 +
346 + function App({children = null}) {
347 + Scheduler.unstable_yieldValue('App render');
348 + React.useLayoutEffect(() => {
349 + Scheduler.unstable_yieldValue('App create layout');
350 + return () => {
351 + Scheduler.unstable_yieldValue('App destroy layout');
352 + };
353 + }, []);
354 + React.useEffect(() => {
355 + Scheduler.unstable_yieldValue('App create passive');
356 + return () => {
357 + Scheduler.unstable_yieldValue('App destroy passive');
358 + };
359 + }, []);
360 + return (
361 + <>
362 + <Suspense fallback={<Text text="Fallback" />}>
363 + <Text text="Inside:Before" />
364 + {children}
365 + <ClassText text="Inside:After" />
366 + </Suspense>
367 + <Text text="Outside" />
368 + </>
369 + );
370 + }
371 +
372 + // Mount and suspend.
373 + ReactNoop.act(() => {
374 + ReactNoop.renderLegacySyncRoot(
375 + <App>
376 + <AsyncText text="Async" ms={1000} />
377 + </App>,
378 + );
379 + });
380 + expect(Scheduler).toHaveYielded([
381 + 'App render',
382 + 'Text:Inside:Before render',
383 + 'Suspend:Async',
384 + 'ClassText:Inside:After render',
385 + 'Text:Fallback render',
386 + 'Text:Outside render',
387 + 'Text:Inside:Before create layout',
388 + 'ClassText:Inside:After componentDidMount',
389 + 'Text:Fallback create layout',
390 + 'Text:Outside create layout',
391 + 'App create layout',
392 + 'Text:Inside:Before create passive',
393 + 'Text:Fallback create passive',
394 + 'Text:Outside create passive',
395 + 'App create passive',
396 + ]);
397 + expect(ReactNoop.getChildren()).toEqual([
398 + spanHidden('Inside:Before'),
399 + spanHidden('Inside:After'),
400 + span('Fallback'),
401 + span('Outside'),
402 + ]);
403 +
404 + // Resolving the suspended resource should
405 + await ReactNoop.act(async () => {
406 + await resolveText('Async');
407 + });
408 + expect(Scheduler).toHaveYielded([
409 + 'AsyncText:Async render',
410 + 'Text:Fallback destroy layout',
411 + 'AsyncText:Async create layout',
412 + 'Text:Fallback destroy passive',
413 + 'AsyncText:Async create passive',
414 + ]);
415 + expect(ReactNoop.getChildren()).toEqual([
416 + span('Inside:Before'),
417 + span('Async'),
418 + span('Inside:After'),
419 + span('Outside'),
420 + ]);
421 +
422 + await ReactNoop.act(async () => {
423 + ReactNoop.renderLegacySyncRoot(null);
424 + });
425 + expect(Scheduler).toHaveYielded([
426 + 'App destroy layout',
427 + 'Text:Inside:Before destroy layout',
428 + 'AsyncText:Async destroy layout',
429 + 'ClassText:Inside:After componentWillUnmount',
430 + 'Text:Outside destroy layout',
431 + 'App destroy passive',
432 + 'Text:Inside:Before destroy passive',
433 + 'AsyncText:Async destroy passive',
434 + 'Text:Outside destroy passive',
435 + ]);
436 + expect(ReactNoop.getChildren()).toEqual([]);
437 + });
438 + });
439 +
440 + describe('layout effects within a tree that re-suspends in an update', () => {
441 + // @gate enableCache
442 + it('should not be destroyed or recreated in legacy roots', async () => {
443 + function App({children = null}) {
444 + Scheduler.unstable_yieldValue('App render');
445 + React.useLayoutEffect(() => {
446 + Scheduler.unstable_yieldValue('App create layout');
447 + return () => {
448 + Scheduler.unstable_yieldValue('App destroy layout');
449 + };
450 + }, []);
451 + React.useEffect(() => {
452 + Scheduler.unstable_yieldValue('App create passive');
453 + return () => {
454 + Scheduler.unstable_yieldValue('App destroy passive');
455 + };
456 + }, []);
457 + return (
458 + <>
459 + <Suspense fallback={<Text text="Fallback" />}>
460 + <Text text="Inside:Before" />
461 + {children}
462 + <Text text="Inside:After" />
463 + </Suspense>
464 + <Text text="Outside" />
465 + </>
466 + );
467 + }
468 +
469 + // Mount
470 + ReactNoop.act(() => {
471 + ReactNoop.renderLegacySyncRoot(<App />);
472 + });
473 + expect(Scheduler).toHaveYielded([
474 + 'App render',
475 + 'Text:Inside:Before render',
476 + 'Text:Inside:After render',
477 + 'Text:Outside render',
478 + 'Text:Inside:Before create layout',
479 + 'Text:Inside:After create layout',
480 + 'Text:Outside create layout',
481 + 'App create layout',
482 + 'Text:Inside:Before create passive',
483 + 'Text:Inside:After create passive',
484 + 'Text:Outside create passive',
485 + 'App create passive',
486 + ]);
487 + expect(ReactNoop.getChildren()).toEqual([
488 + span('Inside:Before'),
489 + span('Inside:After'),
490 + span('Outside'),
491 + ]);
492 +
493 + // Schedule an update that causes React to suspend.
494 + ReactNoop.act(() => {
495 + ReactNoop.renderLegacySyncRoot(
496 + <App>
497 + <AsyncText text="Async" ms={1000} />
498 + </App>,
499 + );
500 + });
501 + expect(Scheduler).toHaveYielded([
502 + 'App render',
503 + 'Text:Inside:Before render',
504 + 'Suspend:Async',
505 + 'Text:Inside:After render',
506 + 'Text:Fallback render',
507 + 'Text:Outside render',
508 + 'Text:Fallback create layout',
509 + 'Text:Fallback create passive',
510 + ]);
511 + expect(ReactNoop.getChildren()).toEqual([
512 + spanHidden('Inside:Before'),
513 + spanHidden('Inside:After'),
514 + span('Fallback'),
515 + span('Outside'),
516 + ]);
517 +
518 + await advanceTimers(1000);
519 +
520 + // Noop since sync root has already committed
521 + expect(Scheduler).toHaveYielded([]);
522 + expect(ReactNoop.getChildren()).toEqual([
523 + spanHidden('Inside:Before'),
524 + spanHidden('Inside:After'),
525 + span('Fallback'),
526 + span('Outside'),
527 + ]);
528 +
529 + // Resolving the suspended resource should re-create inner layout effects.
530 + await ReactNoop.act(async () => {
531 + await resolveText('Async');
532 + });
533 + expect(Scheduler).toHaveYielded([
534 + 'AsyncText:Async render',
535 + 'Text:Fallback destroy layout',
536 + 'AsyncText:Async create layout',
537 + 'Text:Fallback destroy passive',
538 + 'AsyncText:Async create passive',
539 + ]);
540 + expect(ReactNoop.getChildren()).toEqual([
541 + span('Inside:Before'),
542 + span('Async'),
543 + span('Inside:After'),
544 + span('Outside'),
545 + ]);
546 +
547 + await ReactNoop.act(async () => {
548 + ReactNoop.renderLegacySyncRoot(null);
549 + });
550 + expect(Scheduler).toHaveYielded([
551 + 'App destroy layout',
552 + 'Text:Inside:Before destroy layout',
553 + 'AsyncText:Async destroy layout',
554 + 'Text:Inside:After destroy layout',
555 + 'Text:Outside destroy layout',
556 + 'App destroy passive',
557 + 'Text:Inside:Before destroy passive',
558 + 'AsyncText:Async destroy passive',
559 + 'Text:Inside:After destroy passive',
560 + 'Text:Outside destroy passive',
561 + ]);
562 + });
563 +
564 + // @gate enableSuspenseLayoutEffectSemantics
565 + // @gate enableCache
566 + it('should be destroyed and recreated for function components', async () => {
567 + function App({children = null}) {
568 + Scheduler.unstable_yieldValue('App render');
569 + React.useLayoutEffect(() => {
570 + Scheduler.unstable_yieldValue('App create layout');
571 + return () => {
572 + Scheduler.unstable_yieldValue('App destroy layout');
573 + };
574 + }, []);
575 + React.useEffect(() => {
576 + Scheduler.unstable_yieldValue('App create passive');
577 + return () => {
578 + Scheduler.unstable_yieldValue('App destroy passive');
579 + };
580 + }, []);
581 + return (
582 + <>
583 + <Suspense fallback={<Text text="Fallback" />}>
584 + <Text text="Inside:Before" />
585 + {children}
586 + <Text text="Inside:After" />
587 + </Suspense>
588 + <Text text="Outside" />
589 + </>
590 + );
591 + }
592 +
593 + await ReactNoop.act(async () => {
594 + ReactNoop.render(<App />);
595 + });
596 + expect(Scheduler).toHaveYielded([
597 + 'App render',
598 + 'Text:Inside:Before render',
599 + 'Text:Inside:After render',
600 + 'Text:Outside render',
601 + 'Text:Inside:Before create layout',
602 + 'Text:Inside:After create layout',
603 + 'Text:Outside create layout',
604 + 'App create layout',
605 + 'Text:Inside:Before create passive',
606 + 'Text:Inside:After create passive',
607 + 'Text:Outside create passive',
608 + 'App create passive',
609 + ]);
610 + expect(ReactNoop.getChildren()).toEqual([
611 + span('Inside:Before'),
612 + span('Inside:After'),
613 + span('Outside'),
614 + ]);
615 +
616 + // Schedule an update that causes React to suspend.
617 + ReactNoop.act(() => {
618 + ReactNoop.render(
619 + <App>
620 + <AsyncText text="Async" ms={1000} />
621 + </App>,
622 + );
623 + });
624 + expect(Scheduler).toHaveYielded([
625 + 'App render',
626 + 'Text:Inside:Before render',
627 + 'Suspend:Async',
628 + 'Text:Inside:After render',
629 + 'Text:Fallback render',
630 + 'Text:Outside render',
631 + ]);
632 + expect(ReactNoop.getChildren()).toEqual([
633 + span('Inside:Before'),
634 + span('Inside:After'),
635 + span('Outside'),
636 + ]);
637 +
638 + await advanceTimers(1000);
639 +
640 + // Timing out should commit the fallback and destroy inner layout effects.
641 + expect(Scheduler).toHaveYielded([
642 + 'Text:Inside:Before destroy layout',
643 + 'Text:Inside:After destroy layout',
644 + 'Text:Fallback create layout',
645 + ]);
646 + expect(Scheduler).toFlushAndYield(['Text:Fallback create passive']);
647 + expect(ReactNoop.getChildren()).toEqual([
648 + spanHidden('Inside:Before'),
649 + spanHidden('Inside:After'),
650 + span('Fallback'),
651 + span('Outside'),
652 + ]);
653 +
654 + // Resolving the suspended resource should re-create inner layout effects.
655 + await ReactNoop.act(async () => {
656 + await resolveText('Async');
657 + });
658 + expect(Scheduler).toHaveYielded([
659 + 'Text:Inside:Before render',
660 + 'AsyncText:Async render',
661 + 'Text:Inside:After render',
662 + 'Text:Fallback destroy layout',
663 + 'Text:Inside:Before create layout',
664 + 'AsyncText:Async create layout',
665 + 'Text:Inside:After create layout',
666 + 'Text:Fallback destroy passive',
667 + 'AsyncText:Async create passive',
668 + ]);
669 + expect(ReactNoop.getChildren()).toEqual([
670 + span('Inside:Before'),
671 + span('Async'),
672 + span('Inside:After'),
673 + span('Outside'),
674 + ]);
675 +
676 + await ReactNoop.act(async () => {
677 + ReactNoop.render(null);
678 + });
679 + expect(Scheduler).toHaveYielded([
680 + 'App destroy layout',
681 + 'Text:Inside:Before destroy layout',
682 + 'AsyncText:Async destroy layout',
683 + 'Text:Inside:After destroy layout',
684 + 'Text:Outside destroy layout',
685 + 'App destroy passive',
686 + 'Text:Inside:Before destroy passive',
687 + 'AsyncText:Async destroy passive',
688 + 'Text:Inside:After destroy passive',
689 + 'Text:Outside destroy passive',
690 + ]);
691 + });
692 +
693 + // @gate enableSuspenseLayoutEffectSemantics
694 + // @gate enableCache
695 + it('should be destroyed and recreated for class components', async () => {
696 + class ClassText extends React.Component {
697 + componentDidMount() {
698 + const {text} = this.props;
699 + Scheduler.unstable_yieldValue(`ClassText:${text} componentDidMount`);
700 + }
701 + componentDidUpdate() {
702 + const {text} = this.props;
703 + Scheduler.unstable_yieldValue(`ClassText:${text} componentDidUpdate`);
704 + }
705 + componentWillUnmount() {
706 + const {text} = this.props;
707 + Scheduler.unstable_yieldValue(
708 + `ClassText:${text} componentWillUnmount`,
709 + );
710 + }
711 + render() {
712 + const {children, text} = this.props;
713 + Scheduler.unstable_yieldValue(`ClassText:${text} render`);
714 + return <span prop={text}>{children}</span>;
715 + }
716 + }
717 +
718 + function App({children = null}) {
719 + Scheduler.unstable_yieldValue('App render');
720 + React.useLayoutEffect(() => {
721 + Scheduler.unstable_yieldValue('App create layout');
722 + return () => {
723 + Scheduler.unstable_yieldValue('App destroy layout');
724 + };
725 + }, []);
726 + React.useEffect(() => {
727 + Scheduler.unstable_yieldValue('App create passive');
728 + return () => {
729 + Scheduler.unstable_yieldValue('App destroy passive');
730 + };
731 + }, []);
732 + return (
733 + <>
734 + <Suspense fallback={<ClassText text="Fallback" />}>
735 + <ClassText text="Inside:Before" />
736 + {children}
737 + <ClassText text="Inside:After" />
738 + </Suspense>
739 + <ClassText text="Outside" />
740 + </>
741 + );
742 + }
743 +
744 + // Mount
745 + await ReactNoop.act(async () => {
746 + ReactNoop.render(<App />);
747 + });
748 + expect(Scheduler).toHaveYielded([
749 + 'App render',
750 + 'ClassText:Inside:Before render',
751 + 'ClassText:Inside:After render',
752 + 'ClassText:Outside render',
753 + 'ClassText:Inside:Before componentDidMount',
754 + 'ClassText:Inside:After componentDidMount',
755 + 'ClassText:Outside componentDidMount',
756 + 'App create layout',
757 + 'App create passive',
758 + ]);
759 + expect(ReactNoop.getChildren()).toEqual([
760 + span('Inside:Before'),
761 + span('Inside:After'),
762 + span('Outside'),
763 + ]);
764 +
765 + // Schedule an update that causes React to suspend.
766 + ReactNoop.act(() => {
767 + ReactNoop.render(
768 + <App>
769 + <AsyncText text="Async" ms={1000} />
770 + </App>,
771 + );
772 + });
773 + expect(Scheduler).toHaveYielded([
774 + 'App render',
775 + 'ClassText:Inside:Before render',
776 + 'Suspend:Async',
777 + 'ClassText:Inside:After render',
778 + 'ClassText:Fallback render',
779 + 'ClassText:Outside render',
780 + ]);
781 + expect(ReactNoop.getChildren()).toEqual([
782 + span('Inside:Before'),
783 + span('Inside:After'),
784 + span('Outside'),
785 + ]);
786 +
787 + await advanceTimers(1000);
788 +
789 + // Timing out should commit the fallback and destroy inner layout effects.
790 + expect(Scheduler).toHaveYielded([
791 + 'ClassText:Inside:Before componentWillUnmount',
792 + 'ClassText:Inside:After componentWillUnmount',
793 + 'ClassText:Fallback componentDidMount',
794 + 'ClassText:Outside componentDidUpdate',
795 + ]);
796 + expect(ReactNoop.getChildren()).toEqual([
797 + spanHidden('Inside:Before'),
798 + spanHidden('Inside:After'),
799 + span('Fallback'),
800 + span('Outside'),
801 + ]);
802 +
803 + // Resolving the suspended resource should re-create inner layout effects.
804 + await ReactNoop.act(async () => {
805 + await resolveText('Async');
806 + });
807 + expect(Scheduler).toHaveYielded([
808 + 'ClassText:Inside:Before render',
809 + 'AsyncText:Async render',
810 + 'ClassText:Inside:After render',
811 + 'ClassText:Fallback componentWillUnmount',
812 + 'ClassText:Inside:Before componentDidMount',
813 + 'AsyncText:Async create layout',
814 + 'ClassText:Inside:After componentDidMount',
815 + 'AsyncText:Async create passive',
816 + ]);
817 + expect(ReactNoop.getChildren()).toEqual([
818 + span('Inside:Before'),
819 + span('Async'),
820 + span('Inside:After'),
821 + span('Outside'),
822 + ]);
823 +
824 + await ReactNoop.act(async () => {
825 + ReactNoop.render(null);
826 + });
827 + expect(Scheduler).toHaveYielded([
828 + 'App destroy layout',
829 + 'ClassText:Inside:Before componentWillUnmount',
830 + 'AsyncText:Async destroy layout',
831 + 'ClassText:Inside:After componentWillUnmount',
832 + 'ClassText:Outside componentWillUnmount',
833 + 'App destroy passive',
834 + 'AsyncText:Async destroy passive',
835 + ]);
836 + });
837 +
838 + // @gate enableSuspenseLayoutEffectSemantics
839 + // @gate enableCache
840 + it('should be destroyed and recreated when nested below host components', async () => {
841 + function App({children = null}) {
842 + Scheduler.unstable_yieldValue('App render');
843 + React.useLayoutEffect(() => {
844 + Scheduler.unstable_yieldValue('App create layout');
845 + return () => {
846 + Scheduler.unstable_yieldValue('App destroy layout');
847 + };
848 + }, []);
849 + React.useEffect(() => {
850 + Scheduler.unstable_yieldValue('App create passive');
851 + return () => {
852 + Scheduler.unstable_yieldValue('App destroy passive');
853 + };
854 + }, []);
855 + return (
856 + <Suspense fallback={<Text text="Fallback" />}>
857 + {children}
858 + <Text text="Outer">
859 + <Text text="Inner" />
860 + </Text>
861 + </Suspense>
862 + );
863 + }
864 +
865 + // Mount
866 + await ReactNoop.act(async () => {
867 + ReactNoop.render(<App />);
868 + });
869 + expect(Scheduler).toHaveYielded([
870 + 'App render',
871 + 'Text:Outer render',
872 + 'Text:Inner render',
873 + 'Text:Inner create layout',
874 + 'Text:Outer create layout',
875 + 'App create layout',
876 + 'Text:Inner create passive',
877 + 'Text:Outer create passive',
878 + 'App create passive',
879 + ]);
880 + expect(ReactNoop.getChildren()).toEqual([span('Outer', [span('Inner')])]);
881 +
882 + // Schedule an update that causes React to suspend.
883 + ReactNoop.act(() => {
884 + ReactNoop.render(
885 + <App>
886 + <AsyncText text="Async" ms={1000} />
887 + </App>,
888 + );
889 + });
890 + expect(Scheduler).toHaveYielded([
891 + 'App render',
892 + 'Suspend:Async',
893 + 'Text:Outer render',
894 + 'Text:Inner render',
895 + 'Text:Fallback render',
896 + ]);
897 + expect(ReactNoop.getChildren()).toEqual([span('Outer', [span('Inner')])]);
898 +
899 + await advanceTimers(1000);
900 +
901 + // Timing out should commit the fallback and destroy inner layout effects.
902 + expect(Scheduler).toHaveYielded([
903 + 'Text:Outer destroy layout',
904 + 'Text:Inner destroy layout',
905 + 'Text:Fallback create layout',
906 + ]);
907 + expect(Scheduler).toFlushAndYield(['Text:Fallback create passive']);
908 + expect(ReactNoop.getChildren()).toEqual([
909 + spanHidden('Outer', [spanHidden('Inner')]),
910 + span('Fallback'),
911 + ]);
912 +
913 + // Resolving the suspended resource should re-create inner layout effects.
914 + await ReactNoop.act(async () => {
915 + await resolveText('Async');
916 + });
917 + expect(Scheduler).toHaveYielded([
918 + 'AsyncText:Async render',
919 + 'Text:Outer render',
920 + 'Text:Inner render',
921 + 'Text:Fallback destroy layout',
922 + 'AsyncText:Async create layout',
923 + 'Text:Inner create layout',
924 + 'Text:Outer create layout',
925 + 'Text:Fallback destroy passive',
926 + 'AsyncText:Async create passive',
927 + ]);
928 + expect(ReactNoop.getChildren()).toEqual([
929 + span('Async'),
930 + span('Outer', [span('Inner')]),
931 + ]);
932 +
933 + await ReactNoop.act(async () => {
934 + ReactNoop.render(null);
935 + });
936 + expect(Scheduler).toHaveYielded([
937 + 'App destroy layout',
938 + 'AsyncText:Async destroy layout',
939 + 'Text:Outer destroy layout',
940 + 'Text:Inner destroy layout',
941 + 'App destroy passive',
942 + 'AsyncText:Async destroy passive',
943 + 'Text:Outer destroy passive',
944 + 'Text:Inner destroy passive',
945 + ]);
946 + });
947 +
948 + // @gate enableSuspenseLayoutEffectSemantics
949 + // @gate enableCache
950 + it('should be destroyed and recreated even if there is a bailout because of memoization', async () => {
951 + const MemoizedText = React.memo(Text, () => true);
952 +
953 + function App({children = null}) {
954 + Scheduler.unstable_yieldValue('App render');
955 + React.useLayoutEffect(() => {
956 + Scheduler.unstable_yieldValue('App create layout');
957 + return () => {
958 + Scheduler.unstable_yieldValue('App destroy layout');
959 + };
960 + }, []);
961 + React.useEffect(() => {
962 + Scheduler.unstable_yieldValue('App create passive');
963 + return () => {
964 + Scheduler.unstable_yieldValue('App destroy passive');
965 + };
966 + }, []);
967 + return (
968 + <Suspense fallback={<Text text="Fallback" />}>
969 + {children}
970 + <Text text="Outer">
971 + <MemoizedText text="MemoizedInner" />
972 + </Text>
973 + </Suspense>
974 + );
975 + }
976 +
977 + // Mount
978 + await ReactNoop.act(async () => {
979 + ReactNoop.render(<App />);
980 + });
981 + expect(Scheduler).toHaveYielded([
982 + 'App render',
983 + 'Text:Outer render',
984 + 'Text:MemoizedInner render',
985 + 'Text:MemoizedInner create layout',
986 + 'Text:Outer create layout',
987 + 'App create layout',
988 + 'Text:MemoizedInner create passive',
989 + 'Text:Outer create passive',
990 + 'App create passive',
991 + ]);
992 + expect(ReactNoop.getChildren()).toEqual([
993 + span('Outer', [span('MemoizedInner')]),
994 + ]);
995 +
996 + // Schedule an update that causes React to suspend.
997 + ReactNoop.act(() => {
998 + ReactNoop.render(
999 + <App>
1000 + <AsyncText text="Async" ms={1000} />
1001 + </App>,
1002 + );
1003 + });
1004 + expect(Scheduler).toHaveYielded([
1005 + 'App render',
1006 + 'Suspend:Async',
1007 + 'Text:Outer render',
1008 + // Text:MemoizedInner is memoized
1009 + 'Text:Fallback render',
1010 + ]);
1011 + expect(ReactNoop.getChildren()).toEqual([
1012 + span('Outer', [span('MemoizedInner')]),
1013 + ]);
1014 +
1015 + await advanceTimers(1000);
1016 +
1017 + // Timing out should commit the fallback and destroy inner layout effects.
1018 + // Even though the innermost layout effects are beneat a hidden HostComponent.
1019 + expect(Scheduler).toHaveYielded([
1020 + 'Text:Outer destroy layout',
1021 + 'Text:MemoizedInner destroy layout',
1022 + 'Text:Fallback create layout',
1023 + ]);
1024 + expect(Scheduler).toFlushAndYield(['Text:Fallback create passive']);
1025 + expect(ReactNoop.getChildren()).toEqual([
1026 + spanHidden('Outer', [spanHidden('MemoizedInner')]),
1027 + span('Fallback'),
1028 + ]);
1029 +
1030 + // Resolving the suspended resource should re-create inner layout effects.
1031 + await ReactNoop.act(async () => {
1032 + await resolveText('Async');
1033 + });
1034 + expect(Scheduler).toHaveYielded([
1035 + 'AsyncText:Async render',
1036 + 'Text:Outer render',
1037 + 'Text:Fallback destroy layout',
1038 + 'AsyncText:Async create layout',
1039 + 'Text:MemoizedInner create layout',
1040 + 'Text:Outer create layout',
1041 + 'Text:Fallback destroy passive',
1042 + 'AsyncText:Async create passive',
1043 + ]);
1044 + expect(ReactNoop.getChildren()).toEqual([
1045 + span('Async'),
1046 + span('Outer', [span('MemoizedInner')]),
1047 + ]);
1048 +
1049 + await ReactNoop.act(async () => {
1050 + ReactNoop.render(null);
1051 + });
1052 + expect(Scheduler).toHaveYielded([
1053 + 'App destroy layout',
1054 + 'AsyncText:Async destroy layout',
1055 + 'Text:Outer destroy layout',
1056 + 'Text:MemoizedInner destroy layout',
1057 + 'App destroy passive',
1058 + 'AsyncText:Async destroy passive',
1059 + 'Text:Outer destroy passive',
1060 + 'Text:MemoizedInner destroy passive',
1061 + ]);
1062 + });
1063 +
1064 + // @gate enableSuspenseLayoutEffectSemantics
1065 + // @gate enableCache
1066 + it('should respect nested suspense boundaries', async () => {
1067 + function App({innerChildren = null, outerChildren = null}) {
1068 + return (
1069 + <Suspense fallback={<Text text="OuterFallback" />}>
1070 + <Text text="Outer" />
1071 + {outerChildren}
1072 + <Suspense fallback={<Text text="InnerFallback" />}>
1073 + <Text text="Inner" />
1074 + {innerChildren}
1075 + </Suspense>
1076 + </Suspense>
1077 + );
1078 + }
1079 +
1080 + // Mount
1081 + await ReactNoop.act(async () => {
1082 + ReactNoop.render(<App />);
1083 + });
1084 + expect(Scheduler).toHaveYielded([
1085 + 'Text:Outer render',
1086 + 'Text:Inner render',
1087 + 'Text:Outer create layout',
1088 + 'Text:Inner create layout',
1089 + 'Text:Outer create passive',
1090 + 'Text:Inner create passive',
1091 + ]);
1092 + expect(ReactNoop.getChildren()).toEqual([span('Outer'), span('Inner')]);
1093 +
1094 + // Suspend the inner Suspense subtree (only inner effects should be destroyed)
1095 + ReactNoop.act(() => {
1096 + ReactNoop.render(
1097 + <App innerChildren={<AsyncText text="InnerAsync_1" ms={1000} />} />,
1098 + );
1099 + });
1100 + await advanceTimers(1000);
1101 + expect(Scheduler).toHaveYielded([
1102 + 'Text:Outer render',
1103 + 'Text:Inner render',
1104 + 'Suspend:InnerAsync_1',
1105 + 'Text:InnerFallback render',
1106 + 'Text:Inner destroy layout',
1107 + 'Text:InnerFallback create layout',
1108 + ]);
1109 + expect(Scheduler).toFlushAndYield(['Text:InnerFallback create passive']);
1110 + expect(ReactNoop.getChildren()).toEqual([
1111 + span('Outer'),
1112 + spanHidden('Inner'),
1113 + span('InnerFallback'),
1114 + ]);
1115 +
1116 + // Suspend the outer Suspense subtree (outer effects and inner fallback effects should be destroyed)
1117 + // (This check also ensures we don't destroy effects for mounted inner fallback.)
1118 + ReactNoop.act(() => {
1119 + ReactNoop.render(
1120 + <App
1121 + outerChildren={<AsyncText text="OuterAsync_1" ms={1000} />}
1122 + innerChildren={<AsyncText text="InnerAsync_1" ms={1000} />}
1123 + />,
1124 + );
1125 + });
1126 + await advanceTimers(1000);
1127 + expect(Scheduler).toHaveYielded([
1128 + 'Text:Outer render',
1129 + 'Suspend:OuterAsync_1',
1130 + 'Text:Inner render',
1131 + 'Suspend:InnerAsync_1',
1132 + 'Text:InnerFallback render',
1133 + 'Text:OuterFallback render',
1134 + 'Text:Outer destroy layout',
1135 + 'Text:InnerFallback destroy layout',
1136 + 'Text:OuterFallback create layout',
1137 + ]);
1138 + expect(Scheduler).toFlushAndYield(['Text:OuterFallback create passive']);
1139 + expect(ReactNoop.getChildren()).toEqual([
1140 + spanHidden('Outer'),
1141 + spanHidden('Inner'),
1142 + spanHidden('InnerFallback'),
1143 + span('OuterFallback'),
1144 + ]);
1145 +
1146 + // Show the inner Susepnse subtree (no effects should be recreated)
1147 + await ReactNoop.act(async () => {
1148 + await resolveText('InnerAsync_1');
1149 + });
1150 + expect(Scheduler).toHaveYielded([
1151 + 'Text:Outer render',
1152 + 'Suspend:OuterAsync_1',
1153 + 'Text:Inner render',
1154 + 'AsyncText:InnerAsync_1 render',
1155 + ]);
1156 + expect(ReactNoop.getChildren()).toEqual([
1157 + spanHidden('Outer'),
1158 + spanHidden('Inner'),
1159 + spanHidden('InnerFallback'),
1160 + span('OuterFallback'),
1161 + ]);
1162 +
1163 + // Suspend the inner Suspense subtree (no effects should be destroyed)
1164 + ReactNoop.act(() => {
1165 + ReactNoop.render(
1166 + <App
1167 + outerChildren={<AsyncText text="OuterAsync_1" ms={1000} />}
1168 + innerChildren={<AsyncText text="InnerAsync_2" ms={1000} />}
1169 + />,
1170 + );
1171 + });
1172 + await advanceTimers(1000);
1173 + expect(Scheduler).toHaveYielded([
1174 + 'Text:Outer render',
1175 + 'Suspend:OuterAsync_1',
1176 + 'Text:Inner render',
1177 + 'Suspend:InnerAsync_2',
1178 + 'Text:InnerFallback render',
1179 + 'Text:OuterFallback render',
1180 + ]);
1181 + expect(ReactNoop.getChildren()).toEqual([
1182 + spanHidden('Outer'),
1183 + spanHidden('Inner'),
1184 + spanHidden('InnerFallback'),
1185 + span('OuterFallback'),
1186 + ]);
1187 +
1188 + // Show the outer Susepnse subtree (only outer effects should be recreated)
1189 + await ReactNoop.act(async () => {
1190 + await resolveText('OuterAsync_1');
1191 + });
1192 + expect(Scheduler).toHaveYielded([
1193 + 'Text:Outer render',
1194 + 'AsyncText:OuterAsync_1 render',
1195 + 'Text:Inner render',
1196 + 'Suspend:InnerAsync_2',
1197 + 'Text:InnerFallback render',
1198 + 'Text:OuterFallback destroy layout',
1199 + 'Text:Outer create layout',
1200 + 'AsyncText:OuterAsync_1 create layout',
1201 + 'Text:InnerFallback create layout',
1202 + 'Text:OuterFallback destroy passive',
1203 + 'AsyncText:OuterAsync_1 create passive',
1204 + ]);
1205 + expect(ReactNoop.getChildren()).toEqual([
1206 + span('Outer'),
1207 + span('OuterAsync_1'),
1208 + spanHidden('Inner'),
1209 + span('InnerFallback'),
1210 + ]);
1211 +
1212 + // Show the inner Susepnse subtree (only inner effects should be recreated)
1213 + await ReactNoop.act(async () => {
1214 + await resolveText('InnerAsync_2');
1215 + });
1216 + expect(Scheduler).toHaveYielded([
1217 + 'Text:Inner render',
1218 + 'AsyncText:InnerAsync_2 render',
1219 + 'Text:InnerFallback destroy layout',
1220 + 'Text:Inner create layout',
1221 + 'AsyncText:InnerAsync_2 create layout',
1222 + 'Text:InnerFallback destroy passive',
1223 + 'AsyncText:InnerAsync_2 create passive',
1224 + ]);
1225 + expect(ReactNoop.getChildren()).toEqual([
1226 + span('Outer'),
1227 + span('OuterAsync_1'),
1228 + span('Inner'),
1229 + span('InnerAsync_2'),
1230 + ]);
1231 +
1232 + // Suspend the outer Suspense subtree (all effects should be destroyed)
1233 + ReactNoop.act(() => {
1234 + ReactNoop.render(
1235 + <App
1236 + outerChildren={<AsyncText text="OuterAsync_2" ms={1000} />}
1237 + innerChildren={<AsyncText text="InnerAsync_2" ms={1000} />}
1238 + />,
1239 + );
1240 + });
1241 + await advanceTimers(1000);
1242 + expect(Scheduler).toHaveYielded([
1243 + 'Text:Outer render',
1244 + 'Suspend:OuterAsync_2',
1245 + 'Text:Inner render',
1246 + 'AsyncText:InnerAsync_2 render',
1247 + 'Text:OuterFallback render',
1248 + 'Text:Outer destroy layout',
1249 + 'AsyncText:OuterAsync_1 destroy layout',
1250 + 'Text:Inner destroy layout',
1251 + 'AsyncText:InnerAsync_2 destroy layout',
1252 + 'Text:OuterFallback create layout',
1253 + ]);
1254 + expect(ReactNoop.getChildren()).toEqual([
1255 + spanHidden('Outer'),
1256 + spanHidden('OuterAsync_1'),
1257 + spanHidden('Inner'),
1258 + spanHidden('InnerAsync_2'),
1259 + span('OuterFallback'),
1260 + ]);
1261 +
1262 + // Show the outer Suspense subtree (all effects should be recreated)
1263 + await ReactNoop.act(async () => {
1264 + await resolveText('OuterAsync_2');
1265 + });
1266 + expect(Scheduler).toHaveYielded([
1267 + 'Text:OuterFallback create passive',
1268 + 'Text:Outer render',
1269 + 'AsyncText:OuterAsync_2 render',
1270 + 'Text:Inner render',
1271 + 'AsyncText:InnerAsync_2 render',
1272 + 'Text:OuterFallback destroy layout',
1273 + 'Text:Outer create layout',
1274 + 'AsyncText:OuterAsync_2 create layout',
1275 + 'Text:Inner create layout',
1276 + 'AsyncText:InnerAsync_2 create layout',
1277 + 'Text:OuterFallback destroy passive',
1278 + ]);
1279 + expect(ReactNoop.getChildren()).toEqual([
1280 + span('Outer'),
1281 + span('OuterAsync_2'),
1282 + span('Inner'),
1283 + span('InnerAsync_2'),
1284 + ]);
1285 + });
1286 +
1287 + // @gate enableSuspenseLayoutEffectSemantics
1288 + // @gate enableCache
1289 + it('should be cleaned up inside of a fallback that suspends', async () => {
1290 + function App({fallbackChildren = null, outerChildren = null}) {
1291 + return (
1292 + <>
1293 + <Suspense
1294 + fallback={
1295 + <>
1296 + <Suspense fallback={<Text text="Fallback:Fallback" />}>
1297 + <Text text="Fallback:Inside" />
1298 + {fallbackChildren}
1299 + </Suspense>
1300 + <Text text="Fallback:Outside" />
1301 + </>
1302 + }>
1303 + <Text text="Inside" />
1304 + {outerChildren}
1305 + </Suspense>
1306 + <Text text="Outside" />
1307 + </>
1308 + );
1309 + }
1310 +
1311 + // Mount
1312 + await ReactNoop.act(async () => {
1313 + ReactNoop.render(<App />);
1314 + });
1315 + expect(Scheduler).toHaveYielded([
1316 + 'Text:Inside render',
1317 + 'Text:Outside render',
1318 + 'Text:Inside create layout',
1319 + 'Text:Outside create layout',
1320 + 'Text:Inside create passive',
1321 + 'Text:Outside create passive',
1322 + ]);
1323 + expect(ReactNoop.getChildren()).toEqual([
1324 + span('Inside'),
1325 + span('Outside'),
1326 + ]);
1327 +
1328 + // Suspend the outer shell
1329 + ReactNoop.act(() => {
1330 + ReactNoop.render(
1331 + <App outerChildren={<AsyncText text="OutsideAsync" ms={1000} />} />,
1332 + );
1333 + });
1334 + expect(Scheduler).toHaveYielded([
1335 + 'Text:Inside render',
1336 + 'Suspend:OutsideAsync',
1337 + 'Text:Fallback:Inside render',
1338 + 'Text:Fallback:Outside render',
1339 + 'Text:Outside render',
1340 + ]);
1341 + expect(ReactNoop.getChildren()).toEqual([
1342 + span('Inside'),
1343 + span('Outside'),
1344 + ]);
1345 +
1346 + // Timing out should commit the fallback and destroy inner layout effects.
1347 + await advanceTimers(1000);
1348 + expect(Scheduler).toHaveYielded([
1349 + 'Text:Inside destroy layout',
1350 + 'Text:Fallback:Inside create layout',
1351 + 'Text:Fallback:Outside create layout',
1352 + ]);
1353 + expect(Scheduler).toFlushAndYield([
1354 + 'Text:Fallback:Inside create passive',
1355 + 'Text:Fallback:Outside create passive',
1356 + ]);
1357 + expect(ReactNoop.getChildren()).toEqual([
1358 + spanHidden('Inside'),
1359 + span('Fallback:Inside'),
1360 + span('Fallback:Outside'),
1361 + span('Outside'),
1362 + ]);
1363 +
1364 + // Suspend the fallback and verify that it's effects get cleaned up as well
1365 + ReactNoop.act(() => {
1366 + ReactNoop.render(
1367 + <App
1368 + fallbackChildren={<AsyncText text="FallbackAsync" ms={1000} />}
1369 + outerChildren={<AsyncText text="OutsideAsync" ms={1000} />}
1370 + />,
1371 + );
1372 + });
1373 + expect(Scheduler).toHaveYielded([
1374 + 'Text:Inside render',
1375 + 'Suspend:OutsideAsync',
1376 + 'Text:Fallback:Inside render',
1377 + 'Suspend:FallbackAsync',
1378 + 'Text:Fallback:Fallback render',
1379 + 'Text:Fallback:Outside render',
1380 + 'Text:Outside render',
1381 + ]);
1382 + expect(ReactNoop.getChildren()).toEqual([
1383 + spanHidden('Inside'),
1384 + span('Fallback:Inside'),
1385 + span('Fallback:Outside'),
1386 + span('Outside'),
1387 + ]);
1388 +
1389 + // Timing out should commit the inner fallback and destroy outer fallback layout effects.
1390 + await advanceTimers(1000);
1391 + expect(Scheduler).toHaveYielded([
1392 + 'Text:Fallback:Inside destroy layout',
1393 + 'Text:Fallback:Fallback create layout',
1394 + ]);
1395 + expect(Scheduler).toFlushAndYield([
1396 + 'Text:Fallback:Fallback create passive',
1397 + ]);
1398 + expect(ReactNoop.getChildren()).toEqual([
1399 + spanHidden('Inside'),
1400 + spanHidden('Fallback:Inside'),
1401 + span('Fallback:Fallback'),
1402 + span('Fallback:Outside'),
1403 + span('Outside'),
1404 + ]);
1405 +
1406 + // Resolving both resources should cleanup fallabck effects and recreate main effects
1407 + await ReactNoop.act(async () => {
1408 + await resolveText('FallbackAsync');
1409 + await resolveText('OutsideAsync');
1410 + });
1411 + expect(Scheduler).toHaveYielded([
1412 + 'Text:Inside render',
1413 + 'AsyncText:OutsideAsync render',
1414 + 'Text:Fallback:Fallback destroy layout',
1415 + 'Text:Fallback:Outside destroy layout',
1416 + 'Text:Inside create layout',
1417 + 'AsyncText:OutsideAsync create layout',
1418 + 'Text:Fallback:Inside destroy passive',
1419 + 'Text:Fallback:Fallback destroy passive',
1420 + 'Text:Fallback:Outside destroy passive',
1421 + 'AsyncText:OutsideAsync create passive',
1422 + ]);
1423 + expect(ReactNoop.getChildren()).toEqual([
1424 + span('Inside'),
1425 + span('OutsideAsync'),
1426 + span('Outside'),
1427 + ]);
1428 + });
1429 +
1430 + // @gate enableSuspenseLayoutEffectSemantics
1431 + // @gate enableCache
1432 + it('should be cleaned up inside of a fallback that suspends (alternate)', async () => {
1433 + function App({fallbackChildren = null, outerChildren = null}) {
1434 + return (
1435 + <>
1436 + <Suspense
1437 + fallback={
1438 + <>
1439 + <Suspense fallback={<Text text="Fallback:Fallback" />}>
1440 + <Text text="Fallback:Inside" />
1441 + {fallbackChildren}
1442 + </Suspense>
1443 + <Text text="Fallback:Outside" />
1444 + </>
1445 + }>
1446 + <Text text="Inside" />
1447 + {outerChildren}
1448 + </Suspense>
1449 + <Text text="Outside" />
1450 + </>
1451 + );
1452 + }
1453 +
1454 + // Mount
1455 + await ReactNoop.act(async () => {
1456 + ReactNoop.render(<App />);
1457 + });
1458 + expect(Scheduler).toHaveYielded([
1459 + 'Text:Inside render',
1460 + 'Text:Outside render',
1461 + 'Text:Inside create layout',
1462 + 'Text:Outside create layout',
1463 + 'Text:Inside create passive',
1464 + 'Text:Outside create passive',
1465 + ]);
1466 + expect(ReactNoop.getChildren()).toEqual([
1467 + span('Inside'),
1468 + span('Outside'),
1469 + ]);
1470 +
1471 + // Suspend both the outer boundary and the fallback
1472 + ReactNoop.act(() => {
1473 + ReactNoop.render(
1474 + <App
1475 + outerChildren={<AsyncText text="OutsideAsync" ms={1000} />}
1476 + fallbackChildren={<AsyncText text="FallbackAsync" ms={1000} />}
1477 + />,
1478 + );
1479 + });
1480 + await advanceTimers(1000);
1481 + expect(Scheduler).toHaveYielded([
1482 + 'Text:Inside render',
1483 + 'Suspend:OutsideAsync',
1484 + 'Text:Fallback:Inside render',
1485 + 'Suspend:FallbackAsync',
1486 + 'Text:Fallback:Fallback render',
1487 + 'Text:Fallback:Outside render',
1488 + 'Text:Outside render',
1489 + 'Text:Inside destroy layout',
1490 + 'Text:Fallback:Fallback create layout',
1491 + 'Text:Fallback:Outside create layout',
1492 + ]);
1493 + expect(Scheduler).toFlushAndYield([
1494 + 'Text:Fallback:Fallback create passive',
1495 + 'Text:Fallback:Outside create passive',
1496 + ]);
1497 + expect(ReactNoop.getChildren()).toEqual([
1498 + spanHidden('Inside'),
1499 + span('Fallback:Fallback'),
1500 + span('Fallback:Outside'),
1501 + span('Outside'),
1502 + ]);
1503 +
1504 + // Resolving the inside fallback
1505 + await ReactNoop.act(async () => {
1506 + await resolveText('FallbackAsync');
1507 + });
1508 + expect(Scheduler).toHaveYielded([
1509 + 'Text:Fallback:Inside render',
1510 + 'AsyncText:FallbackAsync render',
1511 + 'Text:Fallback:Fallback destroy layout',
1512 + 'Text:Fallback:Inside create layout',
1513 + 'AsyncText:FallbackAsync create layout',
1514 + 'Text:Fallback:Fallback destroy passive',
1515 + 'Text:Fallback:Inside create passive',
1516 + 'AsyncText:FallbackAsync create passive',
1517 + ]);
1518 + expect(ReactNoop.getChildren()).toEqual([
1519 + spanHidden('Inside'),
1520 + span('Fallback:Inside'),
1521 + span('FallbackAsync'),
1522 + span('Fallback:Outside'),
1523 + span('Outside'),
1524 + ]);
1525 +
1526 + // Resolving the outer fallback only
1527 + await ReactNoop.act(async () => {
1528 + await resolveText('OutsideAsync');
1529 + });
1530 + expect(Scheduler).toHaveYielded([
1531 + 'Text:Inside render',
1532 + 'AsyncText:OutsideAsync render',
1533 + 'Text:Fallback:Inside destroy layout',
1534 + 'AsyncText:FallbackAsync destroy layout',
1535 + 'Text:Fallback:Outside destroy layout',
1536 + 'Text:Inside create layout',
1537 + 'AsyncText:OutsideAsync create layout',
1538 + 'Text:Fallback:Inside destroy passive',
1539 + 'AsyncText:FallbackAsync destroy passive',
1540 + 'Text:Fallback:Outside destroy passive',
1541 + 'AsyncText:OutsideAsync create passive',
1542 + ]);
1543 + expect(ReactNoop.getChildren()).toEqual([
1544 + span('Inside'),
1545 + span('OutsideAsync'),
1546 + span('Outside'),
1547 + ]);
1548 + });
1549 +
1550 + // @gate enableSuspenseLayoutEffectSemantics
1551 + // @gate enableCache
1552 + it('should be cleaned up deeper inside of a subtree that suspends', async () => {
1553 + function ConditionalSuspense({shouldSuspend}) {
1554 + if (shouldSuspend) {
1555 + readText('Suspend');
1556 + }
1557 + return <Text text="Inside" />;
1558 + }
1559 +
1560 + function App({children = null, shouldSuspend}) {
1561 + return (
1562 + <>
1563 + <Suspense fallback={<Text text="Fallback" />}>
1564 + <ConditionalSuspense shouldSuspend={shouldSuspend} />
1565 + </Suspense>
1566 + <Text text="Outside" />
1567 + </>
1568 + );
1569 + }
1570 +
1571 + // Mount
1572 + await ReactNoop.act(async () => {
1573 + ReactNoop.render(<App shouldSuspend={false} />);
1574 + });
1575 + expect(Scheduler).toHaveYielded([
1576 + 'Text:Inside render',
1577 + 'Text:Outside render',
1578 + 'Text:Inside create layout',
1579 + 'Text:Outside create layout',
1580 + 'Text:Inside create passive',
1581 + 'Text:Outside create passive',
1582 + ]);
1583 + expect(ReactNoop.getChildren()).toEqual([
1584 + span('Inside'),
1585 + span('Outside'),
1586 + ]);
1587 +
1588 + // Suspending a component in the middle of the tree
1589 + // should still properly cleanup effects deeper in the tree
1590 + ReactNoop.act(() => {
1591 + ReactNoop.render(<App shouldSuspend={true} />);
1592 + });
1593 + expect(Scheduler).toHaveYielded([
1594 + 'Suspend:Suspend',
1595 + 'Text:Fallback render',
1596 + 'Text:Outside render',
1597 + ]);
1598 + expect(ReactNoop.getChildren()).toEqual([
1599 + span('Inside'),
1600 + span('Outside'),
1601 + ]);
1602 +
1603 + // Timing out should commit the inner fallback and destroy outer fallback layout effects.
1604 + await advanceTimers(1000);
1605 + expect(Scheduler).toHaveYielded([
1606 + 'Text:Inside destroy layout',
1607 + 'Text:Fallback create layout',
1608 + ]);
1609 + expect(Scheduler).toFlushAndYield(['Text:Fallback create passive']);
1610 + expect(ReactNoop.getChildren()).toEqual([
1611 + spanHidden('Inside'),
1612 + span('Fallback'),
1613 + span('Outside'),
1614 + ]);
1615 +
1616 + // Resolving should cleanup.
1617 + await ReactNoop.act(async () => {
1618 + await resolveText('Suspend');
1619 + });
1620 + expect(Scheduler).toHaveYielded([
1621 + 'Text:Inside render',
1622 + 'Text:Fallback destroy layout',
1623 + 'Text:Inside create layout',
1624 + 'Text:Fallback destroy passive',
1625 + ]);
1626 + expect(ReactNoop.getChildren()).toEqual([
1627 + span('Inside'),
1628 + span('Outside'),
1629 + ]);
1630 + });
1631 +
1632 + describe('that throw errors', () => {
1633 + // @gate enableSuspenseLayoutEffectSemantics
1634 + // @gate enableCache
1635 + it('are properly handled for componentDidMount', async () => {
1636 + let componentDidMountShouldThrow = false;
1637 +
1638 + class ThrowsInDidMount extends React.Component {
1639 + componentWillUnmount() {
1640 + Scheduler.unstable_yieldValue(
1641 + 'ThrowsInDidMount componentWillUnmount',
1642 + );
1643 + }
1644 + componentDidMount() {
1645 + Scheduler.unstable_yieldValue('ThrowsInDidMount componentDidMount');
1646 + if (componentDidMountShouldThrow) {
1647 + throw Error('expected');
1648 + }
1649 + }
1650 + render() {
1651 + Scheduler.unstable_yieldValue('ThrowsInDidMount render');
1652 + return <span prop="ThrowsInDidMount" />;
1653 + }
1654 + }
1655 +
1656 + function App({children = null}) {
1657 + Scheduler.unstable_yieldValue('App render');
1658 + React.useLayoutEffect(() => {
1659 + Scheduler.unstable_yieldValue('App create layout');
1660 + return () => {
1661 + Scheduler.unstable_yieldValue('App destroy layout');
1662 + };
1663 + }, []);
1664 + return (
1665 + <>
1666 + <Suspense fallback={<Text text="Fallback" />}>
1667 + {children}
1668 + <ThrowsInDidMount />
1669 + <Text text="Inside" />
1670 + </Suspense>
1671 + <Text text="Outside" />
1672 + </>
1673 + );
1674 + }
1675 +
1676 + await ReactNoop.act(async () => {
1677 + ReactNoop.render(
1678 + <ErrorBoundary fallback={<Text text="Error" />}>
1679 + <App />
1680 + </ErrorBoundary>,
1681 + );
1682 + });
1683 + expect(Scheduler).toHaveYielded([
1684 + 'ErrorBoundary render: try',
1685 + 'App render',
1686 + 'ThrowsInDidMount render',
1687 + 'Text:Inside render',
1688 + 'Text:Outside render',
1689 + 'ThrowsInDidMount componentDidMount',
1690 + 'Text:Inside create layout',
1691 + 'Text:Outside create layout',
1692 + 'App create layout',
1693 + 'Text:Inside create passive',
1694 + 'Text:Outside create passive',
1695 + ]);
1696 + expect(ReactNoop.getChildren()).toEqual([
1697 + span('ThrowsInDidMount'),
1698 + span('Inside'),
1699 + span('Outside'),
1700 + ]);
1701 +
1702 + // Schedule an update that causes React to suspend.
1703 + await ReactNoop.act(async () => {
1704 + ReactNoop.render(
1705 + <ErrorBoundary fallback={<Text text="Error" />}>
1706 + <App>
1707 + <AsyncText text="Async" ms={1000} />
1708 + </App>
1709 + </ErrorBoundary>,
1710 + );
1711 + });
1712 + expect(Scheduler).toHaveYielded([
1713 + 'ErrorBoundary render: try',
1714 + 'App render',
1715 + 'Suspend:Async',
1716 + 'ThrowsInDidMount render',
1717 + 'Text:Inside render',
1718 + 'Text:Fallback render',
1719 + 'Text:Outside render',
1720 + 'ThrowsInDidMount componentWillUnmount',
1721 + 'Text:Inside destroy layout',
1722 + 'Text:Fallback create layout',
1723 + 'Text:Fallback create passive',
1724 + ]);
1725 + expect(ReactNoop.getChildren()).toEqual([
1726 + spanHidden('ThrowsInDidMount'),
1727 + spanHidden('Inside'),
1728 + span('Fallback'),
1729 + span('Outside'),
1730 + ]);
1731 +
1732 + // Resolve the pending suspense and throw
1733 + componentDidMountShouldThrow = true;
1734 + await ReactNoop.act(async () => {
1735 + await resolveText('Async');
1736 + });
1737 + expect(Scheduler).toHaveYielded([
1738 + 'AsyncText:Async render',
1739 + 'ThrowsInDidMount render',
1740 + 'Text:Inside render',
1741 + 'Text:Fallback destroy layout',
1742 + 'AsyncText:Async create layout',
1743 +
1744 + // Even though an error was thrown in componentDidMount,
1745 + // subsequent layout effects should still be destroyed.
1746 + 'ThrowsInDidMount componentDidMount',
1747 + 'Text:Inside create layout',
1748 +
1749 + // Finish the in-progress commit
1750 + 'Text:Fallback destroy passive',
1751 + 'AsyncText:Async create passive',
1752 +
1753 + // Destroy layout and passive effects in the errored tree.
1754 + 'App destroy layout',
1755 + 'AsyncText:Async destroy layout',
1756 + 'ThrowsInDidMount componentWillUnmount',
1757 + 'Text:Inside destroy layout',
1758 + 'Text:Outside destroy layout',
1759 + 'AsyncText:Async destroy passive',
1760 + 'Text:Inside destroy passive',
1761 + 'Text:Outside destroy passive',
1762 +
1763 + // Render fallback
1764 + 'ErrorBoundary render: catch',
1765 + 'Text:Error render',
1766 + 'Text:Error create layout',
1767 + 'Text:Error create passive',
1768 + ]);
1769 + expect(ReactNoop.getChildren()).toEqual([span('Error')]);
1770 + });
1771 +
1772 + // @gate enableSuspenseLayoutEffectSemantics
1773 + // @gate enableCache
1774 + it('are properly handled for componentWillUnmount', async () => {
1775 + class ThrowsInWillUnmount extends React.Component {
1776 + componentDidMount() {
1777 + Scheduler.unstable_yieldValue(
1778 + 'ThrowsInWillUnmount componentDidMount',
1779 + );
1780 + }
1781 + componentWillUnmount() {
1782 + Scheduler.unstable_yieldValue(
1783 + 'ThrowsInWillUnmount componentWillUnmount',
1784 + );
1785 + throw Error('expected');
1786 + }
1787 + render() {
1788 + Scheduler.unstable_yieldValue('ThrowsInWillUnmount render');
1789 + return <span prop="ThrowsInWillUnmount" />;
1790 + }
1791 + }
1792 +
1793 + function App({children = null}) {
1794 + Scheduler.unstable_yieldValue('App render');
1795 + React.useLayoutEffect(() => {
1796 + Scheduler.unstable_yieldValue('App create layout');
1797 + return () => {
1798 + Scheduler.unstable_yieldValue('App destroy layout');
1799 + };
1800 + }, []);
1801 + return (
1802 + <>
1803 + <Suspense fallback={<Text text="Fallback" />}>
1804 + {children}
1805 + <ThrowsInWillUnmount />
1806 + <Text text="Inside" />
1807 + </Suspense>
1808 + <Text text="Outside" />
1809 + </>
1810 + );
1811 + }
1812 +
1813 + await ReactNoop.act(async () => {
1814 + ReactNoop.render(
1815 + <ErrorBoundary fallback={<Text text="Error" />}>
1816 + <App />
1817 + </ErrorBoundary>,
1818 + );
1819 + });
1820 + expect(Scheduler).toHaveYielded([
1821 + 'ErrorBoundary render: try',
1822 + 'App render',
1823 + 'ThrowsInWillUnmount render',
1824 + 'Text:Inside render',
1825 + 'Text:Outside render',
1826 + 'ThrowsInWillUnmount componentDidMount',
1827 + 'Text:Inside create layout',
1828 + 'Text:Outside create layout',
1829 + 'App create layout',
1830 + 'Text:Inside create passive',
1831 + 'Text:Outside create passive',
1832 + ]);
1833 + expect(ReactNoop.getChildren()).toEqual([
1834 + span('ThrowsInWillUnmount'),
1835 + span('Inside'),
1836 + span('Outside'),
1837 + ]);
1838 +
1839 + // Schedule an update that suspends and triggers our error code.
1840 + await ReactNoop.act(async () => {
1841 + ReactNoop.render(
1842 + <ErrorBoundary fallback={<Text text="Error" />}>
1843 + <App>
1844 + <AsyncText text="Async" ms={1000} />
1845 + </App>
1846 + </ErrorBoundary>,
1847 + );
1848 + });
1849 + expect(Scheduler).toHaveYielded([
1850 + 'ErrorBoundary render: try',
1851 + 'App render',
1852 + 'Suspend:Async',
1853 + 'ThrowsInWillUnmount render',
1854 + 'Text:Inside render',
1855 + 'Text:Fallback render',
1856 + 'Text:Outside render',
1857 +
1858 + // Even though an error was thrown in componentWillUnmount,
1859 + // subsequent layout effects should still be destroyed.
1860 + 'ThrowsInWillUnmount componentWillUnmount',
1861 + 'Text:Inside destroy layout',
1862 +
1863 + // Finish the in-progess commit
1864 + 'Text:Fallback create layout',
1865 + 'Text:Fallback create passive',
1866 +
1867 + // Destroy layout and passive effects in the errored tree.
1868 + 'App destroy layout',
1869 + 'ThrowsInWillUnmount componentWillUnmount',
1870 + 'Text:Fallback destroy layout',
1871 + 'Text:Outside destroy layout',
1872 + 'Text:Inside destroy passive',
1873 + 'Text:Fallback destroy passive',
1874 + 'Text:Outside destroy passive',
1875 +
1876 + // Render fallback
1877 + 'ErrorBoundary render: catch',
1878 + 'Text:Error render',
1879 + 'Text:Error create layout',
1880 + 'Text:Error create passive',
1881 + ]);
1882 + expect(ReactNoop.getChildren()).toEqual([span('Error')]);
1883 + });
1884 +
1885 + // @gate enableSuspenseLayoutEffectSemantics
1886 + // @gate enableCache
1887 + // @gate replayFailedUnitOfWorkWithInvokeGuardedCallback
1888 + it('are properly handled for layout effect creation', async () => {
1889 + let useLayoutEffectShouldThrow = false;
1890 +
1891 + function ThrowsInLayoutEffect() {
1892 + Scheduler.unstable_yieldValue('ThrowsInLayoutEffect render');
1893 + React.useLayoutEffect(() => {
1894 + Scheduler.unstable_yieldValue(
1895 + 'ThrowsInLayoutEffect useLayoutEffect create',
1896 + );
1897 + if (useLayoutEffectShouldThrow) {
1898 + throw Error('expected');
1899 + }
1900 + return () => {
1901 + Scheduler.unstable_yieldValue(
1902 + 'ThrowsInLayoutEffect useLayoutEffect destroy',
1903 + );
1904 + };
1905 + }, []);
1906 + return <span prop="ThrowsInLayoutEffect" />;
1907 + }
1908 +
1909 + function App({children = null}) {
1910 + Scheduler.unstable_yieldValue('App render');
1911 + React.useLayoutEffect(() => {
1912 + Scheduler.unstable_yieldValue('App create layout');
1913 + return () => {
1914 + Scheduler.unstable_yieldValue('App destroy layout');
1915 + };
1916 + }, []);
1917 + return (
1918 + <>
1919 + <Suspense fallback={<Text text="Fallback" />}>
1920 + {children}
1921 + <ThrowsInLayoutEffect />
1922 + <Text text="Inside" />
1923 + </Suspense>
1924 + <Text text="Outside" />
1925 + </>
1926 + );
1927 + }
1928 +
1929 + await ReactNoop.act(async () => {
1930 + ReactNoop.render(
1931 + <ErrorBoundary fallback={<Text text="Error" />}>
1932 + <App />
1933 + </ErrorBoundary>,
1934 + );
1935 + });
1936 + expect(Scheduler).toHaveYielded([
1937 + 'ErrorBoundary render: try',
1938 + 'App render',
1939 + 'ThrowsInLayoutEffect render',
1940 + 'Text:Inside render',
1941 + 'Text:Outside render',
1942 + 'ThrowsInLayoutEffect useLayoutEffect create',
1943 + 'Text:Inside create layout',
1944 + 'Text:Outside create layout',
1945 + 'App create layout',
1946 + 'Text:Inside create passive',
1947 + 'Text:Outside create passive',
1948 + ]);
1949 + expect(ReactNoop.getChildren()).toEqual([
1950 + span('ThrowsInLayoutEffect'),
1951 + span('Inside'),
1952 + span('Outside'),
1953 + ]);
1954 +
1955 + // Schedule an update that causes React to suspend.
1956 + await ReactNoop.act(async () => {
1957 + ReactNoop.render(
1958 + <ErrorBoundary fallback={<Text text="Error" />}>
1959 + <App>
1960 + <AsyncText text="Async" ms={1000} />
1961 + </App>
1962 + </ErrorBoundary>,
1963 + );
1964 + });
1965 + expect(Scheduler).toHaveYielded([
1966 + 'ErrorBoundary render: try',
1967 + 'App render',
1968 + 'Suspend:Async',
1969 + 'ThrowsInLayoutEffect render',
1970 + 'Text:Inside render',
1971 + 'Text:Fallback render',
1972 + 'Text:Outside render',
1973 + 'ThrowsInLayoutEffect useLayoutEffect destroy',
1974 + 'Text:Inside destroy layout',
1975 + 'Text:Fallback create layout',
1976 + 'Text:Fallback create passive',
1977 + ]);
1978 + expect(ReactNoop.getChildren()).toEqual([
1979 + spanHidden('ThrowsInLayoutEffect'),
1980 + spanHidden('Inside'),
1981 + span('Fallback'),
1982 + span('Outside'),
1983 + ]);
1984 +
1985 + // Resolve the pending suspense and throw
1986 + useLayoutEffectShouldThrow = true;
1987 + await ReactNoop.act(async () => {
1988 + await resolveText('Async');
1989 + });
1990 + expect(Scheduler).toHaveYielded([
1991 + 'AsyncText:Async render',
1992 + 'ThrowsInLayoutEffect render',
1993 + 'Text:Inside render',
1994 +
1995 + 'Text:Fallback destroy layout',
1996 +
1997 + // Even though an error was thrown in useLayoutEffect,
1998 + // subsequent layout effects should still be created.
1999 + 'AsyncText:Async create layout',
2000 + 'ThrowsInLayoutEffect useLayoutEffect create',
2001 + 'Text:Inside create layout',
2002 +
2003 + // Finish the in-progess commit
2004 + 'Text:Fallback destroy passive',
2005 + 'AsyncText:Async create passive',
2006 +
2007 + // Destroy layout and passive effects in the errored tree.
2008 + 'App destroy layout',
2009 + 'AsyncText:Async destroy layout',
2010 + 'Text:Inside destroy layout',
2011 + 'Text:Outside destroy layout',
2012 + 'AsyncText:Async destroy passive',
2013 + 'Text:Inside destroy passive',
2014 + 'Text:Outside destroy passive',
2015 +
2016 + // Render fallback
2017 + 'ErrorBoundary render: catch',
2018 + 'Text:Error render',
2019 + 'Text:Error create layout',
2020 + 'Text:Error create passive',
2021 + ]);
2022 + expect(ReactNoop.getChildren()).toEqual([span('Error')]);
2023 + });
2024 +
2025 + // @gate enableSuspenseLayoutEffectSemantics
2026 + // @gate enableCache
2027 + // @gate replayFailedUnitOfWorkWithInvokeGuardedCallback
2028 + it('are properly handled for layout effect descruction', async () => {
2029 + function ThrowsInLayoutEffectDestroy() {
2030 + Scheduler.unstable_yieldValue('ThrowsInLayoutEffectDestroy render');
2031 + React.useLayoutEffect(() => {
2032 + Scheduler.unstable_yieldValue(
2033 + 'ThrowsInLayoutEffectDestroy useLayoutEffect create',
2034 + );
2035 + return () => {
2036 + Scheduler.unstable_yieldValue(
2037 + 'ThrowsInLayoutEffectDestroy useLayoutEffect destroy',
2038 + );
2039 + throw Error('expected');
2040 + };
2041 + }, []);
2042 + return <span prop="ThrowsInLayoutEffectDestroy" />;
2043 + }
2044 +
2045 + function App({children = null}) {
2046 + Scheduler.unstable_yieldValue('App render');
2047 + React.useLayoutEffect(() => {
2048 + Scheduler.unstable_yieldValue('App create layout');
2049 + return () => {
2050 + Scheduler.unstable_yieldValue('App destroy layout');
2051 + };
2052 + }, []);
2053 + return (
2054 + <>
2055 + <Suspense fallback={<Text text="Fallback" />}>
2056 + {children}
2057 + <ThrowsInLayoutEffectDestroy />
2058 + <Text text="Inside" />
2059 + </Suspense>
2060 + <Text text="Outside" />
2061 + </>
2062 + );
2063 + }
2064 +
2065 + await ReactNoop.act(async () => {
2066 + ReactNoop.render(
2067 + <ErrorBoundary fallback={<Text text="Error" />}>
2068 + <App />
2069 + </ErrorBoundary>,
2070 + );
2071 + });
2072 + expect(Scheduler).toHaveYielded([
2073 + 'ErrorBoundary render: try',
2074 + 'App render',
2075 + 'ThrowsInLayoutEffectDestroy render',
2076 + 'Text:Inside render',
2077 + 'Text:Outside render',
2078 + 'ThrowsInLayoutEffectDestroy useLayoutEffect create',
2079 + 'Text:Inside create layout',
2080 + 'Text:Outside create layout',
2081 + 'App create layout',
2082 + 'Text:Inside create passive',
2083 + 'Text:Outside create passive',
2084 + ]);
2085 + expect(ReactNoop.getChildren()).toEqual([
2086 + span('ThrowsInLayoutEffectDestroy'),
2087 + span('Inside'),
2088 + span('Outside'),
2089 + ]);
2090 +
2091 + // Schedule an update that suspends and triggers our error code.
2092 + await ReactNoop.act(async () => {
2093 + ReactNoop.render(
2094 + <ErrorBoundary fallback={<Text text="Error" />}>
2095 + <App>
2096 + <AsyncText text="Async" ms={1000} />
2097 + </App>
2098 + </ErrorBoundary>,
2099 + );
2100 + });
2101 + expect(Scheduler).toHaveYielded([
2102 + 'ErrorBoundary render: try',
2103 + 'App render',
2104 + 'Suspend:Async',
2105 + 'ThrowsInLayoutEffectDestroy render',
2106 + 'Text:Inside render',
2107 + 'Text:Fallback render',
2108 + 'Text:Outside render',
2109 +
2110 + // Even though an error was thrown in useLayoutEffect destroy,
2111 + // subsequent layout effects should still be destroyed.
2112 + 'ThrowsInLayoutEffectDestroy useLayoutEffect destroy',
2113 + 'Text:Inside destroy layout',
2114 +
2115 + // Finish the in-progess commit
2116 + 'Text:Fallback create layout',
2117 + 'Text:Fallback create passive',
2118 +
2119 + // Destroy layout and passive effects in the errored tree.
2120 + 'App destroy layout',
2121 + 'Text:Fallback destroy layout',
2122 + 'Text:Outside destroy layout',
2123 + 'Text:Inside destroy passive',
2124 + 'Text:Fallback destroy passive',
2125 + 'Text:Outside destroy passive',
2126 +
2127 + // Render fallback
2128 + 'ErrorBoundary render: catch',
2129 + 'Text:Error render',
2130 + 'Text:Error create layout',
2131 + 'Text:Error create passive',
2132 + ]);
2133 + expect(ReactNoop.getChildren()).toEqual([span('Error')]);
2134 + });
2135 + });
2136 +
2137 + // @gate enableSuspenseLayoutEffectSemantics
2138 + // @gate enableCache
2139 + it('should be only destroy layout effects once if a tree suspends in multiple places', async () => {
2140 + class ClassText extends React.Component {
2141 + componentDidMount() {
2142 + const {text} = this.props;
2143 + Scheduler.unstable_yieldValue(`ClassText:${text} componentDidMount`);
2144 + }
2145 + componentDidUpdate() {
2146 + const {text} = this.props;
2147 + Scheduler.unstable_yieldValue(`ClassText:${text} componentDidUpdate`);
2148 + }
2149 + componentWillUnmount() {
2150 + const {text} = this.props;
2151 + Scheduler.unstable_yieldValue(
2152 + `ClassText:${text} componentWillUnmount`,
2153 + );
2154 + }
2155 + render() {
2156 + const {children, text} = this.props;
2157 + Scheduler.unstable_yieldValue(`ClassText:${text} render`);
2158 + return <span prop={text}>{children}</span>;
2159 + }
2160 + }
2161 +
2162 + function App({children = null}) {
2163 + return (
2164 + <Suspense fallback={<ClassText text="Fallback" />}>
2165 + <Text text="Function" />
2166 + {children}
2167 + <ClassText text="Class" />
2168 + </Suspense>
2169 + );
2170 + }
2171 +
2172 + await ReactNoop.act(async () => {
2173 + ReactNoop.render(<App />);
2174 + });
2175 + expect(Scheduler).toHaveYielded([
2176 + 'Text:Function render',
2177 + 'ClassText:Class render',
2178 + 'Text:Function create layout',
2179 + 'ClassText:Class componentDidMount',
2180 + 'Text:Function create passive',
2181 + ]);
2182 + expect(ReactNoop.getChildren()).toEqual([
2183 + span('Function'),
2184 + span('Class'),
2185 + ]);
2186 +
2187 + // Schedule an update that causes React to suspend.
2188 + ReactNoop.act(() => {
2189 + ReactNoop.render(
2190 + <App>
2191 + <AsyncText text="Async_1" ms={1000} />
2192 + <AsyncText text="Async_2" ms={2000} />
2193 + </App>,
2194 + );
2195 + });
2196 + expect(Scheduler).toHaveYielded([
2197 + 'Text:Function render',
2198 + 'Suspend:Async_1',
2199 + 'Suspend:Async_2',
2200 + 'ClassText:Class render',
2201 + 'ClassText:Fallback render',
2202 + ]);
2203 + expect(ReactNoop.getChildren()).toEqual([
2204 + span('Function'),
2205 + span('Class'),
2206 + ]);
2207 +
2208 + await advanceTimers(1000);
2209 +
2210 + // Timing out should commit the fallback and destroy inner layout effects.
2211 + expect(Scheduler).toHaveYielded([
2212 + 'Text:Function destroy layout',
2213 + 'ClassText:Class componentWillUnmount',
2214 + 'ClassText:Fallback componentDidMount',
2215 + ]);
2216 + expect(ReactNoop.getChildren()).toEqual([
2217 + spanHidden('Function'),
2218 + spanHidden('Class'),
2219 + span('Fallback'),
2220 + ]);
2221 +
2222 + // Resolving the suspended resource should re-create inner layout effects.
2223 + await ReactNoop.act(async () => {
2224 + await resolveText('Async_1');
2225 + });
2226 + expect(Scheduler).toHaveYielded([
2227 + 'Text:Function render',
2228 + 'AsyncText:Async_1 render',
2229 + 'Suspend:Async_2',
2230 + 'ClassText:Class render',
2231 + ]);
2232 + expect(ReactNoop.getChildren()).toEqual([
2233 + spanHidden('Function'),
2234 + spanHidden('Class'),
2235 + span('Fallback'),
2236 + ]);
2237 +
2238 + // Resolving the suspended resource should re-create inner layout effects.
2239 + await ReactNoop.act(async () => {
2240 + await resolveText('Async_2');
2241 + });
2242 + expect(Scheduler).toHaveYielded([
2243 + 'Text:Function render',
2244 + 'AsyncText:Async_1 render',
2245 + 'AsyncText:Async_2 render',
2246 + 'ClassText:Class render',
2247 + 'ClassText:Fallback componentWillUnmount',
2248 + 'Text:Function create layout',
2249 + 'AsyncText:Async_1 create layout',
2250 + 'AsyncText:Async_2 create layout',
2251 + 'ClassText:Class componentDidMount',
2252 + 'AsyncText:Async_1 create passive',
2253 + 'AsyncText:Async_2 create passive',
2254 + ]);
2255 + expect(ReactNoop.getChildren()).toEqual([
2256 + span('Function'),
2257 + span('Async_1'),
2258 + span('Async_2'),
2259 + span('Class'),
2260 + ]);
2261 +
2262 + await ReactNoop.act(async () => {
2263 + ReactNoop.render(null);
2264 + });
2265 + expect(Scheduler).toHaveYielded([
2266 + 'Text:Function destroy layout',
2267 + 'AsyncText:Async_1 destroy layout',
2268 + 'AsyncText:Async_2 destroy layout',
2269 + 'ClassText:Class componentWillUnmount',
2270 + 'Text:Function destroy passive',
2271 + 'AsyncText:Async_1 destroy passive',
2272 + 'AsyncText:Async_2 destroy passive',
2273 + ]);
2274 + });
2275 +
2276 + // @gate enableSuspenseLayoutEffectSemantics
2277 + // @gate enableCache
2278 + it('should be only destroy layout effects once if a component suspends multiple times', async () => {
2279 + class ClassText extends React.Component {
2280 + componentDidMount() {
2281 + const {text} = this.props;
2282 + Scheduler.unstable_yieldValue(`ClassText:${text} componentDidMount`);
2283 + }
2284 + componentDidUpdate() {
2285 + const {text} = this.props;
2286 + Scheduler.unstable_yieldValue(`ClassText:${text} componentDidUpdate`);
2287 + }
2288 + componentWillUnmount() {
2289 + const {text} = this.props;
2290 + Scheduler.unstable_yieldValue(
2291 + `ClassText:${text} componentWillUnmount`,
2292 + );
2293 + }
2294 + render() {
2295 + const {children, text} = this.props;
2296 + Scheduler.unstable_yieldValue(`ClassText:${text} render`);
2297 + return <span prop={text}>{children}</span>;
2298 + }
2299 + }
2300 +
2301 + let textToRead = null;
2302 +
2303 + function Suspender() {
2304 + Scheduler.unstable_yieldValue(`Suspender "${textToRead}" render`);
2305 + if (textToRead !== null) {
2306 + readText(textToRead);
2307 + }
2308 + return <span prop="Suspender" />;
2309 + }
2310 +
2311 + function App({children = null}) {
2312 + return (
2313 + <Suspense fallback={<ClassText text="Fallback" />}>
2314 + <Text text="Function" />
2315 + <Suspender />
2316 + <ClassText text="Class" />
2317 + </Suspense>
2318 + );
2319 + }
2320 +
2321 + await ReactNoop.act(async () => {
2322 + ReactNoop.render(<App />);
2323 + });
2324 + expect(Scheduler).toHaveYielded([
2325 + 'Text:Function render',
2326 + 'Suspender "null" render',
2327 + 'ClassText:Class render',
2328 + 'Text:Function create layout',
2329 + 'ClassText:Class componentDidMount',
2330 + 'Text:Function create passive',
2331 + ]);
2332 + expect(ReactNoop.getChildren()).toEqual([
2333 + span('Function'),
2334 + span('Suspender'),
2335 + span('Class'),
2336 + ]);
2337 +
2338 + // Schedule an update that causes React to suspend.
2339 + textToRead = 'A';
2340 + ReactNoop.act(() => {
2341 + ReactNoop.render(<App />);
2342 + });
2343 + expect(Scheduler).toHaveYielded([
2344 + 'Text:Function render',
2345 + 'Suspender "A" render',
2346 + 'Suspend:A',
2347 + 'ClassText:Class render',
2348 + 'ClassText:Fallback render',
2349 + ]);
2350 + expect(ReactNoop.getChildren()).toEqual([
2351 + span('Function'),
2352 + span('Suspender'),
2353 + span('Class'),
2354 + ]);
2355 +
2356 + await advanceTimers(1000);
2357 +
2358 + // Timing out should commit the fallback and destroy inner layout effects.
2359 + expect(Scheduler).toHaveYielded([
2360 + 'Text:Function destroy layout',
2361 + 'ClassText:Class componentWillUnmount',
2362 + 'ClassText:Fallback componentDidMount',
2363 + ]);
2364 + expect(ReactNoop.getChildren()).toEqual([
2365 + spanHidden('Function'),
2366 + spanHidden('Suspender'),
2367 + spanHidden('Class'),
2368 + span('Fallback'),
2369 + ]);
2370 +
2371 + // Resolving the suspended resource should re-create inner layout effects.
2372 + textToRead = 'B';
2373 + await ReactNoop.act(async () => {
2374 + await resolveText('A');
2375 + });
2376 + expect(Scheduler).toHaveYielded([
2377 + 'Text:Function render',
2378 + 'Suspender "B" render',
2379 + 'Suspend:B',
2380 + 'ClassText:Class render',
2381 + ]);
2382 + expect(ReactNoop.getChildren()).toEqual([
2383 + spanHidden('Function'),
2384 + spanHidden('Suspender'),
2385 + spanHidden('Class'),
2386 + span('Fallback'),
2387 + ]);
2388 +
2389 + // Resolving the suspended resource should re-create inner layout effects.
2390 + await ReactNoop.act(async () => {
2391 + await resolveText('B');
2392 + });
2393 + expect(Scheduler).toHaveYielded([
2394 + 'Text:Function render',
2395 + 'Suspender "B" render',
2396 + 'ClassText:Class render',
2397 + 'ClassText:Fallback componentWillUnmount',
2398 + 'Text:Function create layout',
2399 + 'ClassText:Class componentDidMount',
2400 + ]);
2401 + expect(ReactNoop.getChildren()).toEqual([
2402 + span('Function'),
2403 + span('Suspender'),
2404 + span('Class'),
2405 + ]);
2406 +
2407 + await ReactNoop.act(async () => {
2408 + ReactNoop.render(null);
2409 + });
2410 + expect(Scheduler).toHaveYielded([
2411 + 'Text:Function destroy layout',
2412 + 'ClassText:Class componentWillUnmount',
2413 + 'Text:Function destroy passive',
2414 + ]);
2415 + });
2416 + });
2417 +
2418 + describe('refs within a tree that re-suspends in an update', () => {
2419 + function RefCheckerOuter({Component}) {
2420 + const refObject = React.useRef(null);
2421 +
2422 + const manualRef = React.useMemo(() => ({current: null}), []);
2423 + const refCallback = React.useCallback(value => {
2424 + Scheduler.unstable_yieldValue(
2425 + `RefCheckerOuter refCallback value? ${value != null}`,
2426 + );
2427 + manualRef.current = value;
2428 + }, []);
2429 +
2430 + Scheduler.unstable_yieldValue(`RefCheckerOuter render`);
2431 +
2432 + React.useLayoutEffect(() => {
2433 + Scheduler.unstable_yieldValue(
2434 + `RefCheckerOuter create layout refObject? ${refObject.current !=
2435 + null} refCallback? ${manualRef.current != null}`,
2436 + );
2437 + return () => {
2438 + Scheduler.unstable_yieldValue(
2439 + `RefCheckerOuter destroy layout refObject? ${refObject.current !=
2440 + null} refCallback? ${manualRef.current != null}`,
2441 + );
2442 + };
2443 + }, []);
2444 +
2445 + return (
2446 + <>
2447 + <Component ref={refObject} prop="refObject">
2448 + <RefCheckerInner forwardedRef={refObject} text="refObject" />
2449 + </Component>
2450 + <Component ref={refCallback} prop="refCallback">
2451 + <RefCheckerInner forwardedRef={manualRef} text="refCallback" />
2452 + </Component>
2453 + </>
2454 + );
2455 + }
2456 +
2457 + function RefCheckerInner({forwardedRef, text}) {
2458 + Scheduler.unstable_yieldValue(`RefCheckerInner:${text} render`);
2459 + React.useLayoutEffect(() => {
2460 + Scheduler.unstable_yieldValue(
2461 + `RefCheckerInner:${text} create layout ref? ${forwardedRef.current !=
2462 + null}`,
2463 + );
2464 + return () => {
2465 + Scheduler.unstable_yieldValue(
2466 + `RefCheckerInner:${text} destroy layout ref? ${forwardedRef.current !=
2467 + null}`,
2468 + );
2469 + };
2470 + }, []);
2471 + return null;
2472 + }
2473 +
2474 + // @gate enableCache
2475 + it('should not be cleared within legacy roots', async () => {
2476 + class ClassComponent extends React.Component {
2477 + render() {
2478 + Scheduler.unstable_yieldValue(
2479 + `ClassComponent:${this.props.prop} render`,
2480 + );
2481 + return this.props.children;
2482 + }
2483 + }
2484 +
2485 + function App({children}) {
2486 + Scheduler.unstable_yieldValue(`App render`);
2487 + return (
2488 + <Suspense fallback={<Text text="Fallback" />}>
2489 + {children}
2490 + <RefCheckerOuter Component={ClassComponent} />
2491 + </Suspense>
2492 + );
2493 + }
2494 +
2495 + ReactNoop.act(() => {
2496 + ReactNoop.renderLegacySyncRoot(<App />);
2497 + });
2498 + expect(Scheduler).toHaveYielded([
2499 + 'App render',
2500 + 'RefCheckerOuter render',
2501 + 'ClassComponent:refObject render',
2502 + 'RefCheckerInner:refObject render',
2503 + 'ClassComponent:refCallback render',
2504 + 'RefCheckerInner:refCallback render',
2505 + 'RefCheckerInner:refObject create layout ref? false',
2506 + 'RefCheckerInner:refCallback create layout ref? false',
2507 + 'RefCheckerOuter refCallback value? true',
2508 + 'RefCheckerOuter create layout refObject? true refCallback? true',
2509 + ]);
2510 + expect(ReactNoop.getChildren()).toEqual([]);
2511 +
2512 + // Suspend the inner Suspense subtree (only inner effects should be destroyed)
2513 + ReactNoop.act(() => {
2514 + ReactNoop.renderLegacySyncRoot(
2515 + <App children={<AsyncText text="Async" ms={1000} />} />,
2516 + );
2517 + });
2518 + await advanceTimers(1000);
2519 + expect(Scheduler).toHaveYielded([
2520 + 'App render',
2521 + 'Suspend:Async',
2522 + 'RefCheckerOuter render',
2523 + 'ClassComponent:refObject render',
2524 + 'RefCheckerInner:refObject render',
2525 + 'ClassComponent:refCallback render',
2526 + 'RefCheckerInner:refCallback render',
2527 + 'Text:Fallback render',
2528 + 'Text:Fallback create layout',
2529 + 'Text:Fallback create passive',
2530 + ]);
2531 + expect(ReactNoop.getChildren()).toEqual([span('Fallback')]);
2532 +
2533 + // Resolving the suspended resource should re-create inner layout effects.
2534 + await ReactNoop.act(async () => {
2535 + await resolveText('Async');
2536 + });
2537 + expect(Scheduler).toHaveYielded([
2538 + 'AsyncText:Async render',
2539 + 'Text:Fallback destroy layout',
2540 + 'AsyncText:Async create layout',
2541 + 'Text:Fallback destroy passive',
2542 + 'AsyncText:Async create passive',
2543 + ]);
2544 + expect(ReactNoop.getChildren()).toEqual([span('Async')]);
2545 +
2546 + await ReactNoop.act(async () => {
2547 + ReactNoop.renderLegacySyncRoot(null);
2548 + });
2549 + expect(Scheduler).toHaveYielded([
2550 + 'AsyncText:Async destroy layout',
2551 + 'RefCheckerOuter destroy layout refObject? true refCallback? true',
2552 + 'RefCheckerInner:refObject destroy layout ref? false',
2553 + 'RefCheckerOuter refCallback value? false',
2554 + 'RefCheckerInner:refCallback destroy layout ref? false',
2555 + 'AsyncText:Async destroy passive',
2556 + ]);
2557 + expect(ReactNoop.getChildren()).toEqual([]);
2558 + });
2559 +
2560 + // @gate enableSuspenseLayoutEffectSemantics
2561 + // @gate enableCache
2562 + it('should be cleared and reset for host components', async () => {
2563 + function App({children}) {
2564 + Scheduler.unstable_yieldValue(`App render`);
2565 + return (
2566 + <Suspense fallback={<Text text="Fallback" />}>
2567 + {children}
2568 + <RefCheckerOuter Component="span" />
2569 + </Suspense>
2570 + );
2571 + }
2572 +
2573 + // Mount
2574 + await ReactNoop.act(async () => {
2575 + ReactNoop.render(<App />);
2576 + });
2577 + expect(Scheduler).toHaveYielded([
2578 + 'App render',
2579 + 'RefCheckerOuter render',
2580 + 'RefCheckerInner:refObject render',
2581 + 'RefCheckerInner:refCallback render',
2582 + 'RefCheckerInner:refObject create layout ref? false',
2583 + 'RefCheckerInner:refCallback create layout ref? false',
2584 + 'RefCheckerOuter refCallback value? true',
2585 + 'RefCheckerOuter create layout refObject? true refCallback? true',
2586 + ]);
2587 + expect(ReactNoop.getChildren()).toEqual([
2588 + span('refObject'),
2589 + span('refCallback'),
2590 + ]);
2591 +
2592 + // Suspend the inner Suspense subtree (only inner effects should be destroyed)
2593 + ReactNoop.act(() => {
2594 + ReactNoop.render(
2595 + <App children={<AsyncText text="Async" ms={1000} />} />,
2596 + );
2597 + });
2598 + await advanceTimers(1000);
2599 + expect(Scheduler).toHaveYielded([
2600 + 'App render',
2601 + 'Suspend:Async',
2602 + 'RefCheckerOuter render',
2603 + 'RefCheckerInner:refObject render',
2604 + 'RefCheckerInner:refCallback render',
2605 + 'Text:Fallback render',
2606 + 'RefCheckerOuter destroy layout refObject? true refCallback? true',
2607 + 'RefCheckerInner:refObject destroy layout ref? false',
2608 + 'RefCheckerOuter refCallback value? false',
2609 + 'RefCheckerInner:refCallback destroy layout ref? false',
2610 + 'Text:Fallback create layout',
2611 + ]);
2612 + expect(ReactNoop.getChildren()).toEqual([
2613 + spanHidden('refObject'),
2614 + spanHidden('refCallback'),
2615 + span('Fallback'),
2616 + ]);
2617 +
2618 + // Resolving the suspended resource should re-create inner layout effects.
2619 + await ReactNoop.act(async () => {
2620 + await resolveText('Async');
2621 + });
2622 + expect(Scheduler).toHaveYielded([
2623 + 'Text:Fallback create passive',
2624 + 'AsyncText:Async render',
2625 + 'RefCheckerOuter render',
2626 + 'RefCheckerInner:refObject render',
2627 + 'RefCheckerInner:refCallback render',
2628 + 'Text:Fallback destroy layout',
2629 + 'AsyncText:Async create layout',
2630 + 'RefCheckerInner:refObject create layout ref? false',
2631 + 'RefCheckerInner:refCallback create layout ref? false',
2632 + 'RefCheckerOuter refCallback value? true',
2633 + 'RefCheckerOuter create layout refObject? true refCallback? true',
2634 + 'Text:Fallback destroy passive',
2635 + 'AsyncText:Async create passive',
2636 + ]);
2637 + expect(ReactNoop.getChildren()).toEqual([
2638 + span('Async'),
2639 + span('refObject'),
2640 + span('refCallback'),
2641 + ]);
2642 +
2643 + await ReactNoop.act(async () => {
2644 + ReactNoop.render(null);
2645 + });
2646 + expect(Scheduler).toHaveYielded([
2647 + 'AsyncText:Async destroy layout',
2648 + 'RefCheckerOuter destroy layout refObject? true refCallback? true',
2649 + 'RefCheckerInner:refObject destroy layout ref? false',
2650 + 'RefCheckerOuter refCallback value? false',
2651 + 'RefCheckerInner:refCallback destroy layout ref? false',
2652 + 'AsyncText:Async destroy passive',
2653 + ]);
2654 + expect(ReactNoop.getChildren()).toEqual([]);
2655 + });
2656 +
2657 + // @gate enableSuspenseLayoutEffectSemantics
2658 + // @gate enableCache
2659 + it('should be cleared and reset for class components', async () => {
2660 + class ClassComponent extends React.Component {
2661 + render() {
2662 + Scheduler.unstable_yieldValue(
2663 + `ClassComponent:${this.props.prop} render`,
2664 + );
2665 + return this.props.children;
2666 + }
2667 + }
2668 +
2669 + function App({children}) {
2670 + Scheduler.unstable_yieldValue(`App render`);
2671 + return (
2672 + <Suspense fallback={<Text text="Fallback" />}>
2673 + {children}
2674 + <RefCheckerOuter Component={ClassComponent} />
2675 + </Suspense>
2676 + );
2677 + }
2678 +
2679 + // Mount
2680 + await ReactNoop.act(async () => {
2681 + ReactNoop.render(<App />);
2682 + });
2683 + expect(Scheduler).toHaveYielded([
2684 + 'App render',
2685 + 'RefCheckerOuter render',
2686 + 'ClassComponent:refObject render',
2687 + 'RefCheckerInner:refObject render',
2688 + 'ClassComponent:refCallback render',
2689 + 'RefCheckerInner:refCallback render',
2690 + 'RefCheckerInner:refObject create layout ref? false',
2691 + 'RefCheckerInner:refCallback create layout ref? false',
2692 + 'RefCheckerOuter refCallback value? true',
2693 + 'RefCheckerOuter create layout refObject? true refCallback? true',
2694 + ]);
2695 + expect(ReactNoop.getChildren()).toEqual([]);
2696 +
2697 + // Suspend the inner Suspense subtree (only inner effects should be destroyed)
2698 + ReactNoop.act(() => {
2699 + ReactNoop.render(
2700 + <App children={<AsyncText text="Async" ms={1000} />} />,
2701 + );
2702 + });
2703 + await advanceTimers(1000);
2704 + expect(Scheduler).toHaveYielded([
2705 + 'App render',
2706 + 'Suspend:Async',
2707 + 'RefCheckerOuter render',
2708 + 'ClassComponent:refObject render',
2709 + 'RefCheckerInner:refObject render',
2710 + 'ClassComponent:refCallback render',
2711 + 'RefCheckerInner:refCallback render',
2712 + 'Text:Fallback render',
2713 + 'RefCheckerOuter destroy layout refObject? true refCallback? true',
2714 + 'RefCheckerInner:refObject destroy layout ref? false',
2715 + 'RefCheckerOuter refCallback value? false',
2716 + 'RefCheckerInner:refCallback destroy layout ref? false',
2717 + 'Text:Fallback create layout',
2718 + ]);
2719 + expect(ReactNoop.getChildren()).toEqual([span('Fallback')]);
2720 +
2721 + // Resolving the suspended resource should re-create inner layout effects.
2722 + await ReactNoop.act(async () => {
2723 + await resolveText('Async');
2724 + });
2725 + expect(Scheduler).toHaveYielded([
2726 + 'Text:Fallback create passive',
2727 + 'AsyncText:Async render',
2728 + 'RefCheckerOuter render',
2729 + 'ClassComponent:refObject render',
2730 + 'RefCheckerInner:refObject render',
2731 + 'ClassComponent:refCallback render',
2732 + 'RefCheckerInner:refCallback render',
2733 + 'Text:Fallback destroy layout',
2734 + 'AsyncText:Async create layout',
2735 + 'RefCheckerInner:refObject create layout ref? false',
2736 + 'RefCheckerInner:refCallback create layout ref? false',
2737 + 'RefCheckerOuter refCallback value? true',
2738 + 'RefCheckerOuter create layout refObject? true refCallback? true',
2739 + 'Text:Fallback destroy passive',
2740 + 'AsyncText:Async create passive',
2741 + ]);
2742 + expect(ReactNoop.getChildren()).toEqual([span('Async')]);
2743 +
2744 + await ReactNoop.act(async () => {
2745 + ReactNoop.render(null);
2746 + });
2747 + expect(Scheduler).toHaveYielded([
2748 + 'AsyncText:Async destroy layout',
2749 + 'RefCheckerOuter destroy layout refObject? true refCallback? true',
2750 + 'RefCheckerInner:refObject destroy layout ref? false',
2751 + 'RefCheckerOuter refCallback value? false',
2752 + 'RefCheckerInner:refCallback destroy layout ref? false',
2753 + 'AsyncText:Async destroy passive',
2754 + ]);
2755 + expect(ReactNoop.getChildren()).toEqual([]);
2756 + });
2757 +
2758 + // @gate enableSuspenseLayoutEffectSemantics
2759 + // @gate enableCache
2760 + it('should be cleared and reset for function components with useImperativeHandle', async () => {
2761 + const FunctionComponent = React.forwardRef((props, ref) => {
2762 + Scheduler.unstable_yieldValue('FunctionComponent render');
2763 + React.useImperativeHandle(
2764 + ref,
2765 + () => ({
2766 + // Noop
2767 + }),
2768 + [],
2769 + );
2770 + return props.children;
2771 + });
2772 + FunctionComponent.displayName = 'FunctionComponent';
2773 +
2774 + function App({children}) {
2775 + Scheduler.unstable_yieldValue(`App render`);
2776 + return (
2777 + <Suspense fallback={<Text text="Fallback" />}>
2778 + {children}
2779 + <RefCheckerOuter Component={FunctionComponent} />
2780 + </Suspense>
2781 + );
2782 + }
2783 +
2784 + // Mount
2785 + await ReactNoop.act(async () => {
2786 + ReactNoop.render(<App />);
2787 + });
2788 + expect(Scheduler).toHaveYielded([
2789 + 'App render',
2790 + 'RefCheckerOuter render',
2791 + 'FunctionComponent render',
2792 + 'RefCheckerInner:refObject render',
2793 + 'FunctionComponent render',
2794 + 'RefCheckerInner:refCallback render',
2795 + 'RefCheckerInner:refObject create layout ref? false',
2796 + 'RefCheckerInner:refCallback create layout ref? false',
2797 + 'RefCheckerOuter refCallback value? true',
2798 + 'RefCheckerOuter create layout refObject? true refCallback? true',
2799 + ]);
2800 + expect(ReactNoop.getChildren()).toEqual([]);
2801 +
2802 + // Suspend the inner Suspense subtree (only inner effects should be destroyed)
2803 + ReactNoop.act(() => {
2804 + ReactNoop.render(
2805 + <App children={<AsyncText text="Async" ms={1000} />} />,
2806 + );
2807 + });
2808 + await advanceTimers(1000);
2809 + expect(Scheduler).toHaveYielded([
2810 + 'App render',
2811 + 'Suspend:Async',
2812 + 'RefCheckerOuter render',
2813 + 'FunctionComponent render',
2814 + 'RefCheckerInner:refObject render',
2815 + 'FunctionComponent render',
2816 + 'RefCheckerInner:refCallback render',
2817 + 'Text:Fallback render',
2818 + 'RefCheckerOuter destroy layout refObject? true refCallback? true',
2819 + 'RefCheckerInner:refObject destroy layout ref? false',
2820 + 'RefCheckerOuter refCallback value? false',
2821 + 'RefCheckerInner:refCallback destroy layout ref? false',
2822 + 'Text:Fallback create layout',
2823 + ]);
2824 + expect(ReactNoop.getChildren()).toEqual([span('Fallback')]);
2825 +
2826 + // Resolving the suspended resource should re-create inner layout effects.
2827 + await ReactNoop.act(async () => {
2828 + await resolveText('Async');
2829 + });
2830 + expect(Scheduler).toHaveYielded([
2831 + 'Text:Fallback create passive',
2832 + 'AsyncText:Async render',
2833 + 'RefCheckerOuter render',
2834 + 'FunctionComponent render',
2835 + 'RefCheckerInner:refObject render',
2836 + 'FunctionComponent render',
2837 + 'RefCheckerInner:refCallback render',
2838 + 'Text:Fallback destroy layout',
2839 + 'AsyncText:Async create layout',
2840 + 'RefCheckerInner:refObject create layout ref? false',
2841 + 'RefCheckerInner:refCallback create layout ref? false',
2842 + 'RefCheckerOuter refCallback value? true',
2843 + 'RefCheckerOuter create layout refObject? true refCallback? true',
2844 + 'Text:Fallback destroy passive',
2845 + 'AsyncText:Async create passive',
2846 + ]);
2847 + expect(ReactNoop.getChildren()).toEqual([span('Async')]);
2848 +
2849 + await ReactNoop.act(async () => {
2850 + ReactNoop.render(null);
2851 + });
2852 + expect(Scheduler).toHaveYielded([
2853 + 'AsyncText:Async destroy layout',
2854 + 'RefCheckerOuter destroy layout refObject? true refCallback? true',
2855 + 'RefCheckerInner:refObject destroy layout ref? false',
2856 + 'RefCheckerOuter refCallback value? false',
2857 + 'RefCheckerInner:refCallback destroy layout ref? false',
2858 + 'AsyncText:Async destroy passive',
2859 + ]);
2860 + expect(ReactNoop.getChildren()).toEqual([]);
2861 + });
2862 +
2863 + // @gate enableSuspenseLayoutEffectSemantics
2864 + // @gate enableCache
2865 + it('should not reset for user-managed values', async () => {
2866 + function RefChecker({forwardedRef}) {
2867 + Scheduler.unstable_yieldValue(`RefChecker render`);
2868 + React.useLayoutEffect(() => {
2869 + Scheduler.unstable_yieldValue(
2870 + `RefChecker create layout ref? ${forwardedRef.current === 'test'}`,
2871 + );
2872 + return () => {
2873 + Scheduler.unstable_yieldValue(
2874 + `RefChecker destroy layout ref? ${forwardedRef.current ===
2875 + 'test'}`,
2876 + );
2877 + };
2878 + }, []);
2879 + return null;
2880 + }
2881 +
2882 + function App({children = null}) {
2883 + const ref = React.useRef('test');
2884 + Scheduler.unstable_yieldValue(`App render`);
2885 + React.useLayoutEffect(() => {
2886 + Scheduler.unstable_yieldValue(
2887 + `App create layout ref? ${ref.current === 'test'}`,
2888 + );
2889 + return () => {
2890 + Scheduler.unstable_yieldValue(
2891 + `App destroy layout ref? ${ref.current === 'test'}`,
2892 + );
2893 + };
2894 + }, []);
2895 + return (
2896 + <Suspense fallback={<Text text="Fallback" />}>
2897 + {children}
2898 + <RefChecker forwardedRef={ref} />
2899 + </Suspense>
2900 + );
2901 + }
2902 +
2903 + // Mount
2904 + await ReactNoop.act(async () => {
2905 + ReactNoop.render(<App />);
2906 + });
2907 + expect(Scheduler).toHaveYielded([
2908 + 'App render',
2909 + 'RefChecker render',
2910 + 'RefChecker create layout ref? true',
2911 + 'App create layout ref? true',
2912 + ]);
2913 + expect(ReactNoop.getChildren()).toEqual([]);
2914 +
2915 + // Suspend the inner Suspense subtree (only inner effects should be destroyed)
2916 + ReactNoop.act(() => {
2917 + ReactNoop.render(
2918 + <App children={<AsyncText text="Async" ms={1000} />} />,
2919 + );
2920 + });
2921 + await advanceTimers(1000);
2922 + expect(Scheduler).toHaveYielded([
2923 + 'App render',
2924 + 'Suspend:Async',
2925 + 'RefChecker render',
2926 + 'Text:Fallback render',
2927 + 'RefChecker destroy layout ref? true',
2928 + 'Text:Fallback create layout',
2929 + ]);
2930 + expect(ReactNoop.getChildren()).toEqual([span('Fallback')]);
2931 +
2932 + // Resolving the suspended resource should re-create inner layout effects.
2933 + await ReactNoop.act(async () => {
2934 + await resolveText('Async');
2935 + });
2936 + expect(Scheduler).toHaveYielded([
2937 + 'Text:Fallback create passive',
2938 + 'AsyncText:Async render',
2939 + 'RefChecker render',
2940 + 'Text:Fallback destroy layout',
2941 + 'AsyncText:Async create layout',
2942 + 'RefChecker create layout ref? true',
2943 + 'Text:Fallback destroy passive',
2944 + 'AsyncText:Async create passive',
2945 + ]);
2946 + expect(ReactNoop.getChildren()).toEqual([span('Async')]);
2947 +
2948 + await ReactNoop.act(async () => {
2949 + ReactNoop.render(null);
2950 + });
2951 + expect(Scheduler).toHaveYielded([
2952 + 'App destroy layout ref? true',
2953 + 'AsyncText:Async destroy layout',
2954 + 'RefChecker destroy layout ref? true',
2955 + 'AsyncText:Async destroy passive',
2956 + ]);
2957 + expect(ReactNoop.getChildren()).toEqual([]);
2958 + });
2959 +
2960 + describe('that throw errors', () => {
2961 + // @gate enableSuspenseLayoutEffectSemantics
2962 + // @gate enableCache
2963 + // @gate replayFailedUnitOfWorkWithInvokeGuardedCallback
2964 + it('are properly handled in ref callbacks', async () => {
2965 + let useRefCallbackShouldThrow = false;
2966 +
2967 + function ThrowsInRefCallback() {
2968 + Scheduler.unstable_yieldValue('ThrowsInRefCallback render');
2969 + const refCallback = React.useCallback(value => {
2970 + Scheduler.unstable_yieldValue(
2971 + 'ThrowsInRefCallback refCallback ref? ' + !!value,
2972 + );
2973 + if (useRefCallbackShouldThrow) {
2974 + throw Error('expected');
2975 + }
2976 + }, []);
2977 + return <span ref={refCallback} prop="ThrowsInRefCallback" />;
2978 + }
2979 +
2980 + function App({children = null}) {
2981 + Scheduler.unstable_yieldValue('App render');
2982 + React.useLayoutEffect(() => {
2983 + Scheduler.unstable_yieldValue('App create layout');
2984 + return () => {
2985 + Scheduler.unstable_yieldValue('App destroy layout');
2986 + };
2987 + }, []);
2988 + return (
2989 + <>
2990 + <Suspense fallback={<Text text="Fallback" />}>
2991 + {children}
2992 + <ThrowsInRefCallback />
2993 + <Text text="Inside" />
2994 + </Suspense>
2995 + <Text text="Outside" />
2996 + </>
2997 + );
2998 + }
2999 +
3000 + await ReactNoop.act(async () => {
3001 + ReactNoop.render(
3002 + <ErrorBoundary fallback={<Text text="Error" />}>
3003 + <App />
3004 + </ErrorBoundary>,
3005 + );
3006 + });
3007 + expect(Scheduler).toHaveYielded([
3008 + 'ErrorBoundary render: try',
3009 + 'App render',
3010 + 'ThrowsInRefCallback render',
3011 + 'Text:Inside render',
3012 + 'Text:Outside render',
3013 + 'ThrowsInRefCallback refCallback ref? true',
3014 + 'Text:Inside create layout',
3015 + 'Text:Outside create layout',
3016 + 'App create layout',
3017 + 'Text:Inside create passive',
3018 + 'Text:Outside create passive',
3019 + ]);
3020 + expect(ReactNoop.getChildren()).toEqual([
3021 + span('ThrowsInRefCallback'),
3022 + span('Inside'),
3023 + span('Outside'),
3024 + ]);
3025 +
3026 + // Schedule an update that causes React to suspend.
3027 + await ReactNoop.act(async () => {
3028 + ReactNoop.render(
3029 + <ErrorBoundary fallback={<Text text="Error" />}>
3030 + <App>
3031 + <AsyncText text="Async" ms={1000} />
3032 + </App>
3033 + </ErrorBoundary>,
3034 + );
3035 + });
3036 + expect(Scheduler).toHaveYielded([
3037 + 'ErrorBoundary render: try',
3038 + 'App render',
3039 + 'Suspend:Async',
3040 + 'ThrowsInRefCallback render',
3041 + 'Text:Inside render',
3042 + 'Text:Fallback render',
3043 + 'Text:Outside render',
3044 + 'ThrowsInRefCallback refCallback ref? false',
3045 + 'Text:Inside destroy layout',
3046 + 'Text:Fallback create layout',
3047 + 'Text:Fallback create passive',
3048 + ]);
3049 + expect(ReactNoop.getChildren()).toEqual([
3050 + spanHidden('ThrowsInRefCallback'),
3051 + spanHidden('Inside'),
3052 + span('Fallback'),
3053 + span('Outside'),
3054 + ]);
3055 +
3056 + // Resolve the pending suspense and throw
3057 + useRefCallbackShouldThrow = true;
3058 + await ReactNoop.act(async () => {
3059 + await resolveText('Async');
3060 + });
3061 + expect(Scheduler).toHaveYielded([
3062 + 'AsyncText:Async render',
3063 + 'ThrowsInRefCallback render',
3064 + 'Text:Inside render',
3065 +
3066 + // Even though an error was thrown in refCallback,
3067 + // subsequent layout effects should still be created.
3068 + 'Text:Fallback destroy layout',
3069 + 'AsyncText:Async create layout',
3070 + 'ThrowsInRefCallback refCallback ref? true',
3071 + 'Text:Inside create layout',
3072 +
3073 + // Finish the in-progress commit
3074 + 'Text:Fallback destroy passive',
3075 + 'AsyncText:Async create passive',
3076 +
3077 + // Destroy layout and passive effects in the errored tree.
3078 + 'App destroy layout',
3079 + 'AsyncText:Async destroy layout',
3080 + 'ThrowsInRefCallback refCallback ref? false',
3081 + 'Text:Inside destroy layout',
3082 + 'Text:Outside destroy layout',
3083 + 'AsyncText:Async destroy passive',
3084 + 'Text:Inside destroy passive',
3085 + 'Text:Outside destroy passive',
3086 +
3087 + // Render fallback
3088 + 'ErrorBoundary render: catch',
3089 + 'Text:Error render',
3090 + 'Text:Error create layout',
3091 + 'Text:Error create passive',
3092 + ]);
3093 + expect(ReactNoop.getChildren()).toEqual([span('Error')]);
3094 + });
3095 + });
3096 + });
3097 +});
packages/react-reconciler/src/__tests__/ReactSuspenseFuzz-test.internal.js
+48 -14
@@ -100,7 +100,7 @@ describe('ReactSuspenseFuzz', () => {
100 }
101 }, [updates]);
102
103 - const fullText = `${text}:${step}`;
103 + const fullText = `[${text}:${step}]`;
104
105 const shouldSuspend = useContext(ShouldSuspendContext);
106
@@ -163,19 +163,26 @@ describe('ReactSuspenseFuzz', () => {
163 resolveAllTasks();
164 const expectedOutput = expectedRoot.getChildrenAsJSX();
165
166 - resetCache();
167 - ReactNoop.renderLegacySyncRoot(children);
168 - resolveAllTasks();
169 - const legacyOutput = ReactNoop.getChildrenAsJSX();
170 - expect(legacyOutput).toEqual(expectedOutput);
171 - ReactNoop.renderLegacySyncRoot(null);
172 -
173 - resetCache();
174 - const concurrentRoot = ReactNoop.createRoot();
175 - concurrentRoot.render(children);
176 - resolveAllTasks();
177 - const concurrentOutput = concurrentRoot.getChildrenAsJSX();
178 - expect(concurrentOutput).toEqual(expectedOutput);
166 + gate(flags => {
167 + resetCache();
168 + ReactNoop.renderLegacySyncRoot(children);
169 + resolveAllTasks();
170 + const legacyOutput = ReactNoop.getChildrenAsJSX();
171 + expect(legacyOutput).toEqual(expectedOutput);
172 + ReactNoop.renderLegacySyncRoot(null);
173 +
174 + // Observable behavior differs here in a way that's expected:
175 + // If enableSuspenseLayoutEffectSemantics is enabled, layout effects are destroyed on re-suspend
176 + // before larger 'beginAfter' timers have a chance to fire.
177 + if (!flags.enableSuspenseLayoutEffectSemantics) {
178 + resetCache();
179 + const concurrentRoot = ReactNoop.createRoot();
180 + concurrentRoot.render(children);
181 + resolveAllTasks();
182 + const concurrentOutput = concurrentRoot.getChildrenAsJSX();
183 + expect(concurrentOutput).toEqual(expectedOutput);
184 + }
185 + });
186 }
187
188 function pickRandomWeighted(rand, options) {
@@ -410,5 +417,32 @@ Random seed is ${SEED}
417 </>,
418 );
419 });
420 +
421 + it('4', () => {
422 + const {Text, testResolvedOutput} = createFuzzer();
423 + testResolvedOutput(
424 + <React.Suspense fallback="Loading...">
425 + <React.Suspense>
426 + <React.Suspense>
427 + <Text initialDelay={9683} text="E" updates={[]} />
428 + </React.Suspense>
429 + <Text
430 + initialDelay={4053}
431 + text="C"
432 + updates={[
433 + {
434 + beginAfter: 1566,
435 + suspendFor: 4142,
436 + },
437 + {
438 + beginAfter: 9572,
439 + suspendFor: 4832,
440 + },
441 + ]}
442 + />
443 + </React.Suspense>
444 + </React.Suspense>,
445 + );
446 + });
447 });
448 });
packages/react-reconciler/src/__tests__/ReactSuspenseWithNoopRenderer-test.js
+21 -10
@@ -468,19 +468,30 @@ describe('ReactSuspenseWithNoopRenderer', () => {
468
469 await rejectText('Result', new Error('Failed to load: Result'));
470
471 - expect(Scheduler).toFlushAndYield([
472 - 'Error! [Result]',
471 + gate(flags => {
472 + if (flags.enableSuspenseLayoutEffectSemantics) {
473 + expect(Scheduler).toFlushAndYield([
474 + 'Error! [Result]',
475
474 - // React retries one more time
475 - 'Error! [Result]',
476 + // React retries one more time
477 + 'Error! [Result]',
478 + ]);
479 + expect(ReactNoop.getChildren()).toEqual([]);
480 + } else {
481 + expect(Scheduler).toFlushAndYield([
482 + 'Error! [Result]',
483
477 - // Errored again on retry. Now handle it.
484 + // React retries one more time
485 + 'Error! [Result]',
486
479 - 'Caught error: Failed to load: Result',
480 - ]);
481 - expect(ReactNoop.getChildren()).toEqual([
482 - span('Caught error: Failed to load: Result'),
483 - ]);
487 + // Errored again on retry. Now handle it.
488 + 'Caught error: Failed to load: Result',
489 + ]);
490 + expect(ReactNoop.getChildren()).toEqual([
491 + span('Caught error: Failed to load: Result'),
492 + ]);
493 + }
494 + });
495 });
496
497 // @gate enableCache
packages/shared/ReactFeatureFlags.js
+5
@@ -125,6 +125,11 @@ export const skipUnmountedBoundaries = false;
125 // aggressiveness.
126 export const deletedTreeCleanUpLevel = 1;
127
128 +// Destroy layout effects for components that are hidden because something suspended in an update
129 +// and recreate them when they are shown again (after the suspended boundary has resolved).
130 +// Note that this should be an uncommon use case and can be avoided by using the transition API.
131 +export const enableSuspenseLayoutEffectSemantics = false;
132 +
133 // --------------------------
134 // Future APIs to be deprecated
135 // --------------------------
packages/shared/forks/ReactFeatureFlags.native-fb.js
+1
@@ -47,6 +47,7 @@ export const enableFilterEmptyStringAttributesDOM = false;
47 export const disableNativeComponentFrames = false;
48 export const skipUnmountedBoundaries = false;
49 export const deletedTreeCleanUpLevel = 1;
50 +export const enableSuspenseLayoutEffectSemantics = false;
51
52 export const enableNewReconciler = false;
53 export const deferRenderPhaseUpdateToNextBatch = true;
packages/shared/forks/ReactFeatureFlags.native-oss.js
+1
@@ -46,6 +46,7 @@ export const enableFilterEmptyStringAttributesDOM = false;
46 export const disableNativeComponentFrames = false;
47 export const skipUnmountedBoundaries = false;
48 export const deletedTreeCleanUpLevel = 1;
49 +export const enableSuspenseLayoutEffectSemantics = false;
50
51 export const enableNewReconciler = false;
52 export const deferRenderPhaseUpdateToNextBatch = true;
packages/shared/forks/ReactFeatureFlags.test-renderer.js
+1
@@ -46,6 +46,7 @@ export const enableFilterEmptyStringAttributesDOM = false;
46 export const disableNativeComponentFrames = false;
47 export const skipUnmountedBoundaries = false;
48 export const deletedTreeCleanUpLevel = 1;
49 +export const enableSuspenseLayoutEffectSemantics = false;
50
51 export const enableNewReconciler = false;
52 export const deferRenderPhaseUpdateToNextBatch = true;
packages/shared/forks/ReactFeatureFlags.test-renderer.native.js
+1
@@ -46,6 +46,7 @@ export const enableFilterEmptyStringAttributesDOM = false;
46 export const disableNativeComponentFrames = false;
47 export const skipUnmountedBoundaries = false;
48 export const deletedTreeCleanUpLevel = 1;
49 +export const enableSuspenseLayoutEffectSemantics = false;
50
51 export const enableNewReconciler = false;
52 export const deferRenderPhaseUpdateToNextBatch = true;
packages/shared/forks/ReactFeatureFlags.test-renderer.www.js
+1
@@ -46,6 +46,7 @@ export const enableFilterEmptyStringAttributesDOM = false;
46 export const disableNativeComponentFrames = false;
47 export const skipUnmountedBoundaries = false;
48 export const deletedTreeCleanUpLevel = 1;
49 +export const enableSuspenseLayoutEffectSemantics = false;
50
51 export const enableNewReconciler = false;
52 export const deferRenderPhaseUpdateToNextBatch = true;
packages/shared/forks/ReactFeatureFlags.testing.js
+1
@@ -46,6 +46,7 @@ export const enableFilterEmptyStringAttributesDOM = false;
46 export const disableNativeComponentFrames = false;
47 export const skipUnmountedBoundaries = false;
48 export const deletedTreeCleanUpLevel = 1;
49 +export const enableSuspenseLayoutEffectSemantics = false;
50
51 export const enableNewReconciler = false;
52 export const deferRenderPhaseUpdateToNextBatch = true;
packages/shared/forks/ReactFeatureFlags.testing.www.js
+1
@@ -46,6 +46,7 @@ export const enableFilterEmptyStringAttributesDOM = false;
46 export const disableNativeComponentFrames = false;
47 export const skipUnmountedBoundaries = true;
48 export const deletedTreeCleanUpLevel = 1;
49 +export const enableSuspenseLayoutEffectSemantics = false;
50
51 export const enableNewReconciler = false;
52 export const deferRenderPhaseUpdateToNextBatch = true;
packages/shared/forks/ReactFeatureFlags.www-dynamic.js
+1
@@ -18,6 +18,7 @@ export const disableInputAttributeSyncing = __VARIANT__;
18 export const enableFilterEmptyStringAttributesDOM = __VARIANT__;
19 export const enableLegacyFBSupport = __VARIANT__;
20 export const skipUnmountedBoundaries = __VARIANT__;
21 +export const enableSuspenseLayoutEffectSemantics = __VARIANT__;
22
23 // Enable this flag to help with concurrent mode debugging.
24 // It logs information to the console about React scheduling, rendering, and commit phases.
packages/shared/forks/ReactFeatureFlags.www.js
+1
@@ -28,6 +28,7 @@ export const {
28 skipUnmountedBoundaries,
29 enableStrictEffects,
30 createRootStrictEffectsByDefault,
31 + enableSuspenseLayoutEffectSemantics,
32 enableUseRefAccessWarning,
33 disableNativeComponentFrames,
34 disableSchedulerTimeoutInWorkLoop,