@samitouri / QOS-React-2 / commits / 30eb267abd

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