Land forked reconciler changes (#24878)

This applies forked changes from the "new" reconciler to the "old" one.

Includes:

- 67de5e3 [FORKED] Hidden trees should capture Suspense
- 6ab05ee [FORKED] Track nearest Suspense handler on stack
- 051ac55 [FORKED] Add HiddenContext to track if subtree is hidden
This commit is contained in:
Andrew Clark 2022-07-08 11:55:53 -04:00 committed by GitHub
parent 5e4e2dae0b
commit 30eb267abd
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
11 changed files with 633 additions and 385 deletions

View File

@ -38,7 +38,6 @@ import type {UpdateQueue} from './ReactFiberClassUpdateQueue.old';
import type {RootState} from './ReactFiberRoot.old';
import type {TracingMarkerInstance} from './ReactFiberTracingMarkerComponent.old';
import {
enableSuspenseAvoidThisFallback,
enableCPUSuspense,
enableUseMutableSource,
} from 'shared/ReactFeatureFlags';
@ -168,14 +167,21 @@ import {shouldError, shouldSuspend} from './ReactFiberReconciler';
import {pushHostContext, pushHostContainer} from './ReactFiberHostContext.old';
import {
suspenseStackCursor,
pushSuspenseContext,
InvisibleParentSuspenseContext,
pushSuspenseListContext,
ForceSuspenseFallback,
hasSuspenseContext,
setDefaultShallowSuspenseContext,
addSubtreeSuspenseContext,
setShallowSuspenseContext,
hasSuspenseListContext,
setDefaultShallowSuspenseListContext,
setShallowSuspenseListContext,
pushPrimaryTreeSuspenseHandler,
pushFallbackTreeSuspenseHandler,
pushOffscreenSuspenseHandler,
reuseSuspenseHandlerOnStack,
popSuspenseHandler,
} from './ReactFiberSuspenseContext.old';
import {
pushHiddenContext,
reuseHiddenContextOnStack,
} from './ReactFiberHiddenContext.old';
import {findFirstSuspended} from './ReactFiberSuspenseComponent.old';
import {
pushProvider,
@ -233,7 +239,6 @@ import {
renderDidSuspendDelayIfPossible,
markSkippedUpdateLanes,
getWorkInProgressRoot,
pushRenderLanes,
} from './ReactFiberWorkLoop.old';
import {enqueueConcurrentRenderForLane} from './ReactFiberConcurrentUpdates.old';
import {setWorkInProgressVersion} from './ReactMutableSource.old';
@ -675,6 +680,52 @@ function updateOffscreenComponent(
(enableLegacyHidden && nextProps.mode === 'unstable-defer-without-hiding')
) {
// Rendering a hidden tree.
const didSuspend = (workInProgress.flags & DidCapture) !== NoFlags;
if (didSuspend) {
// Something suspended inside a hidden tree
// Include the base lanes from the last render
const nextBaseLanes =
prevState !== null
? mergeLanes(prevState.baseLanes, renderLanes)
: renderLanes;
if (current !== null) {
// Reset to the current children
let currentChild = (workInProgress.child = current.child);
// The current render suspended, but there may be other lanes with
// pending work. We can't read `childLanes` from the current Offscreen
// fiber because we reset it when it was deferred; however, we can read
// the pending lanes from the child fibers.
let currentChildLanes = NoLanes;
while (currentChild !== null) {
currentChildLanes = mergeLanes(
mergeLanes(currentChildLanes, currentChild.lanes),
currentChild.childLanes,
);
currentChild = currentChild.sibling;
}
const lanesWeJustAttempted = nextBaseLanes;
const remainingChildLanes = removeLanes(
currentChildLanes,
lanesWeJustAttempted,
);
workInProgress.childLanes = remainingChildLanes;
} else {
workInProgress.childLanes = NoLanes;
workInProgress.child = null;
}
return deferHiddenOffscreenComponent(
current,
workInProgress,
nextBaseLanes,
renderLanes,
);
}
if ((workInProgress.mode & ConcurrentMode) === NoMode) {
// In legacy sync mode, don't defer the subtree. Render it now.
// TODO: Consider how Offscreen should work with transitions in the future
@ -690,57 +741,29 @@ function updateOffscreenComponent(
pushTransition(workInProgress, null, null);
}
}
pushRenderLanes(workInProgress, renderLanes);
reuseHiddenContextOnStack(workInProgress);
pushOffscreenSuspenseHandler(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;
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;
}
// Schedule this fiber to re-render at offscreen priority. Then bailout.
// Schedule this fiber to re-render at Offscreen priority
workInProgress.lanes = workInProgress.childLanes = laneToLanes(
OffscreenLane,
);
const nextState: OffscreenState = {
baseLanes: nextBaseLanes,
cachePool: spawnedCachePool,
};
workInProgress.memoizedState = nextState;
workInProgress.updateQueue = null;
if (enableCache) {
// push the cache pool even though we're going to bail out
// because otherwise there'd be a context mismatch
if (current !== null) {
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.
pushRenderLanes(workInProgress, nextBaseLanes);
// Include the base lanes from the last render
const nextBaseLanes =
prevState !== null
? mergeLanes(prevState.baseLanes, renderLanes)
: renderLanes;
if (enableLazyContextPropagation && current !== null) {
// Since this tree will resume rendering in a separate render, we need
// to propagate parent contexts now so we don't lose track of which
// ones changed.
propagateParentContextChangesToDeferredTree(
current,
workInProgress,
renderLanes,
);
}
return null;
return deferHiddenOffscreenComponent(
current,
workInProgress,
nextBaseLanes,
renderLanes,
);
} else {
// This is the second render. The surrounding visible content has already
// committed. Now we resume rendering the hidden tree.
@ -751,9 +774,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
@ -764,16 +784,18 @@ 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);
}
pushOffscreenSuspenseHandler(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
@ -794,13 +816,16 @@ function updateOffscreenComponent(
pushTransition(workInProgress, prevCachePool, transitions);
// Push the lanes that were skipped when we bailed out.
pushHiddenContext(workInProgress, prevState);
reuseSuspenseHandlerOnStack(workInProgress);
// 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
@ -810,14 +835,58 @@ 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);
reuseSuspenseHandlerOnStack(workInProgress);
}
pushRenderLanes(workInProgress, subtreeRenderLanes);
}
reconcileChildren(current, workInProgress, nextChildren, renderLanes);
return workInProgress.child;
}
function deferHiddenOffscreenComponent(
current: Fiber | null,
workInProgress: Fiber,
nextBaseLanes: Lanes,
renderLanes: Lanes,
) {
const nextState: OffscreenState = {
baseLanes: nextBaseLanes,
// Save the cache pool so we can resume later.
cachePool: enableCache ? getOffscreenDeferredCache() : null,
};
workInProgress.memoizedState = nextState;
if (enableCache) {
// push the cache pool even though we're going to bail out
// because otherwise there'd be a context mismatch
if (current !== null) {
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);
pushOffscreenSuspenseHandler(workInProgress);
if (enableLazyContextPropagation && current !== null) {
// Since this tree will resume rendering in a separate render, we need
// to propagate parent contexts now so we don't lose track of which
// ones changed.
propagateParentContextChangesToDeferredTree(
current,
workInProgress,
renderLanes,
);
}
return null;
}
// Note: These happen to have identical begin phases, for now. We shouldn't hold
// ourselves to this constraint, though. If the behavior diverges, we should
// fork the function.
@ -1981,7 +2050,6 @@ function updateSuspenseOffscreenState(
// TODO: Probably should inline this back
function shouldRemainOnFallback(
suspenseContext: SuspenseContext,
current: null | Fiber,
workInProgress: Fiber,
renderLanes: Lanes,
@ -2001,7 +2069,8 @@ function shouldRemainOnFallback(
}
// Not currently showing content. Consult the Suspense context.
return hasSuspenseContext(
const suspenseContext: SuspenseContext = suspenseStackCursor.current;
return hasSuspenseListContext(
suspenseContext,
(ForceSuspenseFallback: SuspenseContext),
);
@ -2022,50 +2091,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.
@ -2093,24 +2130,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,
@ -2125,14 +2178,19 @@ function updateSuspenseComponent(current, workInProgress, renderLanes) {
if (enableTransitionTracing) {
const currentTransitions = getPendingTransitions();
if (currentTransitions !== null) {
// If there are no transitions, we don't need to keep track of tracing markers
const parentMarkerInstances = getMarkerInstances();
const primaryChildUpdateQueue: OffscreenQueue = {
transitions: currentTransitions,
markerInstances: parentMarkerInstances,
wakeables: null,
};
primaryChildFragment.updateQueue = primaryChildUpdateQueue;
const offscreenQueue: OffscreenQueue | null = (primaryChildFragment.updateQueue: any);
if (offscreenQueue === null) {
const newOffscreenQueue: OffscreenQueue = {
transitions: currentTransitions,
markerInstances: parentMarkerInstances,
wakeables: null,
};
primaryChildFragment.updateQueue = newOffscreenQueue;
} else {
offscreenQueue.transitions = currentTransitions;
offscreenQueue.markerInstances = parentMarkerInstances;
}
}
}
@ -2144,6 +2202,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,
@ -2156,6 +2215,8 @@ function updateSuspenseComponent(current, workInProgress, renderLanes) {
);
workInProgress.memoizedState = SUSPENDED_MARKER;
// TODO: Transition Tracing is not yet implemented for CPU Suspense.
// Since nothing actually suspended, there will nothing to ping this to
// get it started back up to attempt the next item. While in terms of
// priority this work has the same priority as this current render, it's
@ -2167,6 +2228,7 @@ function updateSuspenseComponent(current, workInProgress, renderLanes) {
workInProgress.lanes = SomeRetryLane;
return fallbackFragment;
} else {
pushPrimaryTreeSuspenseHandler(workInProgress);
return mountSuspensePrimaryChildren(
workInProgress,
nextPrimaryChildren,
@ -2194,6 +2256,8 @@ function updateSuspenseComponent(current, workInProgress, renderLanes) {
}
if (showFallback) {
pushFallbackTreeSuspenseHandler(workInProgress);
const nextFallbackChildren = nextProps.fallback;
const nextPrimaryChildren = nextProps.children;
const fallbackChildFragment = updateSuspenseFallbackChildren(
@ -2214,12 +2278,31 @@ function updateSuspenseComponent(current, workInProgress, renderLanes) {
const currentTransitions = getPendingTransitions();
if (currentTransitions !== null) {
const parentMarkerInstances = getMarkerInstances();
const primaryChildUpdateQueue: OffscreenQueue = {
transitions: currentTransitions,
markerInstances: parentMarkerInstances,
wakeables: null,
};
primaryChildFragment.updateQueue = primaryChildUpdateQueue;
const offscreenQueue: OffscreenQueue | null = (primaryChildFragment.updateQueue: any);
const currentOffscreenQueue: OffscreenQueue | null = (current.updateQueue: any);
if (offscreenQueue === null) {
const newOffscreenQueue: OffscreenQueue = {
transitions: currentTransitions,
markerInstances: parentMarkerInstances,
wakeables: null,
};
primaryChildFragment.updateQueue = newOffscreenQueue;
} else if (offscreenQueue === currentOffscreenQueue) {
// If the work-in-progress queue is the same object as current, we
// can't modify it without cloning it first.
const newOffscreenQueue: OffscreenQueue = {
transitions: currentTransitions,
markerInstances: parentMarkerInstances,
wakeables:
currentOffscreenQueue !== null
? currentOffscreenQueue.wakeables
: null,
};
primaryChildFragment.updateQueue = newOffscreenQueue;
} else {
offscreenQueue.transitions = currentTransitions;
offscreenQueue.markerInstances = parentMarkerInstances;
}
}
}
primaryChildFragment.childLanes = getRemainingWorkInPrimaryTree(
@ -2229,6 +2312,8 @@ function updateSuspenseComponent(current, workInProgress, renderLanes) {
workInProgress.memoizedState = SUSPENDED_MARKER;
return fallbackChildFragment;
} else {
pushPrimaryTreeSuspenseHandler(workInProgress);
const nextPrimaryChildren = nextProps.children;
const primaryChildFragment = updateSuspensePrimaryChildren(
current,
@ -2599,6 +2684,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.
@ -2765,6 +2851,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(
@ -2781,6 +2869,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.
@ -2789,6 +2881,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(
@ -3084,12 +3178,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,
);
@ -3107,9 +3201,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
@ -3577,10 +3671,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.
@ -3603,10 +3696,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(
@ -3626,10 +3716,7 @@ function attemptEarlyBailoutIfNoScheduledUpdate(
}
}
} else {
pushSuspenseContext(
workInProgress,
setDefaultShallowSuspenseContext(suspenseStackCursor.current),
);
pushPrimaryTreeSuspenseHandler(workInProgress);
}
break;
}
@ -3687,7 +3774,7 @@ function attemptEarlyBailoutIfNoScheduledUpdate(
renderState.tail = null;
renderState.lastEffect = null;
}
pushSuspenseContext(workInProgress, suspenseStackCursor.current);
pushSuspenseListContext(workInProgress, suspenseStackCursor.current);
if (hasChildWork) {
break;

View File

@ -1962,40 +1962,65 @@ function commitSuspenseHydrationCallbacks(
}
}
function attachSuspenseRetryListeners(finishedWork: Fiber) {
function getRetryCache(finishedWork) {
// TODO: Unify the interface for the retry cache so we don't have to switch
// on the tag like this.
switch (finishedWork.tag) {
case SuspenseComponent:
case SuspenseListComponent: {
let retryCache = finishedWork.stateNode;
if (retryCache === null) {
retryCache = finishedWork.stateNode = new PossiblyWeakSet();
}
return retryCache;
}
case OffscreenComponent: {
const instance: OffscreenInstance = finishedWork.stateNode;
let retryCache = instance.retryCache;
if (retryCache === null) {
retryCache = instance.retryCache = new PossiblyWeakSet();
}
return retryCache;
}
default: {
throw new Error(
`Unexpected Suspense handler tag (${finishedWork.tag}). This is a ` +
'bug in React.',
);
}
}
}
function attachSuspenseRetryListeners(
finishedWork: Fiber,
wakeables: Set<Wakeable>,
) {
// If this boundary just timed out, then it will have a set of wakeables.
// For each wakeable, attach a listener so that when it resolves, React
// attempts to re-render the boundary in the primary (pre-timeout) state.
const wakeables: Set<Wakeable> | null = (finishedWork.updateQueue: any);
if (wakeables !== null) {
finishedWork.updateQueue = null;
let retryCache = finishedWork.stateNode;
if (retryCache === null) {
retryCache = finishedWork.stateNode = new PossiblyWeakSet();
}
wakeables.forEach(wakeable => {
// Memoize using the boundary fiber to prevent redundant listeners.
const retry = resolveRetryWakeable.bind(null, finishedWork, wakeable);
if (!retryCache.has(wakeable)) {
retryCache.add(wakeable);
const retryCache = getRetryCache(finishedWork);
wakeables.forEach(wakeable => {
// Memoize using the boundary fiber to prevent redundant listeners.
const retry = resolveRetryWakeable.bind(null, finishedWork, wakeable);
if (!retryCache.has(wakeable)) {
retryCache.add(wakeable);
if (enableUpdaterTracking) {
if (isDevToolsPresent) {
if (inProgressLanes !== null && inProgressRoot !== null) {
// If we have pending work still, associate the original updaters with it.
restorePendingUpdaters(inProgressRoot, inProgressLanes);
} else {
throw Error(
'Expected finished root and lanes to be set. This is a bug in React.',
);
}
if (enableUpdaterTracking) {
if (isDevToolsPresent) {
if (inProgressLanes !== null && inProgressRoot !== null) {
// If we have pending work still, associate the original updaters with it.
restorePendingUpdaters(inProgressRoot, inProgressLanes);
} else {
throw Error(
'Expected finished root and lanes to be set. This is a bug in React.',
);
}
}
wakeable.then(retry, retry);
}
});
}
wakeable.then(retry, retry);
}
});
}
// This function detects when a Suspense boundary goes from visible to hidden.
@ -2325,7 +2350,11 @@ function commitMutationEffectsOnFiber(
} catch (error) {
captureCommitPhaseError(finishedWork, finishedWork.return, error);
}
attachSuspenseRetryListeners(finishedWork);
const wakeables: Set<Wakeable> | null = (finishedWork.updateQueue: any);
if (wakeables !== null) {
finishedWork.updateQueue = null;
attachSuspenseRetryListeners(finishedWork, wakeables);
}
}
return;
}
@ -2383,6 +2412,18 @@ function commitMutationEffectsOnFiber(
hideOrUnhideAllChildren(offscreenBoundary, isHidden);
}
}
// TODO: Move to passive phase
if (flags & Update) {
const offscreenQueue: OffscreenQueue | null = (finishedWork.updateQueue: any);
if (offscreenQueue !== null) {
const wakeables = offscreenQueue.wakeables;
if (wakeables !== null) {
offscreenQueue.wakeables = null;
attachSuspenseRetryListeners(finishedWork, wakeables);
}
}
}
return;
}
case SuspenseListComponent: {
@ -2390,7 +2431,11 @@ function commitMutationEffectsOnFiber(
commitReconciliationEffects(finishedWork);
if (flags & Update) {
attachSuspenseRetryListeners(finishedWork);
const wakeables: Set<Wakeable> | null = (finishedWork.updateQueue: any);
if (wakeables !== null) {
finishedWork.updateQueue = null;
attachSuspenseRetryListeners(finishedWork, wakeables);
}
}
return;
}

View File

@ -27,7 +27,6 @@ import type {
SuspenseState,
SuspenseListRenderState,
} from './ReactFiberSuspenseComponent.old';
import type {SuspenseContext} from './ReactFiberSuspenseContext.old';
import type {OffscreenState} from './ReactFiberOffscreenComponent';
import type {TracingMarkerInstance} from './ReactFiberTracingMarkerComponent.old';
import type {Cache} from './ReactFiberCacheComponent.old';
@ -110,14 +109,17 @@ import {
} from './ReactFiberHostContext.old';
import {
suspenseStackCursor,
InvisibleParentSuspenseContext,
hasSuspenseContext,
popSuspenseContext,
pushSuspenseContext,
setShallowSuspenseContext,
popSuspenseListContext,
popSuspenseHandler,
pushSuspenseListContext,
setShallowSuspenseListContext,
ForceSuspenseFallback,
setDefaultShallowSuspenseContext,
setDefaultShallowSuspenseListContext,
} from './ReactFiberSuspenseContext.old';
import {
popHiddenContext,
isCurrentTreeHidden,
} from './ReactFiberHiddenContext.old';
import {findFirstSuspended} from './ReactFiberSuspenseComponent.old';
import {
isContextProvider as isLegacyContextProvider,
@ -147,9 +149,7 @@ import {
renderDidSuspend,
renderDidSuspendDelayIfPossible,
renderHasNotSuspendedYet,
popRenderLanes,
getRenderTargetTime,
subtreeRenderLanes,
getWorkInProgressTransitions,
} from './ReactFiberWorkLoop.old';
import {
@ -1086,7 +1086,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
@ -1195,25 +1195,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();
}
}
}
@ -1275,7 +1273,7 @@ function completeWork(
return null;
}
case SuspenseListComponent: {
popSuspenseContext(workInProgress);
popSuspenseListContext(workInProgress);
const renderState: null | SuspenseListRenderState =
workInProgress.memoizedState;
@ -1341,11 +1339,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,
),
@ -1468,14 +1466,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;
@ -1508,7 +1508,8 @@ function completeWork(
}
case OffscreenComponent:
case LegacyHiddenComponent: {
popRenderLanes(workInProgress);
popSuspenseHandler(workInProgress);
popHiddenContext(workInProgress);
const nextState: OffscreenState | null = workInProgress.memoizedState;
const nextIsHidden = nextState !== null;
@ -1529,7 +1530,11 @@ 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)) &&
// Also don't bubble if the tree suspended
(workInProgress.flags & DidCapture) === NoLanes
) {
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
@ -1544,6 +1549,12 @@ function completeWork(
}
}
if (workInProgress.updateQueue !== null) {
// Schedule an effect to attach Suspense retry listeners
// TODO: Move to passive phase
workInProgress.flags |= Update;
}
if (enableCache) {
let previousCache: Cache | null = null;
if (

View File

@ -1 +1,70 @@
// Intentionally blank. File only exists in new reconciler fork.
/**
* 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.old';
import type {Lanes} from './ReactFiberLane.old';
import {createCursor, push, pop} from './ReactFiberStack.old';
import {getRenderLanes, setRenderLanes} from './ReactFiberWorkLoop.old';
import {NoLanes, mergeLanes} from './ReactFiberLane.old';
// 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<HiddenContext | null> = createCursor(
null,
);
export const prevRenderLanesStackCursor: StackCursor<Lanes> = 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;
}

View File

@ -13,7 +13,6 @@ import type {SuspenseInstance} from './ReactFiberHostConfig';
import type {Lane} from './ReactFiberLane.old';
import type {TreeContext} from './ReactFiberTreeContext.old';
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) {

View File

@ -9,33 +9,109 @@
import type {Fiber} from './ReactInternalTypes';
import type {StackCursor} from './ReactFiberStack.old';
import type {SuspenseState} from './ReactFiberSuspenseComponent.old';
import {enableSuspenseAvoidThisFallback} from 'shared/ReactFeatureFlags';
import {createCursor, push, pop} from './ReactFiberStack.old';
import {isCurrentTreeHidden} from './ReactFiberHiddenContext.old';
import {SuspenseComponent, OffscreenComponent} 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<Fiber | null> = 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.
reuseSuspenseHandlerOnStack(fiber);
}
export function pushOffscreenSuspenseHandler(fiber: Fiber): void {
if (fiber.tag === OffscreenComponent) {
push(suspenseHandlerStackCursor, fiber, fiber);
} else {
// This is a LegacyHidden component.
reuseSuspenseHandlerOnStack(fiber);
}
}
export function reuseSuspenseHandlerOnStack(fiber: Fiber) {
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 +120,33 @@ export const suspenseStackCursor: StackCursor<SuspenseContext> = 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);
}

View File

@ -13,17 +13,18 @@ import type {Lane, Lanes} from './ReactFiberLane.old';
import type {CapturedValue} from './ReactCapturedValue';
import type {Update} from './ReactFiberClassUpdateQueue.old';
import type {Wakeable} from 'shared/ReactTypes';
import type {SuspenseContext} from './ReactFiberSuspenseContext.old';
import type {OffscreenQueue} from './ReactFiberOffscreenComponent';
import getComponentNameFromFiber from 'react-reconciler/src/getComponentNameFromFiber';
import {
ClassComponent,
HostRoot,
SuspenseComponent,
IncompleteClassComponent,
FunctionComponent,
ForwardRef,
SimpleMemoComponent,
SuspenseComponent,
OffscreenComponent,
} from './ReactWorkTags';
import {
DidCapture,
@ -34,7 +35,6 @@ import {
ForceUpdateForLegacySuspense,
ForceClientRender,
} from './ReactFiberFlags';
import {shouldCaptureSuspense} from './ReactFiberSuspenseComponent.old';
import {NoMode, ConcurrentMode, DebugTracingMode} from './ReactTypeOfMode';
import {
enableDebugTracing,
@ -50,11 +50,7 @@ import {
enqueueUpdate,
} from './ReactFiberClassUpdateQueue.old';
import {markFailedErrorBoundaryForHotReloading} from './ReactFiberHotReloading.old';
import {
suspenseStackCursor,
InvisibleParentSuspenseContext,
hasSuspenseContext,
} from './ReactFiberSuspenseContext.old';
import {getSuspenseHandler} from './ReactFiberSuspenseContext.old';
import {
renderDidError,
renderDidSuspendDelayIfPossible,
@ -203,33 +199,6 @@ function attachPingListener(root: FiberRoot, wakeable: Wakeable, lanes: Lanes) {
}
}
function attachRetryListener(
suspenseBoundary: Fiber,
root: FiberRoot,
wakeable: Wakeable,
lanes: Lanes,
) {
// Retry listener
//
// If the fallback does commit, we need to attach a different type of
// listener. This one schedules an update on the Suspense boundary to turn
// the fallback state off.
//
// Stash the wakeable on the boundary fiber so we can access it in the
// commit phase.
//
// When the wakeable resolves, we'll attempt to render the boundary
// again ("retry").
const wakeables: Set<Wakeable> | null = (suspenseBoundary.updateQueue: any);
if (wakeables === null) {
const updateQueue = (new Set(): any);
updateQueue.add(wakeable);
suspenseBoundary.updateQueue = updateQueue;
} else {
wakeables.add(wakeable);
}
}
function resetSuspendedComponent(sourceFiber: Fiber, rootRenderLanes: Lanes) {
if (enableLazyContextPropagation) {
const currentSourceFiber = sourceFiber.alternate;
@ -269,26 +238,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,22 +393,72 @@ 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(
suspenseBoundary,
returnFiber,
sourceFiber,
root,
rootRenderLanes,
);
switch (suspenseBoundary.tag) {
case SuspenseComponent: {
suspenseBoundary.flags &= ~ForceClientRender;
markSuspenseBoundaryShouldCapture(
suspenseBoundary,
returnFiber,
sourceFiber,
root,
rootRenderLanes,
);
// Retry listener
//
// If the fallback does commit, we need to attach a different type of
// listener. This one schedules an update on the Suspense boundary to
// turn the fallback state off.
//
// Stash the wakeable on the boundary fiber so we can access it in the
// commit phase.
//
// When the wakeable resolves, we'll attempt to render the boundary
// again ("retry").
const wakeables: Set<Wakeable> | null = (suspenseBoundary.updateQueue: any);
if (wakeables === null) {
suspenseBoundary.updateQueue = new Set([wakeable]);
} else {
wakeables.add(wakeable);
}
break;
}
case OffscreenComponent: {
if (suspenseBoundary.mode & ConcurrentMode) {
suspenseBoundary.flags |= ShouldCapture;
const offscreenQueue: OffscreenQueue | null = (suspenseBoundary.updateQueue: any);
if (offscreenQueue === null) {
const newOffscreenQueue: OffscreenQueue = {
transitions: null,
markerInstances: null,
wakeables: new Set([wakeable]),
};
suspenseBoundary.updateQueue = newOffscreenQueue;
} else {
const wakeables = offscreenQueue.wakeables;
if (wakeables === null) {
offscreenQueue.wakeables = new Set([wakeable]);
} else {
wakeables.add(wakeable);
}
}
break;
}
}
// eslint-disable-next-line no-fallthrough
default: {
throw new Error(
`Unexpected Suspense handler tag (${suspenseBoundary.tag}). This ` +
'is a bug in React.',
);
}
}
// We only attach ping listeners in concurrent mode. Legacy Suspense always
// commits fallbacks synchronously, so there are no pings.
if (suspenseBoundary.mode & ConcurrentMode) {
attachPingListener(root, wakeable, rootRenderLanes);
}
attachRetryListener(suspenseBoundary, root, wakeable, rootRenderLanes);
return;
} else {
// No boundary was found. Unless this is a sync update, this is OK.
@ -496,7 +495,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

View File

@ -37,7 +37,11 @@ import {
} from 'shared/ReactFeatureFlags';
import {popHostContainer, popHostContext} from './ReactFiberHostContext.old';
import {popSuspenseContext} from './ReactFiberSuspenseContext.old';
import {
popSuspenseListContext,
popSuspenseHandler,
} from './ReactFiberSuspenseContext.old';
import {popHiddenContext} from './ReactFiberHiddenContext.old';
import {resetHydrationState} from './ReactFiberHydrationContext.old';
import {
isContextProvider as isLegacyContextProvider,
@ -45,7 +49,6 @@ import {
popTopLevelContextObject as popTopLevelLegacyContextObject,
} from './ReactFiberContext.old';
import {popProvider} from './ReactFiberNewContext.old';
import {popRenderLanes} from './ReactFiberWorkLoop.old';
import {popCacheProvider} from './ReactFiberCacheComponent.old';
import {transferActualDuration} from './ReactProfilerTimer.old';
import {popTreeContext} from './ReactFiberTreeContext.old';
@ -118,7 +121,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) {
@ -146,7 +149,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;
@ -159,10 +162,24 @@ function unwindWork(
popProvider(context, workInProgress);
return null;
case OffscreenComponent:
case LegacyHiddenComponent:
popRenderLanes(workInProgress);
case LegacyHiddenComponent: {
popSuspenseHandler(workInProgress);
popHiddenContext(workInProgress);
popTransition(workInProgress, current);
const flags = workInProgress.flags;
if (flags & ShouldCapture) {
workInProgress.flags = (flags & ~ShouldCapture) | DidCapture;
// Captured a suspense effect. Re-render the boundary.
if (
enableProfilerTimer &&
(workInProgress.mode & ProfileMode) !== NoMode
) {
transferActualDuration(workInProgress);
}
return workInProgress;
}
return null;
}
case CacheComponent:
if (enableCache) {
const cache: Cache = workInProgress.memoizedState.cache;
@ -224,10 +241,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<any> = interruptedWork.type._context;
@ -235,7 +252,8 @@ function unwindInterruptedWork(
break;
case OffscreenComponent:
case LegacyHiddenComponent:
popRenderLanes(interruptedWork);
popSuspenseHandler(interruptedWork);
popHiddenContext(interruptedWork);
popTransition(interruptedWork, current);
break;
case CacheComponent:

View File

@ -11,7 +11,6 @@ import type {Wakeable} from 'shared/ReactTypes';
import type {Fiber, FiberRoot} from './ReactInternalTypes';
import type {Lanes, Lane} from './ReactFiberLane.old';
import type {SuspenseState} from './ReactFiberSuspenseComponent.old';
import type {StackCursor} from './ReactFiberStack.old';
import type {Flags} from './ReactFiberFlags';
import type {FunctionComponentUpdateQueue} from './ReactFiberHooks.old';
import type {EventPriority} from './ReactEventPriorities.old';
@ -20,6 +19,7 @@ import type {
MarkerTransition,
Transition,
} from './ReactFiberTracingMarkerComponent.old';
import type {OffscreenInstance} from './ReactFiberOffscreenComponent';
import {
warnAboutDeprecatedLifecycles,
@ -96,6 +96,7 @@ import {
ClassComponent,
SuspenseComponent,
SuspenseListComponent,
OffscreenComponent,
FunctionComponent,
ForwardRef,
MemoComponent,
@ -191,11 +192,6 @@ import {
createCapturedValueAtFiber,
type CapturedValue,
} from './ReactCapturedValue';
import {
push as pushToStack,
pop as popFromStack,
createCursor,
} from './ReactFiberStack.old';
import {
enqueueConcurrentRenderForLane,
finishQueueingConcurrentUpdates,
@ -284,26 +280,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<Lanes> = 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;
@ -1454,18 +1444,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 {
@ -1496,7 +1484,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;
@ -1863,10 +1851,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();
@ -1900,10 +1888,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);
}
@ -1918,7 +1906,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.
@ -2761,6 +2749,11 @@ export function resolveRetryWakeable(boundaryFiber: Fiber, wakeable: Wakeable) {
case SuspenseListComponent:
retryCache = boundaryFiber.stateNode;
break;
case OffscreenComponent: {
const instance: OffscreenInstance = boundaryFiber.stateNode;
retryCache = instance.retryCache;
break;
}
default:
throw new Error(
'Pinged unknown suspense boundary type. ' +

View File

@ -90,8 +90,6 @@ describe('ReactOffscreen', () => {
return text;
}
// Only works in new reconciler
// @gate variant
// @gate enableOffscreen
test('basic example of suspending inside hidden tree', async () => {
const root = ReactNoop.createRoot();
@ -172,8 +170,6 @@ describe('ReactOffscreen', () => {
);
});
// Only works in new reconciler
// @gate variant
// @gate experimental || www
test("suspending inside currently hidden tree that's switching to visible", async () => {
const root = ReactNoop.createRoot();
@ -233,8 +229,6 @@ describe('ReactOffscreen', () => {
);
});
// Only works in new reconciler
// @gate variant
// @gate enableOffscreen
test("suspending inside currently visible tree that's switching to hidden", async () => {
const root = ReactNoop.createRoot();
@ -354,8 +348,6 @@ describe('ReactOffscreen', () => {
});
});
// Only works in new reconciler
// @gate variant
// @gate experimental || www
test('updates at multiple priorities that suspend inside hidden tree', async () => {
let setText;

View File

@ -1,3 +0,0 @@
67de5e3fb09eecfab91321246246095058a708a9 [FORKED] Hidden trees should capture Suspense
6ab05ee2e9c5b1f4c8dc1f7ae8906bf613788ba7 [FORKED] Track nearest Suspense handler on stack
051ac55cb75f426b81f8f75b143f34255476b9bc [FORKED] Add HiddenContext to track if subtree is hidden