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:
parent
5e4e2dae0b
commit
30eb267abd
|
@ -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;
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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 (
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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:
|
||||
|
|
|
@ -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. ' +
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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
|
Loading…
Reference in New Issue