diff --git a/packages/react-reconciler/src/ReactFiberBeginWork.new.js b/packages/react-reconciler/src/ReactFiberBeginWork.new.js index 9f60e0ab227e9..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,14 +166,19 @@ 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, + 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); @@ -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 66b484a3d461c..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,14 +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, + isCurrentTreeHidden, +} from './ReactFiberHiddenContext.new'; import {findFirstSuspended} from './ReactFiberSuspenseComponent.new'; import { isContextProvider as isLegacyContextProvider, @@ -146,9 +148,7 @@ import { renderDidSuspend, renderDidSuspendDelayIfPossible, renderHasNotSuspendedYet, - popRenderLanes, getRenderTargetTime, - subtreeRenderLanes, getWorkInProgressTransitions, } from './ReactFiberWorkLoop.new'; import { @@ -1077,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 @@ -1186,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(); } } } @@ -1266,7 +1264,7 @@ function completeWork( return null; } case SuspenseListComponent: { - popSuspenseContext(workInProgress); + popSuspenseListContext(workInProgress); const renderState: null | SuspenseListRenderState = workInProgress.memoizedState; @@ -1332,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, ), @@ -1459,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; @@ -1499,7 +1499,7 @@ function completeWork( } case OffscreenComponent: case LegacyHiddenComponent: { - popRenderLanes(workInProgress); + popHiddenContext(workInProgress); const nextState: OffscreenState | null = workInProgress.memoizedState; const nextIsHidden = nextState !== null; @@ -1520,7 +1520,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..6f6309d6edb94 --- /dev/null +++ b/packages/react-reconciler/src/ReactFiberHiddenContext.new.js @@ -0,0 +1,70 @@ +/** + * 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); +} + +export function isCurrentTreeHidden() { + return currentTreeHiddenStackCursor.current !== null; +} 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/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 03788ff2c9437..6b2dca917d39b 100644 --- a/packages/react-reconciler/src/ReactFiberUnwindWork.new.js +++ b/packages/react-reconciler/src/ReactFiberUnwindWork.new.js @@ -36,7 +36,11 @@ 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 { isContextProvider as isLegacyContextProvider, @@ -44,7 +48,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'; @@ -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; @@ -151,7 +154,7 @@ function unwindWork( return null; case OffscreenComponent: case LegacyHiddenComponent: - popRenderLanes(workInProgress); + popHiddenContext(workInProgress); popTransition(workInProgress, current); return null; case CacheComponent: @@ -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; @@ -219,7 +222,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. diff --git a/scripts/merge-fork/forked-revisions b/scripts/merge-fork/forked-revisions index e69de29bb2d1d..4a723374fb87c 100644 --- a/scripts/merge-fork/forked-revisions +++ b/scripts/merge-fork/forked-revisions @@ -0,0 +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