From 051ac55cb75f426b81f8f75b143f34255476b9bc Mon Sep 17 00:00:00 2001 From: Andrew Clark Date: Wed, 18 May 2022 20:18:43 -0400 Subject: [PATCH 1/4] [FORKED] Add HiddenContext to track if subtree is hidden MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This adds a new stack cursor for tracking whether we're rendering inside a subtree that's currently hidden. This corresponds to the same place where we're already tracking the "base lanes" needed to reveal a hidden subtree — that is, when going from hidden -> visible, the base lanes are the ones that we skipped over when we deferred the subtree. We must includes all the base lanes and their updates in order to avoid an inconsistency with the surrounding content that already committed. I consolidated the base lanes logic and the hidden logic into the same set of push/pop calls. This is intended to replace the InvisibleParentContext that is currently part of SuspenseContext, but I haven't done that part yet. --- .../src/ReactFiberBeginWork.new.js | 48 +++++++------- .../src/ReactFiberCompleteWork.new.js | 7 +- .../src/ReactFiberHiddenContext.new.js | 66 +++++++++++++++++++ .../src/ReactFiberHiddenContext.old.js | 1 + .../src/ReactFiberUnwindWork.new.js | 6 +- .../src/ReactFiberWorkLoop.new.js | 56 ++++++---------- 6 files changed, 118 insertions(+), 66 deletions(-) create mode 100644 packages/react-reconciler/src/ReactFiberHiddenContext.new.js create mode 100644 packages/react-reconciler/src/ReactFiberHiddenContext.old.js diff --git a/packages/react-reconciler/src/ReactFiberBeginWork.new.js b/packages/react-reconciler/src/ReactFiberBeginWork.new.js index 9f60e0ab227e9..fed99735b8ba0 100644 --- a/packages/react-reconciler/src/ReactFiberBeginWork.new.js +++ b/packages/react-reconciler/src/ReactFiberBeginWork.new.js @@ -175,6 +175,10 @@ import { addSubtreeSuspenseContext, setShallowSuspenseContext, } from './ReactFiberSuspenseContext.new'; +import { + pushHiddenContext, + reuseHiddenContextOnStack, +} from './ReactFiberHiddenContext.new'; import {findFirstSuspended} from './ReactFiberSuspenseComponent.new'; import { pushProvider, @@ -232,7 +236,6 @@ import { renderDidSuspendDelayIfPossible, markSkippedUpdateLanes, getWorkInProgressRoot, - pushRenderLanes, } from './ReactFiberWorkLoop.new'; import {enqueueConcurrentRenderForLane} from './ReactFiberConcurrentUpdates.new'; import {setWorkInProgressVersion} from './ReactMutableSource.new'; @@ -688,21 +691,14 @@ function updateOffscreenComponent( pushTransition(workInProgress, null, null); } } - pushRenderLanes(workInProgress, renderLanes); + reuseHiddenContextOnStack(workInProgress); } else if (!includesSomeLane(renderLanes, (OffscreenLane: Lane))) { - let spawnedCachePool: SpawnedCachePool | null = null; // We're hidden, and we're not rendering at Offscreen. We will bail out // and resume this tree later. - let nextBaseLanes; + let nextBaseLanes = renderLanes; if (prevState !== null) { - const prevBaseLanes = prevState.baseLanes; - nextBaseLanes = mergeLanes(prevBaseLanes, renderLanes); - if (enableCache) { - // Save the cache pool so we can resume later. - spawnedCachePool = getOffscreenDeferredCache(); - } - } else { - nextBaseLanes = renderLanes; + // Include the base lanes from the last render + nextBaseLanes = mergeLanes(nextBaseLanes, prevState.baseLanes); } // Schedule this fiber to re-render at offscreen priority. Then bailout. @@ -711,7 +707,8 @@ function updateOffscreenComponent( ); const nextState: OffscreenState = { baseLanes: nextBaseLanes, - cachePool: spawnedCachePool, + // Save the cache pool so we can resume later. + cachePool: enableCache ? getOffscreenDeferredCache() : null, }; workInProgress.memoizedState = nextState; workInProgress.updateQueue = null; @@ -725,7 +722,7 @@ function updateOffscreenComponent( // We're about to bail out, but we need to push this to the stack anyway // to avoid a push/pop misalignment. - pushRenderLanes(workInProgress, nextBaseLanes); + reuseHiddenContextOnStack(workInProgress); if (enableLazyContextPropagation && current !== null) { // Since this tree will resume rendering in a separate render, we need @@ -749,9 +746,6 @@ function updateOffscreenComponent( cachePool: null, }; workInProgress.memoizedState = nextState; - // Push the lanes that were skipped when we bailed out. - const subtreeRenderLanes = - prevState !== null ? prevState.baseLanes : renderLanes; if (enableCache && current !== null) { // If the render that spawned this one accessed the cache pool, resume // using the same cache. Unless the parent changed, since that means @@ -762,16 +756,17 @@ function updateOffscreenComponent( pushTransition(workInProgress, prevCachePool, null); } - pushRenderLanes(workInProgress, subtreeRenderLanes); + // Push the lanes that were skipped when we bailed out. + if (prevState !== null) { + pushHiddenContext(workInProgress, prevState); + } else { + reuseHiddenContextOnStack(workInProgress); + } } } else { // Rendering a visible tree. - let subtreeRenderLanes; if (prevState !== null) { // We're going from hidden -> visible. - - subtreeRenderLanes = mergeLanes(prevState.baseLanes, renderLanes); - let prevCachePool = null; if (enableCache) { // If the render that spawned this one accessed the cache pool, resume @@ -789,13 +784,15 @@ function updateOffscreenComponent( pushTransition(workInProgress, prevCachePool, transitions); + // Push the lanes that were skipped when we bailed out. + pushHiddenContext(workInProgress, prevState); + // Since we're not hidden anymore, reset the state workInProgress.memoizedState = null; } else { // We weren't previously hidden, and we still aren't, so there's nothing // special to do. Need to push to the stack regardless, though, to avoid // a push/pop misalignment. - subtreeRenderLanes = renderLanes; if (enableCache) { // If the render that spawned this one accessed the cache pool, resume @@ -805,8 +802,11 @@ function updateOffscreenComponent( pushTransition(workInProgress, null, null); } } + + // We're about to bail out, but we need to push this to the stack anyway + // to avoid a push/pop misalignment. + reuseHiddenContextOnStack(workInProgress); } - pushRenderLanes(workInProgress, subtreeRenderLanes); } reconcileChildren(current, workInProgress, nextChildren, renderLanes); diff --git a/packages/react-reconciler/src/ReactFiberCompleteWork.new.js b/packages/react-reconciler/src/ReactFiberCompleteWork.new.js index 66b484a3d461c..dd44e9bf8fa06 100644 --- a/packages/react-reconciler/src/ReactFiberCompleteWork.new.js +++ b/packages/react-reconciler/src/ReactFiberCompleteWork.new.js @@ -117,6 +117,7 @@ import { ForceSuspenseFallback, setDefaultShallowSuspenseContext, } from './ReactFiberSuspenseContext.new'; +import {popHiddenContext} from './ReactFiberHiddenContext.new'; import {findFirstSuspended} from './ReactFiberSuspenseComponent.new'; import { isContextProvider as isLegacyContextProvider, @@ -146,9 +147,7 @@ import { renderDidSuspend, renderDidSuspendDelayIfPossible, renderHasNotSuspendedYet, - popRenderLanes, getRenderTargetTime, - subtreeRenderLanes, getWorkInProgressTransitions, } from './ReactFiberWorkLoop.new'; import { @@ -1499,7 +1498,7 @@ function completeWork( } case OffscreenComponent: case LegacyHiddenComponent: { - popRenderLanes(workInProgress); + popHiddenContext(workInProgress); const nextState: OffscreenState | null = workInProgress.memoizedState; const nextIsHidden = nextState !== null; @@ -1520,7 +1519,7 @@ function completeWork( } else { // Don't bubble properties for hidden children unless we're rendering // at offscreen priority. - if (includesSomeLane(subtreeRenderLanes, (OffscreenLane: Lane))) { + if (includesSomeLane(renderLanes, (OffscreenLane: Lane))) { bubbleProperties(workInProgress); // Check if there was an insertion or update in the hidden subtree. // If so, we need to hide those nodes in the commit phase, so diff --git a/packages/react-reconciler/src/ReactFiberHiddenContext.new.js b/packages/react-reconciler/src/ReactFiberHiddenContext.new.js new file mode 100644 index 0000000000000..d64d4bb9dc1a9 --- /dev/null +++ b/packages/react-reconciler/src/ReactFiberHiddenContext.new.js @@ -0,0 +1,66 @@ +/** + * Copyright (c) Facebook, Inc. and its affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + * @flow + */ + +import type {Fiber} from './ReactInternalTypes'; +import type {StackCursor} from './ReactFiberStack.new'; +import type {Lanes} from './ReactFiberLane.new'; + +import {createCursor, push, pop} from './ReactFiberStack.new'; + +import {getRenderLanes, setRenderLanes} from './ReactFiberWorkLoop.new'; +import {NoLanes, mergeLanes} from './ReactFiberLane.new'; + +// TODO: Remove `renderLanes` context in favor of hidden context +type HiddenContext = { + // Represents the lanes that must be included when processing updates in + // order to reveal the hidden content. + // TODO: Remove `subtreeLanes` context from work loop in favor of this one. + baseLanes: number, +}; + +// TODO: This isn't being used yet, but it's intended to replace the +// InvisibleParentContext that is currently managed by SuspenseContext. +export const currentTreeHiddenStackCursor: StackCursor = createCursor( + null, +); +export const prevRenderLanesStackCursor: StackCursor = createCursor( + NoLanes, +); + +export function pushHiddenContext(fiber: Fiber, context: HiddenContext): void { + const prevRenderLanes = getRenderLanes(); + push(prevRenderLanesStackCursor, prevRenderLanes, fiber); + push(currentTreeHiddenStackCursor, context, fiber); + + // When rendering a subtree that's currently hidden, we must include all + // lanes that would have rendered if the hidden subtree hadn't been deferred. + // That is, in order to reveal content from hidden -> visible, we must commit + // all the updates that we skipped when we originally hid the tree. + setRenderLanes(mergeLanes(prevRenderLanes, context.baseLanes)); +} + +export function reuseHiddenContextOnStack(fiber: Fiber): void { + // This subtree is not currently hidden, so we don't need to add any lanes + // to the render lanes. But we still need to push something to avoid a + // context mismatch. Reuse the existing context on the stack. + push(prevRenderLanesStackCursor, getRenderLanes(), fiber); + push( + currentTreeHiddenStackCursor, + currentTreeHiddenStackCursor.current, + fiber, + ); +} + +export function popHiddenContext(fiber: Fiber): void { + // Restore the previous render lanes from the stack + setRenderLanes(prevRenderLanesStackCursor.current); + + pop(currentTreeHiddenStackCursor, fiber); + pop(prevRenderLanesStackCursor, fiber); +} diff --git a/packages/react-reconciler/src/ReactFiberHiddenContext.old.js b/packages/react-reconciler/src/ReactFiberHiddenContext.old.js new file mode 100644 index 0000000000000..087fc9e69be5c --- /dev/null +++ b/packages/react-reconciler/src/ReactFiberHiddenContext.old.js @@ -0,0 +1 @@ +// Intentionally blank. File only exists in new reconciler fork. diff --git a/packages/react-reconciler/src/ReactFiberUnwindWork.new.js b/packages/react-reconciler/src/ReactFiberUnwindWork.new.js index 03788ff2c9437..87376cffcd19a 100644 --- a/packages/react-reconciler/src/ReactFiberUnwindWork.new.js +++ b/packages/react-reconciler/src/ReactFiberUnwindWork.new.js @@ -37,6 +37,7 @@ import { import {popHostContainer, popHostContext} from './ReactFiberHostContext.new'; import {popSuspenseContext} from './ReactFiberSuspenseContext.new'; +import {popHiddenContext} from './ReactFiberHiddenContext.new'; import {resetHydrationState} from './ReactFiberHydrationContext.new'; import { isContextProvider as isLegacyContextProvider, @@ -44,7 +45,6 @@ import { popTopLevelContextObject as popTopLevelLegacyContextObject, } from './ReactFiberContext.new'; import {popProvider} from './ReactFiberNewContext.new'; -import {popRenderLanes} from './ReactFiberWorkLoop.new'; import {popCacheProvider} from './ReactFiberCacheComponent.new'; import {transferActualDuration} from './ReactProfilerTimer.new'; import {popTreeContext} from './ReactFiberTreeContext.new'; @@ -151,7 +151,7 @@ function unwindWork( return null; case OffscreenComponent: case LegacyHiddenComponent: - popRenderLanes(workInProgress); + popHiddenContext(workInProgress); popTransition(workInProgress, current); return null; case CacheComponent: @@ -219,7 +219,7 @@ function unwindInterruptedWork( break; case OffscreenComponent: case LegacyHiddenComponent: - popRenderLanes(interruptedWork); + popHiddenContext(interruptedWork); popTransition(interruptedWork, current); break; case CacheComponent: diff --git a/packages/react-reconciler/src/ReactFiberWorkLoop.new.js b/packages/react-reconciler/src/ReactFiberWorkLoop.new.js index edf3c7ee76745..de03d9f1daa28 100644 --- a/packages/react-reconciler/src/ReactFiberWorkLoop.new.js +++ b/packages/react-reconciler/src/ReactFiberWorkLoop.new.js @@ -11,7 +11,6 @@ import type {Wakeable} from 'shared/ReactTypes'; import type {Fiber, FiberRoot} from './ReactInternalTypes'; import type {Lanes, Lane} from './ReactFiberLane.new'; import type {SuspenseState} from './ReactFiberSuspenseComponent.new'; -import type {StackCursor} from './ReactFiberStack.new'; import type {Flags} from './ReactFiberFlags'; import type {FunctionComponentUpdateQueue} from './ReactFiberHooks.new'; import type {EventPriority} from './ReactEventPriorities.new'; @@ -192,11 +191,6 @@ import { createCapturedValueAtFiber, type CapturedValue, } from './ReactCapturedValue'; -import { - push as pushToStack, - pop as popFromStack, - createCursor, -} from './ReactFiberStack.new'; import { enqueueConcurrentRenderForLane, finishQueueingConcurrentUpdates, @@ -285,26 +279,20 @@ let workInProgress: Fiber | null = null; // The lanes we're rendering let workInProgressRootRenderLanes: Lanes = NoLanes; -// Stack that allows components to change the render lanes for its subtree -// This is a superset of the lanes we started working on at the root. The only -// case where it's different from `workInProgressRootRenderLanes` is when we -// enter a subtree that is hidden and needs to be unhidden: Suspense and -// Offscreen component. +// A contextual version of workInProgressRootRenderLanes. It is a superset of +// the lanes that we started working on at the root. When we enter a subtree +// that is currently hidden, we add the lanes that would have committed if +// the hidden tree hadn't been deferred. This is modified by the +// HiddenContext module. // // Most things in the work loop should deal with workInProgressRootRenderLanes. -// Most things in begin/complete phases should deal with subtreeRenderLanes. -export let subtreeRenderLanes: Lanes = NoLanes; -const subtreeRenderLanesCursor: StackCursor = createCursor(NoLanes); +// Most things in begin/complete phases should deal with renderLanes. +export let renderLanes: Lanes = NoLanes; // Whether to root completed, errored, suspended, etc. let workInProgressRootExitStatus: RootExitStatus = RootInProgress; // A fatal error, if one is thrown let workInProgressRootFatalError: mixed = null; -// "Included" lanes refer to lanes that were worked on during this render. It's -// slightly different than `renderLanes` because `renderLanes` can change as you -// enter and exit an Offscreen tree. This value is the combination of all render -// lanes for the entire render phase. -let workInProgressRootIncludedLanes: Lanes = NoLanes; // The work left over by components that were visited during this render. Only // includes unprocessed updates, not work in bailed out children. let workInProgressRootSkippedLanes: Lanes = NoLanes; @@ -1455,18 +1443,16 @@ export function flushControlled(fn: () => mixed): void { } } -export function pushRenderLanes(fiber: Fiber, lanes: Lanes) { - pushToStack(subtreeRenderLanesCursor, subtreeRenderLanes, fiber); - subtreeRenderLanes = mergeLanes(subtreeRenderLanes, lanes); - workInProgressRootIncludedLanes = mergeLanes( - workInProgressRootIncludedLanes, - lanes, - ); +// This is called by the HiddenContext module when we enter or leave a +// hidden subtree. The stack logic is managed there because that's the only +// place that ever modifies it. Which module it lives in doesn't matter for +// performance because this function will get inlined regardless +export function setRenderLanes(subtreeRenderLanes: Lanes) { + renderLanes = subtreeRenderLanes; } -export function popRenderLanes(fiber: Fiber) { - subtreeRenderLanes = subtreeRenderLanesCursor.current; - popFromStack(subtreeRenderLanesCursor, fiber); +export function getRenderLanes(): Lanes { + return renderLanes; } function prepareFreshStack(root: FiberRoot, lanes: Lanes): Fiber { @@ -1497,7 +1483,7 @@ function prepareFreshStack(root: FiberRoot, lanes: Lanes): Fiber { workInProgressRoot = root; const rootWorkInProgress = createWorkInProgress(root.current, null); workInProgress = rootWorkInProgress; - workInProgressRootRenderLanes = subtreeRenderLanes = workInProgressRootIncludedLanes = lanes; + workInProgressRootRenderLanes = renderLanes = lanes; workInProgressRootExitStatus = RootInProgress; workInProgressRootFatalError = null; workInProgressRootSkippedLanes = NoLanes; @@ -1864,10 +1850,10 @@ function performUnitOfWork(unitOfWork: Fiber): void { let next; if (enableProfilerTimer && (unitOfWork.mode & ProfileMode) !== NoMode) { startProfilerTimer(unitOfWork); - next = beginWork(current, unitOfWork, subtreeRenderLanes); + next = beginWork(current, unitOfWork, renderLanes); stopProfilerTimerIfRunningAndRecordDelta(unitOfWork, true); } else { - next = beginWork(current, unitOfWork, subtreeRenderLanes); + next = beginWork(current, unitOfWork, renderLanes); } resetCurrentDebugFiberInDEV(); @@ -1901,10 +1887,10 @@ function completeUnitOfWork(unitOfWork: Fiber): void { !enableProfilerTimer || (completedWork.mode & ProfileMode) === NoMode ) { - next = completeWork(current, completedWork, subtreeRenderLanes); + next = completeWork(current, completedWork, renderLanes); } else { startProfilerTimer(completedWork); - next = completeWork(current, completedWork, subtreeRenderLanes); + next = completeWork(current, completedWork, renderLanes); // Update render duration assuming we didn't error. stopProfilerTimerIfRunningAndRecordDelta(completedWork, false); } @@ -1919,7 +1905,7 @@ function completeUnitOfWork(unitOfWork: Fiber): void { // This fiber did not complete because something threw. Pop values off // the stack without entering the complete phase. If this is a boundary, // capture values if possible. - const next = unwindWork(current, completedWork, subtreeRenderLanes); + const next = unwindWork(current, completedWork, renderLanes); // Because this fiber did not complete, don't reset its lanes. From a60ae2efad6b0044bb0176d555267637b06938dc Mon Sep 17 00:00:00 2001 From: Andrew Clark Date: Thu, 9 Jun 2022 09:53:19 -0400 Subject: [PATCH 2/4] Add previous commit to forked revisions --- scripts/merge-fork/forked-revisions | 1 + 1 file changed, 1 insertion(+) diff --git a/scripts/merge-fork/forked-revisions b/scripts/merge-fork/forked-revisions index e69de29bb2d1d..a1d798dd1f3f9 100644 --- a/scripts/merge-fork/forked-revisions +++ b/scripts/merge-fork/forked-revisions @@ -0,0 +1 @@ +051ac55cb75f426b81f8f75b143f34255476b9bc [FORKED] Add HiddenContext to track if subtree is hidden \ No newline at end of file From 6ab05ee2e9c5b1f4c8dc1f7ae8906bf613788ba7 Mon Sep 17 00:00:00 2001 From: Andrew Clark Date: Wed, 18 May 2022 16:42:26 -0400 Subject: [PATCH 3/4] [FORKED] Track nearest Suspense handler on stack Instead of traversing the return path whenever something suspends to find the nearest Suspense boundary, we can push the Suspense boundary onto the stack before entering its subtree. This doesn't affect the overall algorithm that much, but because we already do all the same logic in the begin phase, we can save some redundant work by tracking that information on the stack instead of recomputing it every time. --- .../src/ReactFiberBeginWork.new.js | 132 ++++++++---------- .../src/ReactFiberCompleteWork.new.js | 71 +++++----- .../src/ReactFiberHiddenContext.new.js | 4 + .../src/ReactFiberSuspenseComponent.new.js | 32 ----- .../src/ReactFiberSuspenseContext.new.js | 112 +++++++++++---- .../src/ReactFiberThrow.new.js | 33 +---- .../src/ReactFiberUnwindWork.new.js | 13 +- 7 files changed, 197 insertions(+), 200 deletions(-) diff --git a/packages/react-reconciler/src/ReactFiberBeginWork.new.js b/packages/react-reconciler/src/ReactFiberBeginWork.new.js index fed99735b8ba0..e4f4d7173b4b2 100644 --- a/packages/react-reconciler/src/ReactFiberBeginWork.new.js +++ b/packages/react-reconciler/src/ReactFiberBeginWork.new.js @@ -37,7 +37,6 @@ import type {UpdateQueue} from './ReactFiberClassUpdateQueue.new'; import type {RootState} from './ReactFiberRoot.new'; import type {TracingMarkerInstance} from './ReactFiberTracingMarkerComponent.new'; import { - enableSuspenseAvoidThisFallback, enableCPUSuspense, enableUseMutableSource, } from 'shared/ReactFeatureFlags'; @@ -167,13 +166,14 @@ import {shouldError, shouldSuspend} from './ReactFiberReconciler'; import {pushHostContext, pushHostContainer} from './ReactFiberHostContext.new'; import { suspenseStackCursor, - pushSuspenseContext, - InvisibleParentSuspenseContext, + pushSuspenseListContext, ForceSuspenseFallback, - hasSuspenseContext, - setDefaultShallowSuspenseContext, - addSubtreeSuspenseContext, - setShallowSuspenseContext, + hasSuspenseListContext, + setDefaultShallowSuspenseListContext, + setShallowSuspenseListContext, + pushPrimaryTreeSuspenseHandler, + pushFallbackTreeSuspenseHandler, + popSuspenseHandler, } from './ReactFiberSuspenseContext.new'; import { pushHiddenContext, @@ -1969,7 +1969,6 @@ function updateSuspenseOffscreenState( // TODO: Probably should inline this back function shouldRemainOnFallback( - suspenseContext: SuspenseContext, current: null | Fiber, workInProgress: Fiber, renderLanes: Lanes, @@ -1989,7 +1988,8 @@ function shouldRemainOnFallback( } // Not currently showing content. Consult the Suspense context. - return hasSuspenseContext( + const suspenseContext: SuspenseContext = suspenseStackCursor.current; + return hasSuspenseListContext( suspenseContext, (ForceSuspenseFallback: SuspenseContext), ); @@ -2010,50 +2010,18 @@ function updateSuspenseComponent(current, workInProgress, renderLanes) { } } - let suspenseContext: SuspenseContext = suspenseStackCursor.current; - let showFallback = false; const didSuspend = (workInProgress.flags & DidCapture) !== NoFlags; - if ( didSuspend || - shouldRemainOnFallback( - suspenseContext, - current, - workInProgress, - renderLanes, - ) + shouldRemainOnFallback(current, workInProgress, renderLanes) ) { // Something in this boundary's subtree already suspended. Switch to // rendering the fallback children. showFallback = true; workInProgress.flags &= ~DidCapture; - } else { - // Attempting the main content - if ( - current === null || - (current.memoizedState: null | SuspenseState) !== null - ) { - // This is a new mount or this boundary is already showing a fallback state. - // Mark this subtree context as having at least one invisible parent that could - // handle the fallback state. - // Avoided boundaries are not considered since they cannot handle preferred fallback states. - if ( - !enableSuspenseAvoidThisFallback || - nextProps.unstable_avoidThisFallback !== true - ) { - suspenseContext = addSubtreeSuspenseContext( - suspenseContext, - InvisibleParentSuspenseContext, - ); - } - } } - suspenseContext = setDefaultShallowSuspenseContext(suspenseContext); - - pushSuspenseContext(workInProgress, suspenseContext); - // OK, the next part is confusing. We're about to reconcile the Suspense // boundary's children. This involves some custom reconciliation logic. Two // main reasons this is so complicated. @@ -2081,24 +2049,40 @@ function updateSuspenseComponent(current, workInProgress, renderLanes) { // Special path for hydration // If we're currently hydrating, try to hydrate this boundary. - tryToClaimNextHydratableInstance(workInProgress); - // This could've been a dehydrated suspense component. - const suspenseState: null | SuspenseState = workInProgress.memoizedState; - if (suspenseState !== null) { - const dehydrated = suspenseState.dehydrated; - if (dehydrated !== null) { - return mountDehydratedSuspenseComponent( - workInProgress, - dehydrated, - renderLanes, - ); + if (getIsHydrating()) { + // We must push the suspense handler context *before* attempting to + // hydrate, to avoid a mismatch in case it errors. + if (showFallback) { + pushPrimaryTreeSuspenseHandler(workInProgress); + } else { + pushFallbackTreeSuspenseHandler(workInProgress); + } + tryToClaimNextHydratableInstance(workInProgress); + // This could've been a dehydrated suspense component. + const suspenseState: null | SuspenseState = workInProgress.memoizedState; + if (suspenseState !== null) { + const dehydrated = suspenseState.dehydrated; + if (dehydrated !== null) { + return mountDehydratedSuspenseComponent( + workInProgress, + dehydrated, + renderLanes, + ); + } } + // If hydration didn't succeed, fall through to the normal Suspense path. + // To avoid a stack mismatch we need to pop the Suspense handler that we + // pushed above. This will become less awkward when move the hydration + // logic to its own fiber. + popSuspenseHandler(workInProgress); } const nextPrimaryChildren = nextProps.children; const nextFallbackChildren = nextProps.fallback; if (showFallback) { + pushFallbackTreeSuspenseHandler(workInProgress); + const fallbackFragment = mountSuspenseFallbackChildren( workInProgress, nextPrimaryChildren, @@ -2131,6 +2115,7 @@ function updateSuspenseComponent(current, workInProgress, renderLanes) { // This is a CPU-bound tree. Skip this tree and show a placeholder to // unblock the surrounding content. Then immediately retry after the // initial commit. + pushFallbackTreeSuspenseHandler(workInProgress); const fallbackFragment = mountSuspenseFallbackChildren( workInProgress, nextPrimaryChildren, @@ -2154,6 +2139,7 @@ function updateSuspenseComponent(current, workInProgress, renderLanes) { workInProgress.lanes = SomeRetryLane; return fallbackFragment; } else { + pushPrimaryTreeSuspenseHandler(workInProgress); return mountSuspensePrimaryChildren( workInProgress, nextPrimaryChildren, @@ -2181,6 +2167,8 @@ function updateSuspenseComponent(current, workInProgress, renderLanes) { } if (showFallback) { + pushFallbackTreeSuspenseHandler(workInProgress); + const nextFallbackChildren = nextProps.fallback; const nextPrimaryChildren = nextProps.children; const fallbackChildFragment = updateSuspenseFallbackChildren( @@ -2215,6 +2203,8 @@ function updateSuspenseComponent(current, workInProgress, renderLanes) { workInProgress.memoizedState = SUSPENDED_MARKER; return fallbackChildFragment; } else { + pushPrimaryTreeSuspenseHandler(workInProgress); + const nextPrimaryChildren = nextProps.children; const primaryChildFragment = updateSuspensePrimaryChildren( current, @@ -2585,6 +2575,7 @@ function updateDehydratedSuspenseComponent( ): null | Fiber { if (!didSuspend) { // This is the first render pass. Attempt to hydrate. + pushPrimaryTreeSuspenseHandler(workInProgress); // We should never be hydrating at this point because it is the first pass, // but after we've already committed once. @@ -2751,6 +2742,8 @@ function updateDehydratedSuspenseComponent( if (workInProgress.flags & ForceClientRender) { // Something errored during hydration. Try again without hydrating. + pushPrimaryTreeSuspenseHandler(workInProgress); + workInProgress.flags &= ~ForceClientRender; const capturedValue = createCapturedValue( new Error( @@ -2767,6 +2760,10 @@ function updateDehydratedSuspenseComponent( } else if ((workInProgress.memoizedState: null | SuspenseState) !== null) { // Something suspended and we should still be in dehydrated mode. // Leave the existing child in place. + + // Push to avoid a mismatch + pushFallbackTreeSuspenseHandler(workInProgress); + workInProgress.child = current.child; // The dehydrated completion pass expects this flag to be there // but the normal suspense pass doesn't. @@ -2775,6 +2772,8 @@ function updateDehydratedSuspenseComponent( } else { // Suspended but we should no longer be in dehydrated mode. // Therefore we now have to render the fallback. + pushFallbackTreeSuspenseHandler(workInProgress); + const nextPrimaryChildren = nextProps.children; const nextFallbackChildren = nextProps.fallback; const fallbackChildFragment = mountSuspenseFallbackAfterRetryWithoutHydrating( @@ -3070,12 +3069,12 @@ function updateSuspenseListComponent( let suspenseContext: SuspenseContext = suspenseStackCursor.current; - const shouldForceFallback = hasSuspenseContext( + const shouldForceFallback = hasSuspenseListContext( suspenseContext, (ForceSuspenseFallback: SuspenseContext), ); if (shouldForceFallback) { - suspenseContext = setShallowSuspenseContext( + suspenseContext = setShallowSuspenseListContext( suspenseContext, ForceSuspenseFallback, ); @@ -3093,9 +3092,9 @@ function updateSuspenseListComponent( renderLanes, ); } - suspenseContext = setDefaultShallowSuspenseContext(suspenseContext); + suspenseContext = setDefaultShallowSuspenseListContext(suspenseContext); } - pushSuspenseContext(workInProgress, suspenseContext); + pushSuspenseListContext(workInProgress, suspenseContext); if ((workInProgress.mode & ConcurrentMode) === NoMode) { // In legacy mode, SuspenseList doesn't work so we just @@ -3559,10 +3558,9 @@ function attemptEarlyBailoutIfNoScheduledUpdate( const state: SuspenseState | null = workInProgress.memoizedState; if (state !== null) { if (state.dehydrated !== null) { - pushSuspenseContext( - workInProgress, - setDefaultShallowSuspenseContext(suspenseStackCursor.current), - ); + // We're not going to render the children, so this is just to maintain + // push/pop symmetry + pushPrimaryTreeSuspenseHandler(workInProgress); // We know that this component will suspend again because if it has // been unsuspended it has committed as a resolved Suspense component. // If it needs to be retried, it should have work scheduled on it. @@ -3585,10 +3583,7 @@ function attemptEarlyBailoutIfNoScheduledUpdate( } else { // The primary child fragment does not have pending work marked // on it - pushSuspenseContext( - workInProgress, - setDefaultShallowSuspenseContext(suspenseStackCursor.current), - ); + pushPrimaryTreeSuspenseHandler(workInProgress); // The primary children do not have pending work with sufficient // priority. Bailout. const child = bailoutOnAlreadyFinishedWork( @@ -3608,10 +3603,7 @@ function attemptEarlyBailoutIfNoScheduledUpdate( } } } else { - pushSuspenseContext( - workInProgress, - setDefaultShallowSuspenseContext(suspenseStackCursor.current), - ); + pushPrimaryTreeSuspenseHandler(workInProgress); } break; } @@ -3669,7 +3661,7 @@ function attemptEarlyBailoutIfNoScheduledUpdate( renderState.tail = null; renderState.lastEffect = null; } - pushSuspenseContext(workInProgress, suspenseStackCursor.current); + pushSuspenseListContext(workInProgress, suspenseStackCursor.current); if (hasChildWork) { break; diff --git a/packages/react-reconciler/src/ReactFiberCompleteWork.new.js b/packages/react-reconciler/src/ReactFiberCompleteWork.new.js index dd44e9bf8fa06..913abb20c4130 100644 --- a/packages/react-reconciler/src/ReactFiberCompleteWork.new.js +++ b/packages/react-reconciler/src/ReactFiberCompleteWork.new.js @@ -27,7 +27,6 @@ import type { SuspenseState, SuspenseListRenderState, } from './ReactFiberSuspenseComponent.new'; -import type {SuspenseContext} from './ReactFiberSuspenseContext.new'; import type {OffscreenState} from './ReactFiberOffscreenComponent'; import type {Cache} from './ReactFiberCacheComponent.new'; import { @@ -109,15 +108,17 @@ import { } from './ReactFiberHostContext.new'; import { suspenseStackCursor, - InvisibleParentSuspenseContext, - hasSuspenseContext, - popSuspenseContext, - pushSuspenseContext, - setShallowSuspenseContext, + popSuspenseListContext, + popSuspenseHandler, + pushSuspenseListContext, + setShallowSuspenseListContext, ForceSuspenseFallback, - setDefaultShallowSuspenseContext, + setDefaultShallowSuspenseListContext, } from './ReactFiberSuspenseContext.new'; -import {popHiddenContext} from './ReactFiberHiddenContext.new'; +import { + popHiddenContext, + isCurrentTreeHidden, +} from './ReactFiberHiddenContext.new'; import {findFirstSuspended} from './ReactFiberSuspenseComponent.new'; import { isContextProvider as isLegacyContextProvider, @@ -1076,7 +1077,7 @@ function completeWork( return null; } case SuspenseComponent: { - popSuspenseContext(workInProgress); + popSuspenseHandler(workInProgress); const nextState: null | SuspenseState = workInProgress.memoizedState; // Special path for dehydrated boundaries. We may eventually move this @@ -1185,25 +1186,23 @@ function completeWork( // If this render already had a ping or lower pri updates, // and this is the first time we know we're going to suspend we // should be able to immediately restart from within throwException. - const hasInvisibleChildContext = - current === null && - (workInProgress.memoizedProps.unstable_avoidThisFallback !== - true || - !enableSuspenseAvoidThisFallback); - if ( - hasInvisibleChildContext || - hasSuspenseContext( - suspenseStackCursor.current, - (InvisibleParentSuspenseContext: SuspenseContext), - ) - ) { - // If this was in an invisible tree or a new render, then showing - // this boundary is ok. - renderDidSuspend(); - } else { - // Otherwise, we're going to have to hide content so we should - // suspend for longer if possible. + + // Check if this is a "bad" fallback state or a good one. A bad + // fallback state is one that we only show as a last resort; if this + // is a transition, we'll block it from displaying, and wait for + // more data to arrive. + const isBadFallback = + // It's bad to switch to a fallback if content is already visible + (current !== null && !prevDidTimeout && !isCurrentTreeHidden()) || + // Experimental: Some fallbacks are always bad + (enableSuspenseAvoidThisFallback && + workInProgress.memoizedProps.unstable_avoidThisFallback === + true); + + if (isBadFallback) { renderDidSuspendDelayIfPossible(); + } else { + renderDidSuspend(); } } } @@ -1265,7 +1264,7 @@ function completeWork( return null; } case SuspenseListComponent: { - popSuspenseContext(workInProgress); + popSuspenseListContext(workInProgress); const renderState: null | SuspenseListRenderState = workInProgress.memoizedState; @@ -1331,11 +1330,11 @@ function completeWork( workInProgress.subtreeFlags = NoFlags; resetChildFibers(workInProgress, renderLanes); - // Set up the Suspense Context to force suspense and immediately - // rerender the children. - pushSuspenseContext( + // Set up the Suspense List Context to force suspense and + // immediately rerender the children. + pushSuspenseListContext( workInProgress, - setShallowSuspenseContext( + setShallowSuspenseListContext( suspenseStackCursor.current, ForceSuspenseFallback, ), @@ -1458,14 +1457,16 @@ function completeWork( // setting it the first time we go from not suspended to suspended. let suspenseContext = suspenseStackCursor.current; if (didSuspendAlready) { - suspenseContext = setShallowSuspenseContext( + suspenseContext = setShallowSuspenseListContext( suspenseContext, ForceSuspenseFallback, ); } else { - suspenseContext = setDefaultShallowSuspenseContext(suspenseContext); + suspenseContext = setDefaultShallowSuspenseListContext( + suspenseContext, + ); } - pushSuspenseContext(workInProgress, suspenseContext); + pushSuspenseListContext(workInProgress, suspenseContext); // Do a pass over the next row. // Don't bubble properties in this case. return next; diff --git a/packages/react-reconciler/src/ReactFiberHiddenContext.new.js b/packages/react-reconciler/src/ReactFiberHiddenContext.new.js index d64d4bb9dc1a9..6f6309d6edb94 100644 --- a/packages/react-reconciler/src/ReactFiberHiddenContext.new.js +++ b/packages/react-reconciler/src/ReactFiberHiddenContext.new.js @@ -64,3 +64,7 @@ export function popHiddenContext(fiber: Fiber): void { pop(currentTreeHiddenStackCursor, fiber); pop(prevRenderLanesStackCursor, fiber); } + +export function isCurrentTreeHidden() { + return currentTreeHiddenStackCursor.current !== null; +} diff --git a/packages/react-reconciler/src/ReactFiberSuspenseComponent.new.js b/packages/react-reconciler/src/ReactFiberSuspenseComponent.new.js index a5952ace94745..a67a5fd8e6d1e 100644 --- a/packages/react-reconciler/src/ReactFiberSuspenseComponent.new.js +++ b/packages/react-reconciler/src/ReactFiberSuspenseComponent.new.js @@ -13,7 +13,6 @@ import type {SuspenseInstance} from './ReactFiberHostConfig'; import type {Lane} from './ReactFiberLane.new'; import type {TreeContext} from './ReactFiberTreeContext.new'; -import {enableSuspenseAvoidThisFallback} from 'shared/ReactFeatureFlags'; import {SuspenseComponent, SuspenseListComponent} from './ReactWorkTags'; import {NoFlags, DidCapture} from './ReactFiberFlags'; import { @@ -67,37 +66,6 @@ export type SuspenseListRenderState = {| tailMode: SuspenseListTailMode, |}; -export function shouldCaptureSuspense( - workInProgress: Fiber, - hasInvisibleParent: boolean, -): boolean { - // If it was the primary children that just suspended, capture and render the - // fallback. Otherwise, don't capture and bubble to the next boundary. - const nextState: SuspenseState | null = workInProgress.memoizedState; - if (nextState !== null) { - if (nextState.dehydrated !== null) { - // A dehydrated boundary always captures. - return true; - } - return false; - } - const props = workInProgress.memoizedProps; - // Regular boundaries always capture. - if ( - !enableSuspenseAvoidThisFallback || - props.unstable_avoidThisFallback !== true - ) { - return true; - } - // If it's a boundary we should avoid, then we prefer to bubble up to the - // parent boundary if it is currently invisible. - if (hasInvisibleParent) { - return false; - } - // If the parent is not able to handle it, we must handle it. - return true; -} - export function findFirstSuspended(row: Fiber): null | Fiber { let node = row; while (node !== null) { diff --git a/packages/react-reconciler/src/ReactFiberSuspenseContext.new.js b/packages/react-reconciler/src/ReactFiberSuspenseContext.new.js index fd6892063091e..a49862d3fad85 100644 --- a/packages/react-reconciler/src/ReactFiberSuspenseContext.new.js +++ b/packages/react-reconciler/src/ReactFiberSuspenseContext.new.js @@ -9,33 +9,96 @@ import type {Fiber} from './ReactInternalTypes'; import type {StackCursor} from './ReactFiberStack.new'; +import type {SuspenseState} from './ReactFiberSuspenseComponent.new'; +import {enableSuspenseAvoidThisFallback} from 'shared/ReactFeatureFlags'; import {createCursor, push, pop} from './ReactFiberStack.new'; +import {isCurrentTreeHidden} from './ReactFiberHiddenContext.new'; +import {SuspenseComponent} from './ReactWorkTags'; +// The Suspense handler is the boundary that should capture if something +// suspends, i.e. it's the nearest `catch` block on the stack. +const suspenseHandlerStackCursor: StackCursor = createCursor( + null, +); + +function shouldAvoidedBoundaryCapture( + workInProgress: Fiber, + handlerOnStack: Fiber, + props: any, +): boolean { + if (enableSuspenseAvoidThisFallback) { + // If the parent is already showing content, and we're not inside a hidden + // tree, then we should show the avoided fallback. + if (handlerOnStack.alternate !== null && !isCurrentTreeHidden()) { + return true; + } + + // If the handler on the stack is also an avoided boundary, then we should + // favor this inner one. + if ( + handlerOnStack.tag === SuspenseComponent && + handlerOnStack.memoizedProps.unstable_avoidThisFallback === true + ) { + return true; + } + + // If this avoided boundary is dehydrated, then it should capture. + const suspenseState: SuspenseState | null = workInProgress.memoizedState; + if (suspenseState !== null && suspenseState.dehydrated !== null) { + return true; + } + } + + // If none of those cases apply, then we should avoid this fallback and show + // the outer one instead. + return false; +} + +export function pushPrimaryTreeSuspenseHandler(handler: Fiber): void { + const props = handler.pendingProps; + const handlerOnStack = suspenseHandlerStackCursor.current; + if ( + enableSuspenseAvoidThisFallback && + props.unstable_avoidThisFallback === true && + handlerOnStack !== null && + !shouldAvoidedBoundaryCapture(handler, handlerOnStack, props) + ) { + // This boundary should not capture if something suspends. Reuse the + // existing handler on the stack. + push(suspenseHandlerStackCursor, handlerOnStack, handler); + } else { + // Push this handler onto the stack. + push(suspenseHandlerStackCursor, handler, handler); + } +} + +export function pushFallbackTreeSuspenseHandler(fiber: Fiber): void { + // We're about to render the fallback. If something in the fallback suspends, + // it's akin to throwing inside of a `catch` block. This boundary should not + // capture. Reuse the existing handler on the stack. + push(suspenseHandlerStackCursor, getSuspenseHandler(), fiber); +} + +export function getSuspenseHandler(): Fiber | null { + return suspenseHandlerStackCursor.current; +} + +export function popSuspenseHandler(fiber: Fiber): void { + pop(suspenseHandlerStackCursor, fiber); +} + +// SuspenseList context +// TODO: Move to a separate module? We may change the SuspenseList +// implementation to hide/show in the commit phase, anyway. export opaque type SuspenseContext = number; export opaque type SubtreeSuspenseContext: SuspenseContext = number; export opaque type ShallowSuspenseContext: SuspenseContext = number; const DefaultSuspenseContext: SuspenseContext = 0b00; -// The Suspense Context is split into two parts. The lower bits is -// inherited deeply down the subtree. The upper bits only affect -// this immediate suspense boundary and gets reset each new -// boundary or suspense list. const SubtreeSuspenseContextMask: SuspenseContext = 0b01; -// Subtree Flags: - -// InvisibleParentSuspenseContext indicates that one of our parent Suspense -// boundaries is not currently showing visible main content. -// Either because it is already showing a fallback or is not mounted at all. -// We can use this to determine if it is desirable to trigger a fallback at -// the parent. If not, then we might need to trigger undesirable boundaries -// and/or suspend the commit to avoid hiding the parent content. -export const InvisibleParentSuspenseContext: SubtreeSuspenseContext = 0b01; - -// Shallow Flags: - // ForceSuspenseFallback can be used by SuspenseList to force newly added // items into their fallback state during one of the render passes. export const ForceSuspenseFallback: ShallowSuspenseContext = 0b10; @@ -44,40 +107,33 @@ export const suspenseStackCursor: StackCursor = createCursor( DefaultSuspenseContext, ); -export function hasSuspenseContext( +export function hasSuspenseListContext( parentContext: SuspenseContext, flag: SuspenseContext, ): boolean { return (parentContext & flag) !== 0; } -export function setDefaultShallowSuspenseContext( +export function setDefaultShallowSuspenseListContext( parentContext: SuspenseContext, ): SuspenseContext { return parentContext & SubtreeSuspenseContextMask; } -export function setShallowSuspenseContext( +export function setShallowSuspenseListContext( parentContext: SuspenseContext, shallowContext: ShallowSuspenseContext, ): SuspenseContext { return (parentContext & SubtreeSuspenseContextMask) | shallowContext; } -export function addSubtreeSuspenseContext( - parentContext: SuspenseContext, - subtreeContext: SubtreeSuspenseContext, -): SuspenseContext { - return parentContext | subtreeContext; -} - -export function pushSuspenseContext( +export function pushSuspenseListContext( fiber: Fiber, newContext: SuspenseContext, ): void { push(suspenseStackCursor, newContext, fiber); } -export function popSuspenseContext(fiber: Fiber): void { +export function popSuspenseListContext(fiber: Fiber): void { pop(suspenseStackCursor, fiber); } diff --git a/packages/react-reconciler/src/ReactFiberThrow.new.js b/packages/react-reconciler/src/ReactFiberThrow.new.js index f3dc2edf00f01..92af7312a78c1 100644 --- a/packages/react-reconciler/src/ReactFiberThrow.new.js +++ b/packages/react-reconciler/src/ReactFiberThrow.new.js @@ -13,13 +13,11 @@ import type {Lane, Lanes} from './ReactFiberLane.new'; import type {CapturedValue} from './ReactCapturedValue'; import type {Update} from './ReactFiberClassUpdateQueue.new'; import type {Wakeable} from 'shared/ReactTypes'; -import type {SuspenseContext} from './ReactFiberSuspenseContext.new'; import getComponentNameFromFiber from 'react-reconciler/src/getComponentNameFromFiber'; import { ClassComponent, HostRoot, - SuspenseComponent, IncompleteClassComponent, FunctionComponent, ForwardRef, @@ -34,7 +32,6 @@ import { ForceUpdateForLegacySuspense, ForceClientRender, } from './ReactFiberFlags'; -import {shouldCaptureSuspense} from './ReactFiberSuspenseComponent.new'; import {NoMode, ConcurrentMode, DebugTracingMode} from './ReactTypeOfMode'; import { enableDebugTracing, @@ -50,11 +47,7 @@ import { enqueueUpdate, } from './ReactFiberClassUpdateQueue.new'; import {markFailedErrorBoundaryForHotReloading} from './ReactFiberHotReloading.new'; -import { - suspenseStackCursor, - InvisibleParentSuspenseContext, - hasSuspenseContext, -} from './ReactFiberSuspenseContext.new'; +import {getSuspenseHandler} from './ReactFiberSuspenseContext.new'; import { renderDidError, renderDidSuspendDelayIfPossible, @@ -269,26 +262,6 @@ function resetSuspendedComponent(sourceFiber: Fiber, rootRenderLanes: Lanes) { } } -function getNearestSuspenseBoundaryToCapture(returnFiber: Fiber) { - let node = returnFiber; - const hasInvisibleParentBoundary = hasSuspenseContext( - suspenseStackCursor.current, - (InvisibleParentSuspenseContext: SuspenseContext), - ); - do { - if ( - node.tag === SuspenseComponent && - shouldCaptureSuspense(node, hasInvisibleParentBoundary) - ) { - return node; - } - // This boundary already captured during this render. Continue to the next - // boundary. - node = node.return; - } while (node !== null); - return null; -} - function markSuspenseBoundaryShouldCapture( suspenseBoundary: Fiber, returnFiber: Fiber, @@ -444,7 +417,7 @@ function throwException( } // Schedule the nearest Suspense to re-render the timed out view. - const suspenseBoundary = getNearestSuspenseBoundaryToCapture(returnFiber); + const suspenseBoundary = getSuspenseHandler(); if (suspenseBoundary !== null) { suspenseBoundary.flags &= ~ForceClientRender; markSuspenseBoundaryShouldCapture( @@ -496,7 +469,7 @@ function throwException( // This is a regular error, not a Suspense wakeable. if (getIsHydrating() && sourceFiber.mode & ConcurrentMode) { markDidThrowWhileHydratingDEV(); - const suspenseBoundary = getNearestSuspenseBoundaryToCapture(returnFiber); + const suspenseBoundary = getSuspenseHandler(); // If the error was thrown during hydration, we may be able to recover by // discarding the dehydrated content and switching to a client render. // Instead of surfacing the error, find the nearest Suspense boundary diff --git a/packages/react-reconciler/src/ReactFiberUnwindWork.new.js b/packages/react-reconciler/src/ReactFiberUnwindWork.new.js index 87376cffcd19a..6b2dca917d39b 100644 --- a/packages/react-reconciler/src/ReactFiberUnwindWork.new.js +++ b/packages/react-reconciler/src/ReactFiberUnwindWork.new.js @@ -36,7 +36,10 @@ import { } from 'shared/ReactFeatureFlags'; import {popHostContainer, popHostContext} from './ReactFiberHostContext.new'; -import {popSuspenseContext} from './ReactFiberSuspenseContext.new'; +import { + popSuspenseListContext, + popSuspenseHandler, +} from './ReactFiberSuspenseContext.new'; import {popHiddenContext} from './ReactFiberHiddenContext.new'; import {resetHydrationState} from './ReactFiberHydrationContext.new'; import { @@ -109,7 +112,7 @@ function unwindWork( return null; } case SuspenseComponent: { - popSuspenseContext(workInProgress); + popSuspenseHandler(workInProgress); const suspenseState: null | SuspenseState = workInProgress.memoizedState; if (suspenseState !== null && suspenseState.dehydrated !== null) { if (workInProgress.alternate === null) { @@ -137,7 +140,7 @@ function unwindWork( return null; } case SuspenseListComponent: { - popSuspenseContext(workInProgress); + popSuspenseListContext(workInProgress); // SuspenseList doesn't actually catch anything. It should've been // caught by a nested boundary. If not, it should bubble through. return null; @@ -208,10 +211,10 @@ function unwindInterruptedWork( popHostContainer(interruptedWork); break; case SuspenseComponent: - popSuspenseContext(interruptedWork); + popSuspenseHandler(interruptedWork); break; case SuspenseListComponent: - popSuspenseContext(interruptedWork); + popSuspenseListContext(interruptedWork); break; case ContextProvider: const context: ReactContext = interruptedWork.type._context; From 396b786877996b7fc2025e2a7f970d5f6acc26ca Mon Sep 17 00:00:00 2001 From: Andrew Clark Date: Thu, 9 Jun 2022 09:53:19 -0400 Subject: [PATCH 4/4] Add previous commit to forked revisions --- scripts/merge-fork/forked-revisions | 1 + 1 file changed, 1 insertion(+) diff --git a/scripts/merge-fork/forked-revisions b/scripts/merge-fork/forked-revisions index a1d798dd1f3f9..4a723374fb87c 100644 --- a/scripts/merge-fork/forked-revisions +++ b/scripts/merge-fork/forked-revisions @@ -1 +1,2 @@ +6ab05ee2e9c5b1f4c8dc1f7ae8906bf613788ba7 [FORKED] Track nearest Suspense handler on stack 051ac55cb75f426b81f8f75b143f34255476b9bc [FORKED] Add HiddenContext to track if subtree is hidden \ No newline at end of file