Land forked reconciler changes (#24878)
This applies forked changes from the "new" reconciler to the "old" one. Includes: - 67de5e3 [FORKED] Hidden trees should capture Suspense - 6ab05ee [FORKED] Track nearest Suspense handler on stack - 051ac55 [FORKED] Add HiddenContext to track if subtree is hidden
Andrew Clark committed
Jul 8, 2022 at 11:55 UTC
30eb267abdb032bf78e6394bd9ad79102e1d3fb1
11 files changed
+634
-386
packages/react-reconciler/src/ReactFiberBeginWork.old.js
+223
-136
@@ -38,7 +38,6 @@ import type {UpdateQueue} from './ReactFiberClassUpdateQueue.old';
38
import type {RootState} from './ReactFiberRoot.old';
39
import type {TracingMarkerInstance} from './ReactFiberTracingMarkerComponent.old';
40
import {
41
- enableSuspenseAvoidThisFallback,
41
enableCPUSuspense,
42
enableUseMutableSource,
43
} from 'shared/ReactFeatureFlags';
@@ -168,14 +167,21 @@ import {shouldError, shouldSuspend} from './ReactFiberReconciler';
167
import {pushHostContext, pushHostContainer} from './ReactFiberHostContext.old';
168
import {
169
suspenseStackCursor,
171
- pushSuspenseContext,
172
- InvisibleParentSuspenseContext,
170
+ pushSuspenseListContext,
171
ForceSuspenseFallback,
174
- hasSuspenseContext,
175
- setDefaultShallowSuspenseContext,
176
- addSubtreeSuspenseContext,
177
- setShallowSuspenseContext,
172
+ hasSuspenseListContext,
173
+ setDefaultShallowSuspenseListContext,
174
+ setShallowSuspenseListContext,
175
+ pushPrimaryTreeSuspenseHandler,
176
+ pushFallbackTreeSuspenseHandler,
177
+ pushOffscreenSuspenseHandler,
178
+ reuseSuspenseHandlerOnStack,
179
+ popSuspenseHandler,
180
} from './ReactFiberSuspenseContext.old';
181
+import {
182
+ pushHiddenContext,
183
+ reuseHiddenContextOnStack,
184
+} from './ReactFiberHiddenContext.old';
185
import {findFirstSuspended} from './ReactFiberSuspenseComponent.old';
186
import {
187
pushProvider,
@@ -233,7 +239,6 @@ import {
239
renderDidSuspendDelayIfPossible,
240
markSkippedUpdateLanes,
241
getWorkInProgressRoot,
236
- pushRenderLanes,
242
} from './ReactFiberWorkLoop.old';
243
import {enqueueConcurrentRenderForLane} from './ReactFiberConcurrentUpdates.old';
244
import {setWorkInProgressVersion} from './ReactMutableSource.old';
@@ -675,6 +680,52 @@ function updateOffscreenComponent(
680
(enableLegacyHidden && nextProps.mode === 'unstable-defer-without-hiding')
681
) {
682
// Rendering a hidden tree.
683
+
684
+ const didSuspend = (workInProgress.flags & DidCapture) !== NoFlags;
685
+ if (didSuspend) {
686
+ // Something suspended inside a hidden tree
687
+
688
+ // Include the base lanes from the last render
689
+ const nextBaseLanes =
690
+ prevState !== null
691
+ ? mergeLanes(prevState.baseLanes, renderLanes)
692
+ : renderLanes;
693
+
694
+ if (current !== null) {
695
+ // Reset to the current children
696
+ let currentChild = (workInProgress.child = current.child);
697
+
698
+ // The current render suspended, but there may be other lanes with
699
+ // pending work. We can't read `childLanes` from the current Offscreen
700
+ // fiber because we reset it when it was deferred; however, we can read
701
+ // the pending lanes from the child fibers.
702
+ let currentChildLanes = NoLanes;
703
+ while (currentChild !== null) {
704
+ currentChildLanes = mergeLanes(
705
+ mergeLanes(currentChildLanes, currentChild.lanes),
706
+ currentChild.childLanes,
707
+ );
708
+ currentChild = currentChild.sibling;
709
+ }
710
+ const lanesWeJustAttempted = nextBaseLanes;
711
+ const remainingChildLanes = removeLanes(
712
+ currentChildLanes,
713
+ lanesWeJustAttempted,
714
+ );
715
+ workInProgress.childLanes = remainingChildLanes;
716
+ } else {
717
+ workInProgress.childLanes = NoLanes;
718
+ workInProgress.child = null;
719
+ }
720
+
721
+ return deferHiddenOffscreenComponent(
722
+ current,
723
+ workInProgress,
724
+ nextBaseLanes,
725
+ renderLanes,
726
+ );
727
+ }
728
+
729
if ((workInProgress.mode & ConcurrentMode) === NoMode) {
730
// In legacy sync mode, don't defer the subtree. Render it now.
731
// TODO: Consider how Offscreen should work with transitions in the future
@@ -690,57 +741,29 @@ function updateOffscreenComponent(
741
pushTransition(workInProgress, null, null);
742
}
743
}
693
- pushRenderLanes(workInProgress, renderLanes);
744
+ reuseHiddenContextOnStack(workInProgress);
745
+ pushOffscreenSuspenseHandler(workInProgress);
746
} else if (!includesSomeLane(renderLanes, (OffscreenLane: Lane))) {
695
- let spawnedCachePool: SpawnedCachePool | null = null;
747
// We're hidden, and we're not rendering at Offscreen. We will bail out
748
// and resume this tree later.
698
- let nextBaseLanes;
699
- if (prevState !== null) {
700
- const prevBaseLanes = prevState.baseLanes;
701
- nextBaseLanes = mergeLanes(prevBaseLanes, renderLanes);
702
- if (enableCache) {
703
- // Save the cache pool so we can resume later.
704
- spawnedCachePool = getOffscreenDeferredCache();
705
- }
706
- } else {
707
- nextBaseLanes = renderLanes;
708
- }
749
710
- // Schedule this fiber to re-render at offscreen priority. Then bailout.
750
+ // Schedule this fiber to re-render at Offscreen priority
751
workInProgress.lanes = workInProgress.childLanes = laneToLanes(
752
OffscreenLane,
753
);
714
- const nextState: OffscreenState = {
715
- baseLanes: nextBaseLanes,
716
- cachePool: spawnedCachePool,
717
- };
718
- workInProgress.memoizedState = nextState;
719
- workInProgress.updateQueue = null;
720
- if (enableCache) {
721
- // push the cache pool even though we're going to bail out
722
- // because otherwise there'd be a context mismatch
723
- if (current !== null) {
724
- pushTransition(workInProgress, null, null);
725
- }
726
- }
754
728
- // We're about to bail out, but we need to push this to the stack anyway
729
- // to avoid a push/pop misalignment.
730
- pushRenderLanes(workInProgress, nextBaseLanes);
755
+ // Include the base lanes from the last render
756
+ const nextBaseLanes =
757
+ prevState !== null
758
+ ? mergeLanes(prevState.baseLanes, renderLanes)
759
+ : renderLanes;
760
732
- if (enableLazyContextPropagation && current !== null) {
733
- // Since this tree will resume rendering in a separate render, we need
734
- // to propagate parent contexts now so we don't lose track of which
735
- // ones changed.
736
- propagateParentContextChangesToDeferredTree(
737
- current,
738
- workInProgress,
739
- renderLanes,
740
- );
741
- }
742
-
743
- return null;
761
+ return deferHiddenOffscreenComponent(
762
+ current,
763
+ workInProgress,
764
+ nextBaseLanes,
765
+ renderLanes,
766
+ );
767
} else {
768
// This is the second render. The surrounding visible content has already
769
// committed. Now we resume rendering the hidden tree.
@@ -751,9 +774,6 @@ function updateOffscreenComponent(
774
cachePool: null,
775
};
776
workInProgress.memoizedState = nextState;
754
- // Push the lanes that were skipped when we bailed out.
755
- const subtreeRenderLanes =
756
- prevState !== null ? prevState.baseLanes : renderLanes;
777
if (enableCache && current !== null) {
778
// If the render that spawned this one accessed the cache pool, resume
779
// using the same cache. Unless the parent changed, since that means
@@ -764,16 +784,18 @@ function updateOffscreenComponent(
784
pushTransition(workInProgress, prevCachePool, null);
785
}
786
767
- pushRenderLanes(workInProgress, subtreeRenderLanes);
787
+ // Push the lanes that were skipped when we bailed out.
788
+ if (prevState !== null) {
789
+ pushHiddenContext(workInProgress, prevState);
790
+ } else {
791
+ reuseHiddenContextOnStack(workInProgress);
792
+ }
793
+ pushOffscreenSuspenseHandler(workInProgress);
794
}
795
} else {
796
// Rendering a visible tree.
771
- let subtreeRenderLanes;
797
if (prevState !== null) {
798
// We're going from hidden -> visible.
774
-
775
- subtreeRenderLanes = mergeLanes(prevState.baseLanes, renderLanes);
776
-
799
let prevCachePool = null;
800
if (enableCache) {
801
// If the render that spawned this one accessed the cache pool, resume
@@ -794,13 +816,16 @@ function updateOffscreenComponent(
816
817
pushTransition(workInProgress, prevCachePool, transitions);
818
819
+ // Push the lanes that were skipped when we bailed out.
820
+ pushHiddenContext(workInProgress, prevState);
821
+ reuseSuspenseHandlerOnStack(workInProgress);
822
+
823
// Since we're not hidden anymore, reset the state
824
workInProgress.memoizedState = null;
825
} else {
826
// We weren't previously hidden, and we still aren't, so there's nothing
827
// special to do. Need to push to the stack regardless, though, to avoid
828
// a push/pop misalignment.
803
- subtreeRenderLanes = renderLanes;
829
830
if (enableCache) {
831
// If the render that spawned this one accessed the cache pool, resume
@@ -810,14 +835,58 @@ function updateOffscreenComponent(
835
pushTransition(workInProgress, null, null);
836
}
837
}
838
+
839
+ // We're about to bail out, but we need to push this to the stack anyway
840
+ // to avoid a push/pop misalignment.
841
+ reuseHiddenContextOnStack(workInProgress);
842
+ reuseSuspenseHandlerOnStack(workInProgress);
843
}
814
- pushRenderLanes(workInProgress, subtreeRenderLanes);
844
}
845
846
reconcileChildren(current, workInProgress, nextChildren, renderLanes);
847
return workInProgress.child;
848
}
849
850
+function deferHiddenOffscreenComponent(
851
+ current: Fiber | null,
852
+ workInProgress: Fiber,
853
+ nextBaseLanes: Lanes,
854
+ renderLanes: Lanes,
855
+) {
856
+ const nextState: OffscreenState = {
857
+ baseLanes: nextBaseLanes,
858
+ // Save the cache pool so we can resume later.
859
+ cachePool: enableCache ? getOffscreenDeferredCache() : null,
860
+ };
861
+ workInProgress.memoizedState = nextState;
862
+ if (enableCache) {
863
+ // push the cache pool even though we're going to bail out
864
+ // because otherwise there'd be a context mismatch
865
+ if (current !== null) {
866
+ pushTransition(workInProgress, null, null);
867
+ }
868
+ }
869
+
870
+ // We're about to bail out, but we need to push this to the stack anyway
871
+ // to avoid a push/pop misalignment.
872
+ reuseHiddenContextOnStack(workInProgress);
873
+
874
+ pushOffscreenSuspenseHandler(workInProgress);
875
+
876
+ if (enableLazyContextPropagation && current !== null) {
877
+ // Since this tree will resume rendering in a separate render, we need
878
+ // to propagate parent contexts now so we don't lose track of which
879
+ // ones changed.
880
+ propagateParentContextChangesToDeferredTree(
881
+ current,
882
+ workInProgress,
883
+ renderLanes,
884
+ );
885
+ }
886
+
887
+ return null;
888
+}
889
+
890
// Note: These happen to have identical begin phases, for now. We shouldn't hold
891
// ourselves to this constraint, though. If the behavior diverges, we should
892
// fork the function.
@@ -1981,7 +2050,6 @@ function updateSuspenseOffscreenState(
2050
2051
// TODO: Probably should inline this back
2052
function shouldRemainOnFallback(
1984
- suspenseContext: SuspenseContext,
2053
current: null | Fiber,
2054
workInProgress: Fiber,
2055
renderLanes: Lanes,
@@ -2001,7 +2069,8 @@ function shouldRemainOnFallback(
2069
}
2070
2071
// Not currently showing content. Consult the Suspense context.
2004
- return hasSuspenseContext(
2072
+ const suspenseContext: SuspenseContext = suspenseStackCursor.current;
2073
+ return hasSuspenseListContext(
2074
suspenseContext,
2075
(ForceSuspenseFallback: SuspenseContext),
2076
);
@@ -2022,50 +2091,18 @@ function updateSuspenseComponent(current, workInProgress, renderLanes) {
2091
}
2092
}
2093
2025
- let suspenseContext: SuspenseContext = suspenseStackCursor.current;
2026
-
2094
let showFallback = false;
2095
const didSuspend = (workInProgress.flags & DidCapture) !== NoFlags;
2029
-
2096
if (
2097
didSuspend ||
2032
- shouldRemainOnFallback(
2033
- suspenseContext,
2034
- current,
2035
- workInProgress,
2036
- renderLanes,
2037
- )
2098
+ shouldRemainOnFallback(current, workInProgress, renderLanes)
2099
) {
2100
// Something in this boundary's subtree already suspended. Switch to
2101
// rendering the fallback children.
2102
showFallback = true;
2103
workInProgress.flags &= ~DidCapture;
2043
- } else {
2044
- // Attempting the main content
2045
- if (
2046
- current === null ||
2047
- (current.memoizedState: null | SuspenseState) !== null
2048
- ) {
2049
- // This is a new mount or this boundary is already showing a fallback state.
2050
- // Mark this subtree context as having at least one invisible parent that could
2051
- // handle the fallback state.
2052
- // Avoided boundaries are not considered since they cannot handle preferred fallback states.
2053
- if (
2054
- !enableSuspenseAvoidThisFallback ||
2055
- nextProps.unstable_avoidThisFallback !== true
2056
- ) {
2057
- suspenseContext = addSubtreeSuspenseContext(
2058
- suspenseContext,
2059
- InvisibleParentSuspenseContext,
2060
- );
2061
- }
2062
- }
2104
}
2105
2065
- suspenseContext = setDefaultShallowSuspenseContext(suspenseContext);
2066
-
2067
- pushSuspenseContext(workInProgress, suspenseContext);
2068
-
2106
// OK, the next part is confusing. We're about to reconcile the Suspense
2107
// boundary's children. This involves some custom reconciliation logic. Two
2108
// main reasons this is so complicated.
@@ -2093,24 +2130,40 @@ function updateSuspenseComponent(current, workInProgress, renderLanes) {
2130
2131
// Special path for hydration
2132
// If we're currently hydrating, try to hydrate this boundary.
2096
- tryToClaimNextHydratableInstance(workInProgress);
2097
- // This could've been a dehydrated suspense component.
2098
- const suspenseState: null | SuspenseState = workInProgress.memoizedState;
2099
- if (suspenseState !== null) {
2100
- const dehydrated = suspenseState.dehydrated;
2101
- if (dehydrated !== null) {
2102
- return mountDehydratedSuspenseComponent(
2103
- workInProgress,
2104
- dehydrated,
2105
- renderLanes,
2106
- );
2133
+ if (getIsHydrating()) {
2134
+ // We must push the suspense handler context *before* attempting to
2135
+ // hydrate, to avoid a mismatch in case it errors.
2136
+ if (showFallback) {
2137
+ pushPrimaryTreeSuspenseHandler(workInProgress);
2138
+ } else {
2139
+ pushFallbackTreeSuspenseHandler(workInProgress);
2140
+ }
2141
+ tryToClaimNextHydratableInstance(workInProgress);
2142
+ // This could've been a dehydrated suspense component.
2143
+ const suspenseState: null | SuspenseState = workInProgress.memoizedState;
2144
+ if (suspenseState !== null) {
2145
+ const dehydrated = suspenseState.dehydrated;
2146
+ if (dehydrated !== null) {
2147
+ return mountDehydratedSuspenseComponent(
2148
+ workInProgress,
2149
+ dehydrated,
2150
+ renderLanes,
2151
+ );
2152
+ }
2153
}
2154
+ // If hydration didn't succeed, fall through to the normal Suspense path.
2155
+ // To avoid a stack mismatch we need to pop the Suspense handler that we
2156
+ // pushed above. This will become less awkward when move the hydration
2157
+ // logic to its own fiber.
2158
+ popSuspenseHandler(workInProgress);
2159
}
2160
2161
const nextPrimaryChildren = nextProps.children;
2162
const nextFallbackChildren = nextProps.fallback;
2163
2164
if (showFallback) {
2165
+ pushFallbackTreeSuspenseHandler(workInProgress);
2166
+
2167
const fallbackFragment = mountSuspenseFallbackChildren(
2168
workInProgress,
2169
nextPrimaryChildren,
@@ -2125,14 +2178,19 @@ function updateSuspenseComponent(current, workInProgress, renderLanes) {
2178
if (enableTransitionTracing) {
2179
const currentTransitions = getPendingTransitions();
2180
if (currentTransitions !== null) {
2128
- // If there are no transitions, we don't need to keep track of tracing markers
2181
const parentMarkerInstances = getMarkerInstances();
2130
- const primaryChildUpdateQueue: OffscreenQueue = {
2131
- transitions: currentTransitions,
2132
- markerInstances: parentMarkerInstances,
2133
- wakeables: null,
2134
- };
2135
- primaryChildFragment.updateQueue = primaryChildUpdateQueue;
2182
+ const offscreenQueue: OffscreenQueue | null = (primaryChildFragment.updateQueue: any);
2183
+ if (offscreenQueue === null) {
2184
+ const newOffscreenQueue: OffscreenQueue = {
2185
+ transitions: currentTransitions,
2186
+ markerInstances: parentMarkerInstances,
2187
+ wakeables: null,
2188
+ };
2189
+ primaryChildFragment.updateQueue = newOffscreenQueue;
2190
+ } else {
2191
+ offscreenQueue.transitions = currentTransitions;
2192
+ offscreenQueue.markerInstances = parentMarkerInstances;
2193
+ }
2194
}
2195
}
2196
@@ -2144,6 +2202,7 @@ function updateSuspenseComponent(current, workInProgress, renderLanes) {
2202
// This is a CPU-bound tree. Skip this tree and show a placeholder to
2203
// unblock the surrounding content. Then immediately retry after the
2204
// initial commit.
2205
+ pushFallbackTreeSuspenseHandler(workInProgress);
2206
const fallbackFragment = mountSuspenseFallbackChildren(
2207
workInProgress,
2208
nextPrimaryChildren,
@@ -2156,6 +2215,8 @@ function updateSuspenseComponent(current, workInProgress, renderLanes) {
2215
);
2216
workInProgress.memoizedState = SUSPENDED_MARKER;
2217
2218
+ // TODO: Transition Tracing is not yet implemented for CPU Suspense.
2219
+
2220
// Since nothing actually suspended, there will nothing to ping this to
2221
// get it started back up to attempt the next item. While in terms of
2222
// priority this work has the same priority as this current render, it's
@@ -2167,6 +2228,7 @@ function updateSuspenseComponent(current, workInProgress, renderLanes) {
2228
workInProgress.lanes = SomeRetryLane;
2229
return fallbackFragment;
2230
} else {
2231
+ pushPrimaryTreeSuspenseHandler(workInProgress);
2232
return mountSuspensePrimaryChildren(
2233
workInProgress,
2234
nextPrimaryChildren,
@@ -2194,6 +2256,8 @@ function updateSuspenseComponent(current, workInProgress, renderLanes) {
2256
}
2257
2258
if (showFallback) {
2259
+ pushFallbackTreeSuspenseHandler(workInProgress);
2260
+
2261
const nextFallbackChildren = nextProps.fallback;
2262
const nextPrimaryChildren = nextProps.children;
2263
const fallbackChildFragment = updateSuspenseFallbackChildren(
@@ -2214,12 +2278,31 @@ function updateSuspenseComponent(current, workInProgress, renderLanes) {
2278
const currentTransitions = getPendingTransitions();
2279
if (currentTransitions !== null) {
2280
const parentMarkerInstances = getMarkerInstances();
2217
- const primaryChildUpdateQueue: OffscreenQueue = {
2218
- transitions: currentTransitions,
2219
- markerInstances: parentMarkerInstances,
2220
- wakeables: null,
2221
- };
2222
- primaryChildFragment.updateQueue = primaryChildUpdateQueue;
2281
+ const offscreenQueue: OffscreenQueue | null = (primaryChildFragment.updateQueue: any);
2282
+ const currentOffscreenQueue: OffscreenQueue | null = (current.updateQueue: any);
2283
+ if (offscreenQueue === null) {
2284
+ const newOffscreenQueue: OffscreenQueue = {
2285
+ transitions: currentTransitions,
2286
+ markerInstances: parentMarkerInstances,
2287
+ wakeables: null,
2288
+ };
2289
+ primaryChildFragment.updateQueue = newOffscreenQueue;
2290
+ } else if (offscreenQueue === currentOffscreenQueue) {
2291
+ // If the work-in-progress queue is the same object as current, we
2292
+ // can't modify it without cloning it first.
2293
+ const newOffscreenQueue: OffscreenQueue = {
2294
+ transitions: currentTransitions,
2295
+ markerInstances: parentMarkerInstances,
2296
+ wakeables:
2297
+ currentOffscreenQueue !== null
2298
+ ? currentOffscreenQueue.wakeables
2299
+ : null,
2300
+ };
2301
+ primaryChildFragment.updateQueue = newOffscreenQueue;
2302
+ } else {
2303
+ offscreenQueue.transitions = currentTransitions;
2304
+ offscreenQueue.markerInstances = parentMarkerInstances;
2305
+ }
2306
}
2307
}
2308
primaryChildFragment.childLanes = getRemainingWorkInPrimaryTree(
@@ -2229,6 +2312,8 @@ function updateSuspenseComponent(current, workInProgress, renderLanes) {
2312
workInProgress.memoizedState = SUSPENDED_MARKER;
2313
return fallbackChildFragment;
2314
} else {
2315
+ pushPrimaryTreeSuspenseHandler(workInProgress);
2316
+
2317
const nextPrimaryChildren = nextProps.children;
2318
const primaryChildFragment = updateSuspensePrimaryChildren(
2319
current,
@@ -2599,6 +2684,7 @@ function updateDehydratedSuspenseComponent(
2684
): null | Fiber {
2685
if (!didSuspend) {
2686
// This is the first render pass. Attempt to hydrate.
2687
+ pushPrimaryTreeSuspenseHandler(workInProgress);
2688
2689
// We should never be hydrating at this point because it is the first pass,
2690
// but after we've already committed once.
@@ -2765,6 +2851,8 @@ function updateDehydratedSuspenseComponent(
2851
2852
if (workInProgress.flags & ForceClientRender) {
2853
// Something errored during hydration. Try again without hydrating.
2854
+ pushPrimaryTreeSuspenseHandler(workInProgress);
2855
+
2856
workInProgress.flags &= ~ForceClientRender;
2857
const capturedValue = createCapturedValue(
2858
new Error(
@@ -2781,6 +2869,10 @@ function updateDehydratedSuspenseComponent(
2869
} else if ((workInProgress.memoizedState: null | SuspenseState) !== null) {
2870
// Something suspended and we should still be in dehydrated mode.
2871
// Leave the existing child in place.
2872
+
2873
+ // Push to avoid a mismatch
2874
+ pushFallbackTreeSuspenseHandler(workInProgress);
2875
+
2876
workInProgress.child = current.child;
2877
// The dehydrated completion pass expects this flag to be there
2878
// but the normal suspense pass doesn't.
@@ -2789,6 +2881,8 @@ function updateDehydratedSuspenseComponent(
2881
} else {
2882
// Suspended but we should no longer be in dehydrated mode.
2883
// Therefore we now have to render the fallback.
2884
+ pushFallbackTreeSuspenseHandler(workInProgress);
2885
+
2886
const nextPrimaryChildren = nextProps.children;
2887
const nextFallbackChildren = nextProps.fallback;
2888
const fallbackChildFragment = mountSuspenseFallbackAfterRetryWithoutHydrating(
@@ -3084,12 +3178,12 @@ function updateSuspenseListComponent(
3178
3179
let suspenseContext: SuspenseContext = suspenseStackCursor.current;
3180
3087
- const shouldForceFallback = hasSuspenseContext(
3181
+ const shouldForceFallback = hasSuspenseListContext(
3182
suspenseContext,
3183
(ForceSuspenseFallback: SuspenseContext),
3184
);
3185
if (shouldForceFallback) {
3092
- suspenseContext = setShallowSuspenseContext(
3186
+ suspenseContext = setShallowSuspenseListContext(
3187
suspenseContext,
3188
ForceSuspenseFallback,
3189
);
@@ -3107,9 +3201,9 @@ function updateSuspenseListComponent(
3201
renderLanes,
3202
);
3203
}
3110
- suspenseContext = setDefaultShallowSuspenseContext(suspenseContext);
3204
+ suspenseContext = setDefaultShallowSuspenseListContext(suspenseContext);
3205
}
3112
- pushSuspenseContext(workInProgress, suspenseContext);
3206
+ pushSuspenseListContext(workInProgress, suspenseContext);
3207
3208
if ((workInProgress.mode & ConcurrentMode) === NoMode) {
3209
// In legacy mode, SuspenseList doesn't work so we just
@@ -3577,10 +3671,9 @@ function attemptEarlyBailoutIfNoScheduledUpdate(
3671
const state: SuspenseState | null = workInProgress.memoizedState;
3672
if (state !== null) {
3673
if (state.dehydrated !== null) {
3580
- pushSuspenseContext(
3581
- workInProgress,
3582
- setDefaultShallowSuspenseContext(suspenseStackCursor.current),
3583
- );
3674
+ // We're not going to render the children, so this is just to maintain
3675
+ // push/pop symmetry
3676
+ pushPrimaryTreeSuspenseHandler(workInProgress);
3677
// We know that this component will suspend again because if it has
3678
// been unsuspended it has committed as a resolved Suspense component.
3679
// If it needs to be retried, it should have work scheduled on it.
@@ -3603,10 +3696,7 @@ function attemptEarlyBailoutIfNoScheduledUpdate(
3696
} else {
3697
// The primary child fragment does not have pending work marked
3698
// on it
3606
- pushSuspenseContext(
3607
- workInProgress,
3608
- setDefaultShallowSuspenseContext(suspenseStackCursor.current),
3609
- );
3699
+ pushPrimaryTreeSuspenseHandler(workInProgress);
3700
// The primary children do not have pending work with sufficient
3701
// priority. Bailout.
3702
const child = bailoutOnAlreadyFinishedWork(
@@ -3626,10 +3716,7 @@ function attemptEarlyBailoutIfNoScheduledUpdate(
3716
}
3717
}
3718
} else {
3629
- pushSuspenseContext(
3630
- workInProgress,
3631
- setDefaultShallowSuspenseContext(suspenseStackCursor.current),
3632
- );
3719
+ pushPrimaryTreeSuspenseHandler(workInProgress);
3720
}
3721
break;
3722
}
@@ -3687,7 +3774,7 @@ function attemptEarlyBailoutIfNoScheduledUpdate(
3774
renderState.tail = null;
3775
renderState.lastEffect = null;
3776
}
3690
- pushSuspenseContext(workInProgress, suspenseStackCursor.current);
3777
+ pushSuspenseListContext(workInProgress, suspenseStackCursor.current);
3778
3779
if (hasChildWork) {
3780
break;
packages/react-reconciler/src/ReactFiberCommitWork.old.js
+75
-30
@@ -1962,40 +1962,65 @@ function commitSuspenseHydrationCallbacks(
1962
}
1963
}
1964
1965
-function attachSuspenseRetryListeners(finishedWork: Fiber) {
1965
+function getRetryCache(finishedWork) {
1966
+ // TODO: Unify the interface for the retry cache so we don't have to switch
1967
+ // on the tag like this.
1968
+ switch (finishedWork.tag) {
1969
+ case SuspenseComponent:
1970
+ case SuspenseListComponent: {
1971
+ let retryCache = finishedWork.stateNode;
1972
+ if (retryCache === null) {
1973
+ retryCache = finishedWork.stateNode = new PossiblyWeakSet();
1974
+ }
1975
+ return retryCache;
1976
+ }
1977
+ case OffscreenComponent: {
1978
+ const instance: OffscreenInstance = finishedWork.stateNode;
1979
+ let retryCache = instance.retryCache;
1980
+ if (retryCache === null) {
1981
+ retryCache = instance.retryCache = new PossiblyWeakSet();
1982
+ }
1983
+ return retryCache;
1984
+ }
1985
+ default: {
1986
+ throw new Error(
1987
+ `Unexpected Suspense handler tag (${finishedWork.tag}). This is a ` +
1988
+ 'bug in React.',
1989
+ );
1990
+ }
1991
+ }
1992
+}
1993
+
1994
+function attachSuspenseRetryListeners(
1995
+ finishedWork: Fiber,
1996
+ wakeables: Set<Wakeable>,
1997
+) {
1998
// If this boundary just timed out, then it will have a set of wakeables.
1999
// For each wakeable, attach a listener so that when it resolves, React
2000
// attempts to re-render the boundary in the primary (pre-timeout) state.
1969
- const wakeables: Set<Wakeable> | null = (finishedWork.updateQueue: any);
1970
- if (wakeables !== null) {
1971
- finishedWork.updateQueue = null;
1972
- let retryCache = finishedWork.stateNode;
1973
- if (retryCache === null) {
1974
- retryCache = finishedWork.stateNode = new PossiblyWeakSet();
1975
- }
1976
- wakeables.forEach(wakeable => {
1977
- // Memoize using the boundary fiber to prevent redundant listeners.
1978
- const retry = resolveRetryWakeable.bind(null, finishedWork, wakeable);
1979
- if (!retryCache.has(wakeable)) {
1980
- retryCache.add(wakeable);
1981
-
1982
- if (enableUpdaterTracking) {
1983
- if (isDevToolsPresent) {
1984
- if (inProgressLanes !== null && inProgressRoot !== null) {
1985
- // If we have pending work still, associate the original updaters with it.
1986
- restorePendingUpdaters(inProgressRoot, inProgressLanes);
1987
- } else {
1988
- throw Error(
1989
- 'Expected finished root and lanes to be set. This is a bug in React.',
1990
- );
1991
- }
2001
+ const retryCache = getRetryCache(finishedWork);
2002
+ wakeables.forEach(wakeable => {
2003
+ // Memoize using the boundary fiber to prevent redundant listeners.
2004
+ const retry = resolveRetryWakeable.bind(null, finishedWork, wakeable);
2005
+ if (!retryCache.has(wakeable)) {
2006
+ retryCache.add(wakeable);
2007
+
2008
+ if (enableUpdaterTracking) {
2009
+ if (isDevToolsPresent) {
2010
+ if (inProgressLanes !== null && inProgressRoot !== null) {
2011
+ // If we have pending work still, associate the original updaters with it.
2012
+ restorePendingUpdaters(inProgressRoot, inProgressLanes);
2013
+ } else {
2014
+ throw Error(
2015
+ 'Expected finished root and lanes to be set. This is a bug in React.',
2016
+ );
2017
}
2018
}
1994
-
1995
- wakeable.then(retry, retry);
2019
}
1997
- });
1998
- }
2020
+
2021
+ wakeable.then(retry, retry);
2022
+ }
2023
+ });
2024
}
2025
2026
// This function detects when a Suspense boundary goes from visible to hidden.
@@ -2325,7 +2350,11 @@ function commitMutationEffectsOnFiber(
2350
} catch (error) {
2351
captureCommitPhaseError(finishedWork, finishedWork.return, error);
2352
}
2328
- attachSuspenseRetryListeners(finishedWork);
2353
+ const wakeables: Set<Wakeable> | null = (finishedWork.updateQueue: any);
2354
+ if (wakeables !== null) {
2355
+ finishedWork.updateQueue = null;
2356
+ attachSuspenseRetryListeners(finishedWork, wakeables);
2357
+ }
2358
}
2359
return;
2360
}
@@ -2383,6 +2412,18 @@ function commitMutationEffectsOnFiber(
2412
hideOrUnhideAllChildren(offscreenBoundary, isHidden);
2413
}
2414
}
2415
+
2416
+ // TODO: Move to passive phase
2417
+ if (flags & Update) {
2418
+ const offscreenQueue: OffscreenQueue | null = (finishedWork.updateQueue: any);
2419
+ if (offscreenQueue !== null) {
2420
+ const wakeables = offscreenQueue.wakeables;
2421
+ if (wakeables !== null) {
2422
+ offscreenQueue.wakeables = null;
2423
+ attachSuspenseRetryListeners(finishedWork, wakeables);
2424
+ }
2425
+ }
2426
+ }
2427
return;
2428
}
2429
case SuspenseListComponent: {
@@ -2390,7 +2431,11 @@ function commitMutationEffectsOnFiber(
2431
commitReconciliationEffects(finishedWork);
2432
2433
if (flags & Update) {
2393
- attachSuspenseRetryListeners(finishedWork);
2434
+ const wakeables: Set<Wakeable> | null = (finishedWork.updateQueue: any);
2435
+ if (wakeables !== null) {
2436
+ finishedWork.updateQueue = null;
2437
+ attachSuspenseRetryListeners(finishedWork, wakeables);
2438
+ }
2439
}
2440
return;
2441
}
packages/react-reconciler/src/ReactFiberCompleteWork.old.js
+49
-38
@@ -27,7 +27,6 @@ import type {
27
SuspenseState,
28
SuspenseListRenderState,
29
} from './ReactFiberSuspenseComponent.old';
30
-import type {SuspenseContext} from './ReactFiberSuspenseContext.old';
30
import type {OffscreenState} from './ReactFiberOffscreenComponent';
31
import type {TracingMarkerInstance} from './ReactFiberTracingMarkerComponent.old';
32
import type {Cache} from './ReactFiberCacheComponent.old';
@@ -110,14 +109,17 @@ import {
109
} from './ReactFiberHostContext.old';
110
import {
111
suspenseStackCursor,
113
- InvisibleParentSuspenseContext,
114
- hasSuspenseContext,
115
- popSuspenseContext,
116
- pushSuspenseContext,
117
- setShallowSuspenseContext,
112
+ popSuspenseListContext,
113
+ popSuspenseHandler,
114
+ pushSuspenseListContext,
115
+ setShallowSuspenseListContext,
116
ForceSuspenseFallback,
119
- setDefaultShallowSuspenseContext,
117
+ setDefaultShallowSuspenseListContext,
118
} from './ReactFiberSuspenseContext.old';
119
+import {
120
+ popHiddenContext,
121
+ isCurrentTreeHidden,
122
+} from './ReactFiberHiddenContext.old';
123
import {findFirstSuspended} from './ReactFiberSuspenseComponent.old';
124
import {
125
isContextProvider as isLegacyContextProvider,
@@ -147,9 +149,7 @@ import {
149
renderDidSuspend,
150
renderDidSuspendDelayIfPossible,
151
renderHasNotSuspendedYet,
150
- popRenderLanes,
152
getRenderTargetTime,
152
- subtreeRenderLanes,
153
getWorkInProgressTransitions,
154
} from './ReactFiberWorkLoop.old';
155
import {
@@ -1086,7 +1086,7 @@ function completeWork(
1086
return null;
1087
}
1088
case SuspenseComponent: {
1089
- popSuspenseContext(workInProgress);
1089
+ popSuspenseHandler(workInProgress);
1090
const nextState: null | SuspenseState = workInProgress.memoizedState;
1091
1092
// Special path for dehydrated boundaries. We may eventually move this
@@ -1195,25 +1195,23 @@ function completeWork(
1195
// If this render already had a ping or lower pri updates,
1196
// and this is the first time we know we're going to suspend we
1197
// should be able to immediately restart from within throwException.
1198
- const hasInvisibleChildContext =
1199
- current === null &&
1200
- (workInProgress.memoizedProps.unstable_avoidThisFallback !==
1201
- true ||
1202
- !enableSuspenseAvoidThisFallback);
1203
- if (
1204
- hasInvisibleChildContext ||
1205
- hasSuspenseContext(
1206
- suspenseStackCursor.current,
1207
- (InvisibleParentSuspenseContext: SuspenseContext),
1208
- )
1209
- ) {
1210
- // If this was in an invisible tree or a new render, then showing
1211
- // this boundary is ok.
1212
- renderDidSuspend();
1213
- } else {
1214
- // Otherwise, we're going to have to hide content so we should
1215
- // suspend for longer if possible.
1198
+
1199
+ // Check if this is a "bad" fallback state or a good one. A bad
1200
+ // fallback state is one that we only show as a last resort; if this
1201
+ // is a transition, we'll block it from displaying, and wait for
1202
+ // more data to arrive.
1203
+ const isBadFallback =
1204
+ // It's bad to switch to a fallback if content is already visible
1205
+ (current !== null && !prevDidTimeout && !isCurrentTreeHidden()) ||
1206
+ // Experimental: Some fallbacks are always bad
1207
+ (enableSuspenseAvoidThisFallback &&
1208
+ workInProgress.memoizedProps.unstable_avoidThisFallback ===
1209
+ true);
1210
+
1211
+ if (isBadFallback) {
1212
renderDidSuspendDelayIfPossible();
1213
+ } else {
1214
+ renderDidSuspend();
1215
}
1216
}
1217
}
@@ -1275,7 +1273,7 @@ function completeWork(
1273
return null;
1274
}
1275
case SuspenseListComponent: {
1278
- popSuspenseContext(workInProgress);
1276
+ popSuspenseListContext(workInProgress);
1277
1278
const renderState: null | SuspenseListRenderState =
1279
workInProgress.memoizedState;
@@ -1341,11 +1339,11 @@ function completeWork(
1339
workInProgress.subtreeFlags = NoFlags;
1340
resetChildFibers(workInProgress, renderLanes);
1341
1344
- // Set up the Suspense Context to force suspense and immediately
1345
- // rerender the children.
1346
- pushSuspenseContext(
1342
+ // Set up the Suspense List Context to force suspense and
1343
+ // immediately rerender the children.
1344
+ pushSuspenseListContext(
1345
workInProgress,
1348
- setShallowSuspenseContext(
1346
+ setShallowSuspenseListContext(
1347
suspenseStackCursor.current,
1348
ForceSuspenseFallback,
1349
),
@@ -1468,14 +1466,16 @@ function completeWork(
1466
// setting it the first time we go from not suspended to suspended.
1467
let suspenseContext = suspenseStackCursor.current;
1468
if (didSuspendAlready) {
1471
- suspenseContext = setShallowSuspenseContext(
1469
+ suspenseContext = setShallowSuspenseListContext(
1470
suspenseContext,
1471
ForceSuspenseFallback,
1472
);
1473
} else {
1476
- suspenseContext = setDefaultShallowSuspenseContext(suspenseContext);
1474
+ suspenseContext = setDefaultShallowSuspenseListContext(
1475
+ suspenseContext,
1476
+ );
1477
}
1478
- pushSuspenseContext(workInProgress, suspenseContext);
1478
+ pushSuspenseListContext(workInProgress, suspenseContext);
1479
// Do a pass over the next row.
1480
// Don't bubble properties in this case.
1481
return next;
@@ -1508,7 +1508,8 @@ function completeWork(
1508
}
1509
case OffscreenComponent:
1510
case LegacyHiddenComponent: {
1511
- popRenderLanes(workInProgress);
1511
+ popSuspenseHandler(workInProgress);
1512
+ popHiddenContext(workInProgress);
1513
const nextState: OffscreenState | null = workInProgress.memoizedState;
1514
const nextIsHidden = nextState !== null;
1515
@@ -1529,7 +1530,11 @@ function completeWork(
1530
} else {
1531
// Don't bubble properties for hidden children unless we're rendering
1532
// at offscreen priority.
1532
- if (includesSomeLane(subtreeRenderLanes, (OffscreenLane: Lane))) {
1533
+ if (
1534
+ includesSomeLane(renderLanes, (OffscreenLane: Lane)) &&
1535
+ // Also don't bubble if the tree suspended
1536
+ (workInProgress.flags & DidCapture) === NoLanes
1537
+ ) {
1538
bubbleProperties(workInProgress);
1539
// Check if there was an insertion or update in the hidden subtree.
1540
// If so, we need to hide those nodes in the commit phase, so
@@ -1544,6 +1549,12 @@ function completeWork(
1549
}
1550
}
1551
1552
+ if (workInProgress.updateQueue !== null) {
1553
+ // Schedule an effect to attach Suspense retry listeners
1554
+ // TODO: Move to passive phase
1555
+ workInProgress.flags |= Update;
1556
+ }
1557
+
1558
if (enableCache) {
1559
let previousCache: Cache | null = null;
1560
if (
packages/react-reconciler/src/ReactFiberHiddenContext.old.js
+70
-1
@@ -1 +1,70 @@
1
-// Intentionally blank. File only exists in new reconciler fork.
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
+import type {Fiber} from './ReactInternalTypes';
11
+import type {StackCursor} from './ReactFiberStack.old';
12
+import type {Lanes} from './ReactFiberLane.old';
13
+
14
+import {createCursor, push, pop} from './ReactFiberStack.old';
15
+
16
+import {getRenderLanes, setRenderLanes} from './ReactFiberWorkLoop.old';
17
+import {NoLanes, mergeLanes} from './ReactFiberLane.old';
18
+
19
+// TODO: Remove `renderLanes` context in favor of hidden context
20
+type HiddenContext = {
21
+ // Represents the lanes that must be included when processing updates in
22
+ // order to reveal the hidden content.
23
+ // TODO: Remove `subtreeLanes` context from work loop in favor of this one.
24
+ baseLanes: number,
25
+};
26
+
27
+// TODO: This isn't being used yet, but it's intended to replace the
28
+// InvisibleParentContext that is currently managed by SuspenseContext.
29
+export const currentTreeHiddenStackCursor: StackCursor<HiddenContext | null> = createCursor(
30
+ null,
31
+);
32
+export const prevRenderLanesStackCursor: StackCursor<Lanes> = createCursor(
33
+ NoLanes,
34
+);
35
+
36
+export function pushHiddenContext(fiber: Fiber, context: HiddenContext): void {
37
+ const prevRenderLanes = getRenderLanes();
38
+ push(prevRenderLanesStackCursor, prevRenderLanes, fiber);
39
+ push(currentTreeHiddenStackCursor, context, fiber);
40
+
41
+ // When rendering a subtree that's currently hidden, we must include all
42
+ // lanes that would have rendered if the hidden subtree hadn't been deferred.
43
+ // That is, in order to reveal content from hidden -> visible, we must commit
44
+ // all the updates that we skipped when we originally hid the tree.
45
+ setRenderLanes(mergeLanes(prevRenderLanes, context.baseLanes));
46
+}
47
+
48
+export function reuseHiddenContextOnStack(fiber: Fiber): void {
49
+ // This subtree is not currently hidden, so we don't need to add any lanes
50
+ // to the render lanes. But we still need to push something to avoid a
51
+ // context mismatch. Reuse the existing context on the stack.
52
+ push(prevRenderLanesStackCursor, getRenderLanes(), fiber);
53
+ push(
54
+ currentTreeHiddenStackCursor,
55
+ currentTreeHiddenStackCursor.current,
56
+ fiber,
57
+ );
58
+}
59
+
60
+export function popHiddenContext(fiber: Fiber): void {
61
+ // Restore the previous render lanes from the stack
62
+ setRenderLanes(prevRenderLanesStackCursor.current);
63
+
64
+ pop(currentTreeHiddenStackCursor, fiber);
65
+ pop(prevRenderLanesStackCursor, fiber);
66
+}
67
+
68
+export function isCurrentTreeHidden() {
69
+ return currentTreeHiddenStackCursor.current !== null;
70
+}
packages/react-reconciler/src/ReactFiberSuspenseComponent.old.js
-32
@@ -13,7 +13,6 @@ import type {SuspenseInstance} from './ReactFiberHostConfig';
13
import type {Lane} from './ReactFiberLane.old';
14
import type {TreeContext} from './ReactFiberTreeContext.old';
15
16
-import {enableSuspenseAvoidThisFallback} from 'shared/ReactFeatureFlags';
16
import {SuspenseComponent, SuspenseListComponent} from './ReactWorkTags';
17
import {NoFlags, DidCapture} from './ReactFiberFlags';
18
import {
@@ -67,37 +66,6 @@ export type SuspenseListRenderState = {|
66
tailMode: SuspenseListTailMode,
67
|};
68
70
-export function shouldCaptureSuspense(
71
- workInProgress: Fiber,
72
- hasInvisibleParent: boolean,
73
-): boolean {
74
- // If it was the primary children that just suspended, capture and render the
75
- // fallback. Otherwise, don't capture and bubble to the next boundary.
76
- const nextState: SuspenseState | null = workInProgress.memoizedState;
77
- if (nextState !== null) {
78
- if (nextState.dehydrated !== null) {
79
- // A dehydrated boundary always captures.
80
- return true;
81
- }
82
- return false;
83
- }
84
- const props = workInProgress.memoizedProps;
85
- // Regular boundaries always capture.
86
- if (
87
- !enableSuspenseAvoidThisFallback ||
88
- props.unstable_avoidThisFallback !== true
89
- ) {
90
- return true;
91
- }
92
- // If it's a boundary we should avoid, then we prefer to bubble up to the
93
- // parent boundary if it is currently invisible.
94
- if (hasInvisibleParent) {
95
- return false;
96
- }
97
- // If the parent is not able to handle it, we must handle it.
98
- return true;
99
-}
100
-
69
export function findFirstSuspended(row: Fiber): null | Fiber {
70
let node = row;
71
while (node !== null) {
packages/react-reconciler/src/ReactFiberSuspenseContext.old.js
+97
-28
@@ -9,33 +9,109 @@
9
10
import type {Fiber} from './ReactInternalTypes';
11
import type {StackCursor} from './ReactFiberStack.old';
12
+import type {SuspenseState} from './ReactFiberSuspenseComponent.old';
13
14
+import {enableSuspenseAvoidThisFallback} from 'shared/ReactFeatureFlags';
15
import {createCursor, push, pop} from './ReactFiberStack.old';
16
+import {isCurrentTreeHidden} from './ReactFiberHiddenContext.old';
17
+import {SuspenseComponent, OffscreenComponent} from './ReactWorkTags';
18
19
+// The Suspense handler is the boundary that should capture if something
20
+// suspends, i.e. it's the nearest `catch` block on the stack.
21
+const suspenseHandlerStackCursor: StackCursor<Fiber | null> = createCursor(
22
+ null,
23
+);
24
+
25
+function shouldAvoidedBoundaryCapture(
26
+ workInProgress: Fiber,
27
+ handlerOnStack: Fiber,
28
+ props: any,
29
+): boolean {
30
+ if (enableSuspenseAvoidThisFallback) {
31
+ // If the parent is already showing content, and we're not inside a hidden
32
+ // tree, then we should show the avoided fallback.
33
+ if (handlerOnStack.alternate !== null && !isCurrentTreeHidden()) {
34
+ return true;
35
+ }
36
+
37
+ // If the handler on the stack is also an avoided boundary, then we should
38
+ // favor this inner one.
39
+ if (
40
+ handlerOnStack.tag === SuspenseComponent &&
41
+ handlerOnStack.memoizedProps.unstable_avoidThisFallback === true
42
+ ) {
43
+ return true;
44
+ }
45
+
46
+ // If this avoided boundary is dehydrated, then it should capture.
47
+ const suspenseState: SuspenseState | null = workInProgress.memoizedState;
48
+ if (suspenseState !== null && suspenseState.dehydrated !== null) {
49
+ return true;
50
+ }
51
+ }
52
+
53
+ // If none of those cases apply, then we should avoid this fallback and show
54
+ // the outer one instead.
55
+ return false;
56
+}
57
+
58
+export function pushPrimaryTreeSuspenseHandler(handler: Fiber): void {
59
+ const props = handler.pendingProps;
60
+ const handlerOnStack = suspenseHandlerStackCursor.current;
61
+ if (
62
+ enableSuspenseAvoidThisFallback &&
63
+ props.unstable_avoidThisFallback === true &&
64
+ handlerOnStack !== null &&
65
+ !shouldAvoidedBoundaryCapture(handler, handlerOnStack, props)
66
+ ) {
67
+ // This boundary should not capture if something suspends. Reuse the
68
+ // existing handler on the stack.
69
+ push(suspenseHandlerStackCursor, handlerOnStack, handler);
70
+ } else {
71
+ // Push this handler onto the stack.
72
+ push(suspenseHandlerStackCursor, handler, handler);
73
+ }
74
+}
75
+
76
+export function pushFallbackTreeSuspenseHandler(fiber: Fiber): void {
77
+ // We're about to render the fallback. If something in the fallback suspends,
78
+ // it's akin to throwing inside of a `catch` block. This boundary should not
79
+ // capture. Reuse the existing handler on the stack.
80
+ reuseSuspenseHandlerOnStack(fiber);
81
+}
82
+
83
+export function pushOffscreenSuspenseHandler(fiber: Fiber): void {
84
+ if (fiber.tag === OffscreenComponent) {
85
+ push(suspenseHandlerStackCursor, fiber, fiber);
86
+ } else {
87
+ // This is a LegacyHidden component.
88
+ reuseSuspenseHandlerOnStack(fiber);
89
+ }
90
+}
91
+
92
+export function reuseSuspenseHandlerOnStack(fiber: Fiber) {
93
+ push(suspenseHandlerStackCursor, getSuspenseHandler(), fiber);
94
+}
95
+
96
+export function getSuspenseHandler(): Fiber | null {
97
+ return suspenseHandlerStackCursor.current;
98
+}
99
+
100
+export function popSuspenseHandler(fiber: Fiber): void {
101
+ pop(suspenseHandlerStackCursor, fiber);
102
+}
103
+
104
+// SuspenseList context
105
+// TODO: Move to a separate module? We may change the SuspenseList
106
+// implementation to hide/show in the commit phase, anyway.
107
export opaque type SuspenseContext = number;
108
export opaque type SubtreeSuspenseContext: SuspenseContext = number;
109
export opaque type ShallowSuspenseContext: SuspenseContext = number;
110
111
const DefaultSuspenseContext: SuspenseContext = 0b00;
112
21
-// The Suspense Context is split into two parts. The lower bits is
22
-// inherited deeply down the subtree. The upper bits only affect
23
-// this immediate suspense boundary and gets reset each new
24
-// boundary or suspense list.
113
const SubtreeSuspenseContextMask: SuspenseContext = 0b01;
114
27
-// Subtree Flags:
28
-
29
-// InvisibleParentSuspenseContext indicates that one of our parent Suspense
30
-// boundaries is not currently showing visible main content.
31
-// Either because it is already showing a fallback or is not mounted at all.
32
-// We can use this to determine if it is desirable to trigger a fallback at
33
-// the parent. If not, then we might need to trigger undesirable boundaries
34
-// and/or suspend the commit to avoid hiding the parent content.
35
-export const InvisibleParentSuspenseContext: SubtreeSuspenseContext = 0b01;
36
-
37
-// Shallow Flags:
38
-
115
// ForceSuspenseFallback can be used by SuspenseList to force newly added
116
// items into their fallback state during one of the render passes.
117
export const ForceSuspenseFallback: ShallowSuspenseContext = 0b10;
@@ -44,40 +120,33 @@ export const suspenseStackCursor: StackCursor<SuspenseContext> = createCursor(
120
DefaultSuspenseContext,
121
);
122
47
-export function hasSuspenseContext(
123
+export function hasSuspenseListContext(
124
parentContext: SuspenseContext,
125
flag: SuspenseContext,
126
): boolean {
127
return (parentContext & flag) !== 0;
128
}
129
54
-export function setDefaultShallowSuspenseContext(
130
+export function setDefaultShallowSuspenseListContext(
131
parentContext: SuspenseContext,
132
): SuspenseContext {
133
return parentContext & SubtreeSuspenseContextMask;
134
}
135
60
-export function setShallowSuspenseContext(
136
+export function setShallowSuspenseListContext(
137
parentContext: SuspenseContext,
138
shallowContext: ShallowSuspenseContext,
139
): SuspenseContext {
140
return (parentContext & SubtreeSuspenseContextMask) | shallowContext;
141
}
142
67
-export function addSubtreeSuspenseContext(
68
- parentContext: SuspenseContext,
69
- subtreeContext: SubtreeSuspenseContext,
70
-): SuspenseContext {
71
- return parentContext | subtreeContext;
72
-}
73
-
74
-export function pushSuspenseContext(
143
+export function pushSuspenseListContext(
144
fiber: Fiber,
145
newContext: SuspenseContext,
146
): void {
147
push(suspenseStackCursor, newContext, fiber);
148
}
149
81
-export function popSuspenseContext(fiber: Fiber): void {
150
+export function popSuspenseListContext(fiber: Fiber): void {
151
pop(suspenseStackCursor, fiber);
152
}
packages/react-reconciler/src/ReactFiberThrow.old.js
+65
-66
@@ -13,17 +13,18 @@ import type {Lane, Lanes} from './ReactFiberLane.old';
13
import type {CapturedValue} from './ReactCapturedValue';
14
import type {Update} from './ReactFiberClassUpdateQueue.old';
15
import type {Wakeable} from 'shared/ReactTypes';
16
-import type {SuspenseContext} from './ReactFiberSuspenseContext.old';
16
+import type {OffscreenQueue} from './ReactFiberOffscreenComponent';
17
18
import getComponentNameFromFiber from 'react-reconciler/src/getComponentNameFromFiber';
19
import {
20
ClassComponent,
21
HostRoot,
22
- SuspenseComponent,
22
IncompleteClassComponent,
23
FunctionComponent,
24
ForwardRef,
25
SimpleMemoComponent,
26
+ SuspenseComponent,
27
+ OffscreenComponent,
28
} from './ReactWorkTags';
29
import {
30
DidCapture,
@@ -34,7 +35,6 @@ import {
35
ForceUpdateForLegacySuspense,
36
ForceClientRender,
37
} from './ReactFiberFlags';
37
-import {shouldCaptureSuspense} from './ReactFiberSuspenseComponent.old';
38
import {NoMode, ConcurrentMode, DebugTracingMode} from './ReactTypeOfMode';
39
import {
40
enableDebugTracing,
@@ -50,11 +50,7 @@ import {
50
enqueueUpdate,
51
} from './ReactFiberClassUpdateQueue.old';
52
import {markFailedErrorBoundaryForHotReloading} from './ReactFiberHotReloading.old';
53
-import {
54
- suspenseStackCursor,
55
- InvisibleParentSuspenseContext,
56
- hasSuspenseContext,
57
-} from './ReactFiberSuspenseContext.old';
53
+import {getSuspenseHandler} from './ReactFiberSuspenseContext.old';
54
import {
55
renderDidError,
56
renderDidSuspendDelayIfPossible,
@@ -203,33 +199,6 @@ function attachPingListener(root: FiberRoot, wakeable: Wakeable, lanes: Lanes) {
199
}
200
}
201
206
-function attachRetryListener(
207
- suspenseBoundary: Fiber,
208
- root: FiberRoot,
209
- wakeable: Wakeable,
210
- lanes: Lanes,
211
-) {
212
- // Retry listener
213
- //
214
- // If the fallback does commit, we need to attach a different type of
215
- // listener. This one schedules an update on the Suspense boundary to turn
216
- // the fallback state off.
217
- //
218
- // Stash the wakeable on the boundary fiber so we can access it in the
219
- // commit phase.
220
- //
221
- // When the wakeable resolves, we'll attempt to render the boundary
222
- // again ("retry").
223
- const wakeables: Set<Wakeable> | null = (suspenseBoundary.updateQueue: any);
224
- if (wakeables === null) {
225
- const updateQueue = (new Set(): any);
226
- updateQueue.add(wakeable);
227
- suspenseBoundary.updateQueue = updateQueue;
228
- } else {
229
- wakeables.add(wakeable);
230
- }
231
-}
232
-
202
function resetSuspendedComponent(sourceFiber: Fiber, rootRenderLanes: Lanes) {
203
if (enableLazyContextPropagation) {
204
const currentSourceFiber = sourceFiber.alternate;
@@ -269,26 +238,6 @@ function resetSuspendedComponent(sourceFiber: Fiber, rootRenderLanes: Lanes) {
238
}
239
}
240
272
-function getNearestSuspenseBoundaryToCapture(returnFiber: Fiber) {
273
- let node = returnFiber;
274
- const hasInvisibleParentBoundary = hasSuspenseContext(
275
- suspenseStackCursor.current,
276
- (InvisibleParentSuspenseContext: SuspenseContext),
277
- );
278
- do {
279
- if (
280
- node.tag === SuspenseComponent &&
281
- shouldCaptureSuspense(node, hasInvisibleParentBoundary)
282
- ) {
283
- return node;
284
- }
285
- // This boundary already captured during this render. Continue to the next
286
- // boundary.
287
- node = node.return;
288
- } while (node !== null);
289
- return null;
290
-}
291
-
241
function markSuspenseBoundaryShouldCapture(
242
suspenseBoundary: Fiber,
243
returnFiber: Fiber,
@@ -444,22 +393,72 @@ function throwException(
393
}
394
395
// Schedule the nearest Suspense to re-render the timed out view.
447
- const suspenseBoundary = getNearestSuspenseBoundaryToCapture(returnFiber);
396
+ const suspenseBoundary = getSuspenseHandler();
397
if (suspenseBoundary !== null) {
449
- suspenseBoundary.flags &= ~ForceClientRender;
450
- markSuspenseBoundaryShouldCapture(
451
- suspenseBoundary,
452
- returnFiber,
453
- sourceFiber,
454
- root,
455
- rootRenderLanes,
456
- );
398
+ switch (suspenseBoundary.tag) {
399
+ case SuspenseComponent: {
400
+ suspenseBoundary.flags &= ~ForceClientRender;
401
+ markSuspenseBoundaryShouldCapture(
402
+ suspenseBoundary,
403
+ returnFiber,
404
+ sourceFiber,
405
+ root,
406
+ rootRenderLanes,
407
+ );
408
+ // Retry listener
409
+ //
410
+ // If the fallback does commit, we need to attach a different type of
411
+ // listener. This one schedules an update on the Suspense boundary to
412
+ // turn the fallback state off.
413
+ //
414
+ // Stash the wakeable on the boundary fiber so we can access it in the
415
+ // commit phase.
416
+ //
417
+ // When the wakeable resolves, we'll attempt to render the boundary
418
+ // again ("retry").
419
+ const wakeables: Set<Wakeable> | null = (suspenseBoundary.updateQueue: any);
420
+ if (wakeables === null) {
421
+ suspenseBoundary.updateQueue = new Set([wakeable]);
422
+ } else {
423
+ wakeables.add(wakeable);
424
+ }
425
+ break;
426
+ }
427
+ case OffscreenComponent: {
428
+ if (suspenseBoundary.mode & ConcurrentMode) {
429
+ suspenseBoundary.flags |= ShouldCapture;
430
+ const offscreenQueue: OffscreenQueue | null = (suspenseBoundary.updateQueue: any);
431
+ if (offscreenQueue === null) {
432
+ const newOffscreenQueue: OffscreenQueue = {
433
+ transitions: null,
434
+ markerInstances: null,
435
+ wakeables: new Set([wakeable]),
436
+ };
437
+ suspenseBoundary.updateQueue = newOffscreenQueue;
438
+ } else {
439
+ const wakeables = offscreenQueue.wakeables;
440
+ if (wakeables === null) {
441
+ offscreenQueue.wakeables = new Set([wakeable]);
442
+ } else {
443
+ wakeables.add(wakeable);
444
+ }
445
+ }
446
+ break;
447
+ }
448
+ }
449
+ // eslint-disable-next-line no-fallthrough
450
+ default: {
451
+ throw new Error(
452
+ `Unexpected Suspense handler tag (${suspenseBoundary.tag}). This ` +
453
+ 'is a bug in React.',
454
+ );
455
+ }
456
+ }
457
// We only attach ping listeners in concurrent mode. Legacy Suspense always
458
// commits fallbacks synchronously, so there are no pings.
459
if (suspenseBoundary.mode & ConcurrentMode) {
460
attachPingListener(root, wakeable, rootRenderLanes);
461
}
462
- attachRetryListener(suspenseBoundary, root, wakeable, rootRenderLanes);
462
return;
463
} else {
464
// No boundary was found. Unless this is a sync update, this is OK.
@@ -496,7 +495,7 @@ function throwException(
495
// This is a regular error, not a Suspense wakeable.
496
if (getIsHydrating() && sourceFiber.mode & ConcurrentMode) {
497
markDidThrowWhileHydratingDEV();
499
- const suspenseBoundary = getNearestSuspenseBoundaryToCapture(returnFiber);
498
+ const suspenseBoundary = getSuspenseHandler();
499
// If the error was thrown during hydration, we may be able to recover by
500
// discarding the dehydrated content and switching to a client render.
501
// Instead of surfacing the error, find the nearest Suspense boundary
packages/react-reconciler/src/ReactFiberUnwindWork.old.js
+27
-9
@@ -37,7 +37,11 @@ import {
37
} from 'shared/ReactFeatureFlags';
38
39
import {popHostContainer, popHostContext} from './ReactFiberHostContext.old';
40
-import {popSuspenseContext} from './ReactFiberSuspenseContext.old';
40
+import {
41
+ popSuspenseListContext,
42
+ popSuspenseHandler,
43
+} from './ReactFiberSuspenseContext.old';
44
+import {popHiddenContext} from './ReactFiberHiddenContext.old';
45
import {resetHydrationState} from './ReactFiberHydrationContext.old';
46
import {
47
isContextProvider as isLegacyContextProvider,
@@ -45,7 +49,6 @@ import {
49
popTopLevelContextObject as popTopLevelLegacyContextObject,
50
} from './ReactFiberContext.old';
51
import {popProvider} from './ReactFiberNewContext.old';
48
-import {popRenderLanes} from './ReactFiberWorkLoop.old';
52
import {popCacheProvider} from './ReactFiberCacheComponent.old';
53
import {transferActualDuration} from './ReactProfilerTimer.old';
54
import {popTreeContext} from './ReactFiberTreeContext.old';
@@ -118,7 +121,7 @@ function unwindWork(
121
return null;
122
}
123
case SuspenseComponent: {
121
- popSuspenseContext(workInProgress);
124
+ popSuspenseHandler(workInProgress);
125
const suspenseState: null | SuspenseState = workInProgress.memoizedState;
126
if (suspenseState !== null && suspenseState.dehydrated !== null) {
127
if (workInProgress.alternate === null) {
@@ -146,7 +149,7 @@ function unwindWork(
149
return null;
150
}
151
case SuspenseListComponent: {
149
- popSuspenseContext(workInProgress);
152
+ popSuspenseListContext(workInProgress);
153
// SuspenseList doesn't actually catch anything. It should've been
154
// caught by a nested boundary. If not, it should bubble through.
155
return null;
@@ -159,10 +162,24 @@ function unwindWork(
162
popProvider(context, workInProgress);
163
return null;
164
case OffscreenComponent:
162
- case LegacyHiddenComponent:
163
- popRenderLanes(workInProgress);
165
+ case LegacyHiddenComponent: {
166
+ popSuspenseHandler(workInProgress);
167
+ popHiddenContext(workInProgress);
168
popTransition(workInProgress, current);
169
+ const flags = workInProgress.flags;
170
+ if (flags & ShouldCapture) {
171
+ workInProgress.flags = (flags & ~ShouldCapture) | DidCapture;
172
+ // Captured a suspense effect. Re-render the boundary.
173
+ if (
174
+ enableProfilerTimer &&
175
+ (workInProgress.mode & ProfileMode) !== NoMode
176
+ ) {
177
+ transferActualDuration(workInProgress);
178
+ }
179
+ return workInProgress;
180
+ }
181
return null;
182
+ }
183
case CacheComponent:
184
if (enableCache) {
185
const cache: Cache = workInProgress.memoizedState.cache;
@@ -224,10 +241,10 @@ function unwindInterruptedWork(
241
popHostContainer(interruptedWork);
242
break;
243
case SuspenseComponent:
227
- popSuspenseContext(interruptedWork);
244
+ popSuspenseHandler(interruptedWork);
245
break;
246
case SuspenseListComponent:
230
- popSuspenseContext(interruptedWork);
247
+ popSuspenseListContext(interruptedWork);
248
break;
249
case ContextProvider:
250
const context: ReactContext<any> = interruptedWork.type._context;
@@ -235,7 +252,8 @@ function unwindInterruptedWork(
252
break;
253
case OffscreenComponent:
254
case LegacyHiddenComponent:
238
- popRenderLanes(interruptedWork);
255
+ popSuspenseHandler(interruptedWork);
256
+ popHiddenContext(interruptedWork);
257
popTransition(interruptedWork, current);
258
break;
259
case CacheComponent:
packages/react-reconciler/src/ReactFiberWorkLoop.old.js
+28
-35
@@ -11,7 +11,6 @@ import type {Wakeable} from 'shared/ReactTypes';
11
import type {Fiber, FiberRoot} from './ReactInternalTypes';
12
import type {Lanes, Lane} from './ReactFiberLane.old';
13
import type {SuspenseState} from './ReactFiberSuspenseComponent.old';
14
-import type {StackCursor} from './ReactFiberStack.old';
14
import type {Flags} from './ReactFiberFlags';
15
import type {FunctionComponentUpdateQueue} from './ReactFiberHooks.old';
16
import type {EventPriority} from './ReactEventPriorities.old';
@@ -20,6 +19,7 @@ import type {
19
MarkerTransition,
20
Transition,
21
} from './ReactFiberTracingMarkerComponent.old';
22
+import type {OffscreenInstance} from './ReactFiberOffscreenComponent';
23
24
import {
25
warnAboutDeprecatedLifecycles,
@@ -96,6 +96,7 @@ import {
96
ClassComponent,
97
SuspenseComponent,
98
SuspenseListComponent,
99
+ OffscreenComponent,
100
FunctionComponent,
101
ForwardRef,
102
MemoComponent,
@@ -191,11 +192,6 @@ import {
192
createCapturedValueAtFiber,
193
type CapturedValue,
194
} from './ReactCapturedValue';
194
-import {
195
- push as pushToStack,
196
- pop as popFromStack,
197
- createCursor,
198
-} from './ReactFiberStack.old';
195
import {
196
enqueueConcurrentRenderForLane,
197
finishQueueingConcurrentUpdates,
@@ -284,26 +280,20 @@ let workInProgress: Fiber | null = null;
280
// The lanes we're rendering
281
let workInProgressRootRenderLanes: Lanes = NoLanes;
282
287
-// Stack that allows components to change the render lanes for its subtree
288
-// This is a superset of the lanes we started working on at the root. The only
289
-// case where it's different from `workInProgressRootRenderLanes` is when we
290
-// enter a subtree that is hidden and needs to be unhidden: Suspense and
291
-// Offscreen component.
283
+// A contextual version of workInProgressRootRenderLanes. It is a superset of
284
+// the lanes that we started working on at the root. When we enter a subtree
285
+// that is currently hidden, we add the lanes that would have committed if
286
+// the hidden tree hadn't been deferred. This is modified by the
287
+// HiddenContext module.
288
//
289
// Most things in the work loop should deal with workInProgressRootRenderLanes.
294
-// Most things in begin/complete phases should deal with subtreeRenderLanes.
295
-export let subtreeRenderLanes: Lanes = NoLanes;
296
-const subtreeRenderLanesCursor: StackCursor<Lanes> = createCursor(NoLanes);
290
+// Most things in begin/complete phases should deal with renderLanes.
291
+export let renderLanes: Lanes = NoLanes;
292
293
// Whether to root completed, errored, suspended, etc.
294
let workInProgressRootExitStatus: RootExitStatus = RootInProgress;
295
// A fatal error, if one is thrown
296
let workInProgressRootFatalError: mixed = null;
302
-// "Included" lanes refer to lanes that were worked on during this render. It's
303
-// slightly different than `renderLanes` because `renderLanes` can change as you
304
-// enter and exit an Offscreen tree. This value is the combination of all render
305
-// lanes for the entire render phase.
306
-let workInProgressRootIncludedLanes: Lanes = NoLanes;
297
// The work left over by components that were visited during this render. Only
298
// includes unprocessed updates, not work in bailed out children.
299
let workInProgressRootSkippedLanes: Lanes = NoLanes;
@@ -1454,18 +1444,16 @@ export function flushControlled(fn: () => mixed): void {
1444
}
1445
}
1446
1457
-export function pushRenderLanes(fiber: Fiber, lanes: Lanes) {
1458
- pushToStack(subtreeRenderLanesCursor, subtreeRenderLanes, fiber);
1459
- subtreeRenderLanes = mergeLanes(subtreeRenderLanes, lanes);
1460
- workInProgressRootIncludedLanes = mergeLanes(
1461
- workInProgressRootIncludedLanes,
1462
- lanes,
1463
- );
1447
+// This is called by the HiddenContext module when we enter or leave a
1448
+// hidden subtree. The stack logic is managed there because that's the only
1449
+// place that ever modifies it. Which module it lives in doesn't matter for
1450
+// performance because this function will get inlined regardless
1451
+export function setRenderLanes(subtreeRenderLanes: Lanes) {
1452
+ renderLanes = subtreeRenderLanes;
1453
}
1454
1466
-export function popRenderLanes(fiber: Fiber) {
1467
- subtreeRenderLanes = subtreeRenderLanesCursor.current;
1468
- popFromStack(subtreeRenderLanesCursor, fiber);
1455
+export function getRenderLanes(): Lanes {
1456
+ return renderLanes;
1457
}
1458
1459
function prepareFreshStack(root: FiberRoot, lanes: Lanes): Fiber {
@@ -1496,7 +1484,7 @@ function prepareFreshStack(root: FiberRoot, lanes: Lanes): Fiber {
1484
workInProgressRoot = root;
1485
const rootWorkInProgress = createWorkInProgress(root.current, null);
1486
workInProgress = rootWorkInProgress;
1499
- workInProgressRootRenderLanes = subtreeRenderLanes = workInProgressRootIncludedLanes = lanes;
1487
+ workInProgressRootRenderLanes = renderLanes = lanes;
1488
workInProgressRootExitStatus = RootInProgress;
1489
workInProgressRootFatalError = null;
1490
workInProgressRootSkippedLanes = NoLanes;
@@ -1863,10 +1851,10 @@ function performUnitOfWork(unitOfWork: Fiber): void {
1851
let next;
1852
if (enableProfilerTimer && (unitOfWork.mode & ProfileMode) !== NoMode) {
1853
startProfilerTimer(unitOfWork);
1866
- next = beginWork(current, unitOfWork, subtreeRenderLanes);
1854
+ next = beginWork(current, unitOfWork, renderLanes);
1855
stopProfilerTimerIfRunningAndRecordDelta(unitOfWork, true);
1856
} else {
1869
- next = beginWork(current, unitOfWork, subtreeRenderLanes);
1857
+ next = beginWork(current, unitOfWork, renderLanes);
1858
}
1859
1860
resetCurrentDebugFiberInDEV();
@@ -1900,10 +1888,10 @@ function completeUnitOfWork(unitOfWork: Fiber): void {
1888
!enableProfilerTimer ||
1889
(completedWork.mode & ProfileMode) === NoMode
1890
) {
1903
- next = completeWork(current, completedWork, subtreeRenderLanes);
1891
+ next = completeWork(current, completedWork, renderLanes);
1892
} else {
1893
startProfilerTimer(completedWork);
1906
- next = completeWork(current, completedWork, subtreeRenderLanes);
1894
+ next = completeWork(current, completedWork, renderLanes);
1895
// Update render duration assuming we didn't error.
1896
stopProfilerTimerIfRunningAndRecordDelta(completedWork, false);
1897
}
@@ -1918,7 +1906,7 @@ function completeUnitOfWork(unitOfWork: Fiber): void {
1906
// This fiber did not complete because something threw. Pop values off
1907
// the stack without entering the complete phase. If this is a boundary,
1908
// capture values if possible.
1921
- const next = unwindWork(current, completedWork, subtreeRenderLanes);
1909
+ const next = unwindWork(current, completedWork, renderLanes);
1910
1911
// Because this fiber did not complete, don't reset its lanes.
1912
@@ -2761,6 +2749,11 @@ export function resolveRetryWakeable(boundaryFiber: Fiber, wakeable: Wakeable) {
2749
case SuspenseListComponent:
2750
retryCache = boundaryFiber.stateNode;
2751
break;
2752
+ case OffscreenComponent: {
2753
+ const instance: OffscreenInstance = boundaryFiber.stateNode;
2754
+ retryCache = instance.retryCache;
2755
+ break;
2756
+ }
2757
default:
2758
throw new Error(
2759
'Pinged unknown suspense boundary type. ' +
packages/react-reconciler/src/__tests__/ReactOffscreenSuspense-test.js
-8
@@ -90,8 +90,6 @@ describe('ReactOffscreen', () => {
90
return text;
91
}
92
93
- // Only works in new reconciler
94
- // @gate variant
93
// @gate enableOffscreen
94
test('basic example of suspending inside hidden tree', async () => {
95
const root = ReactNoop.createRoot();
@@ -172,8 +170,6 @@ describe('ReactOffscreen', () => {
170
);
171
});
172
175
- // Only works in new reconciler
176
- // @gate variant
173
// @gate experimental || www
174
test("suspending inside currently hidden tree that's switching to visible", async () => {
175
const root = ReactNoop.createRoot();
@@ -233,8 +229,6 @@ describe('ReactOffscreen', () => {
229
);
230
});
231
236
- // Only works in new reconciler
237
- // @gate variant
232
// @gate enableOffscreen
233
test("suspending inside currently visible tree that's switching to hidden", async () => {
234
const root = ReactNoop.createRoot();
@@ -354,8 +348,6 @@ describe('ReactOffscreen', () => {
348
});
349
});
350
357
- // Only works in new reconciler
358
- // @gate variant
351
// @gate experimental || www
352
test('updates at multiple priorities that suspend inside hidden tree', async () => {
353
let setText;
scripts/merge-fork/forked-revisions
-3
@@ -1,3 +0,0 @@
1
-67de5e3fb09eecfab91321246246095058a708a9 [FORKED] Hidden trees should capture Suspense
2
-6ab05ee2e9c5b1f4c8dc1f7ae8906bf613788ba7 [FORKED] Track nearest Suspense handler on stack
3
-051ac55cb75f426b81f8f75b143f34255476b9bc [FORKED] Add HiddenContext to track if subtree is hidden