Files

4365 lines
146 KiB
JavaScript

/**
* Copyright (c) Meta Platforms, Inc. and 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 {
ReactConsumerType,
ReactContext,
ReactNodeList,
ViewTransitionProps,
ActivityProps,
SuspenseProps,
SuspenseListProps,
SuspenseListRevealOrder,
SuspenseListTailMode,
TracingMarkerProps,
CacheProps,
ProfilerProps,
} from 'shared/ReactTypes';
import type {LazyComponent as LazyComponentType} from 'react/src/ReactLazy';
import type {Fiber, FiberRoot} from './ReactInternalTypes';
import type {TypeOfMode} from './ReactTypeOfMode';
import type {Lanes, Lane} from './ReactFiberLane';
import type {ActivityState} from './ReactFiberActivityComponent';
import type {
SuspenseState,
SuspenseListRenderState,
} from './ReactFiberSuspenseComponent';
import type {SuspenseContext} from './ReactFiberSuspenseContext';
import type {
LegacyHiddenProps,
OffscreenProps,
OffscreenState,
OffscreenQueue,
OffscreenInstance,
} from './ReactFiberOffscreenComponent';
import type {
Cache,
CacheComponentState,
SpawnedCachePool,
} from './ReactFiberCacheComponent';
import type {UpdateQueue} from './ReactFiberClassUpdateQueue';
import type {RootState} from './ReactFiberRoot';
import type {TracingMarkerInstance} from './ReactFiberTracingMarkerComponent';
import {
markComponentRenderStarted,
markComponentRenderStopped,
setIsStrictModeForDevtools,
} from './ReactFiberDevToolsHook';
import {
FunctionComponent,
ClassComponent,
HostRoot,
HostComponent,
HostHoistable,
HostSingleton,
HostText,
HostPortal,
ForwardRef,
Fragment,
Mode,
ContextProvider,
ContextConsumer,
Profiler,
SuspenseComponent,
SuspenseListComponent,
MemoComponent,
SimpleMemoComponent,
LazyComponent,
IncompleteClassComponent,
IncompleteFunctionComponent,
ScopeComponent,
OffscreenComponent,
LegacyHiddenComponent,
CacheComponent,
TracingMarkerComponent,
Throw,
ViewTransitionComponent,
ActivityComponent,
} from './ReactWorkTags';
import {
NoFlags,
PerformedWork,
Placement,
Hydrating,
Callback,
ContentReset,
DidCapture,
Update,
Ref,
RefStatic,
ChildDeletion,
ForceUpdateForLegacySuspense,
StaticMask,
ShouldCapture,
ForceClientRender,
Passive,
DidDefer,
ViewTransitionNamedStatic,
ViewTransitionNamedMount,
LayoutStatic,
} from './ReactFiberFlags';
import {
disableLegacyContext,
disableLegacyContextForFunctionComponents,
enableProfilerCommitHooks,
enableProfilerTimer,
enableScopeAPI,
enableSchedulingProfiler,
enableTransitionTracing,
enableLegacyHidden,
enableCPUSuspense,
enablePostpone,
disableLegacyMode,
enableHydrationLaneScheduling,
enableViewTransition,
enableFragmentRefs,
} from 'shared/ReactFeatureFlags';
import shallowEqual from 'shared/shallowEqual';
import getComponentNameFromFiber from 'react-reconciler/src/getComponentNameFromFiber';
import getComponentNameFromType from 'shared/getComponentNameFromType';
import ReactStrictModeWarnings from './ReactStrictModeWarnings';
import {
REACT_LAZY_TYPE,
REACT_FORWARD_REF_TYPE,
REACT_MEMO_TYPE,
} from 'shared/ReactSymbols';
import {setCurrentFiber} from './ReactCurrentFiber';
import {
resolveFunctionForHotReloading,
resolveForwardRefForHotReloading,
resolveClassForHotReloading,
} from './ReactFiberHotReloading';
import {
mountChildFibers,
reconcileChildFibers,
cloneChildFibers,
validateSuspenseListChildren,
} from './ReactChildFiber';
import {
processUpdateQueue,
cloneUpdateQueue,
initializeUpdateQueue,
enqueueCapturedUpdate,
suspendIfUpdateReadFromEntangledAsyncAction,
} from './ReactFiberClassUpdateQueue';
import {
NoLane,
NoLanes,
OffscreenLane,
DefaultLane,
DefaultHydrationLane,
SomeRetryLane,
includesSomeLane,
includesOnlyRetries,
laneToLanes,
removeLanes,
mergeLanes,
getBumpedLaneForHydration,
pickArbitraryLane,
} from './ReactFiberLane';
import {
ConcurrentMode,
NoMode,
ProfileMode,
StrictLegacyMode,
} from './ReactTypeOfMode';
import {
shouldSetTextContent,
isSuspenseInstancePending,
isSuspenseInstanceFallback,
getSuspenseInstanceFallbackErrorDetails,
supportsHydration,
supportsResources,
supportsSingletons,
isPrimaryRenderer,
getResource,
createHoistableInstance,
HostTransitionContext,
} from './ReactFiberConfig';
import type {ActivityInstance, SuspenseInstance} from './ReactFiberConfig';
import {shouldError, shouldSuspend} from './ReactFiberReconciler';
import {
pushHostContext,
pushHostContainer,
getRootHostContainer,
} from './ReactFiberHostContext';
import {
suspenseStackCursor,
pushSuspenseListContext,
ForceSuspenseFallback,
hasSuspenseListContext,
setDefaultShallowSuspenseListContext,
setShallowSuspenseListContext,
pushPrimaryTreeSuspenseHandler,
pushFallbackTreeSuspenseHandler,
pushDehydratedActivitySuspenseHandler,
pushOffscreenSuspenseHandler,
reuseSuspenseHandlerOnStack,
popSuspenseHandler,
} from './ReactFiberSuspenseContext';
import {
pushHiddenContext,
reuseHiddenContextOnStack,
} from './ReactFiberHiddenContext';
import {findFirstSuspended} from './ReactFiberSuspenseComponent';
import {
pushProvider,
propagateContextChange,
lazilyPropagateParentContextChanges,
propagateParentContextChangesToDeferredTree,
checkIfContextChanged,
readContext,
prepareToReadContext,
scheduleContextWorkOnParentPath,
} from './ReactFiberNewContext';
import {
renderWithHooks,
checkDidRenderIdHook,
bailoutHooks,
replaySuspendedComponentWithHooks,
renderTransitionAwareHostComponentWithHooks,
} from './ReactFiberHooks';
import {stopProfilerTimerIfRunning} from './ReactProfilerTimer';
import {
getMaskedContext,
getUnmaskedContext,
hasContextChanged as hasLegacyContextChanged,
pushContextProvider as pushLegacyContextProvider,
isContextProvider as isLegacyContextProvider,
pushTopLevelContextObject,
invalidateContextProvider,
} from './ReactFiberLegacyContext';
import {
getIsHydrating,
enterHydrationState,
reenterHydrationStateFromDehydratedActivityInstance,
reenterHydrationStateFromDehydratedSuspenseInstance,
resetHydrationState,
claimHydratableSingleton,
tryToClaimNextHydratableInstance,
tryToClaimNextHydratableTextInstance,
claimNextHydratableActivityInstance,
claimNextHydratableSuspenseInstance,
warnIfHydrating,
queueHydrationError,
} from './ReactFiberHydrationContext';
import {
constructClassInstance,
mountClassInstance,
resumeMountClassInstance,
updateClassInstance,
resolveClassComponentProps,
} from './ReactFiberClassComponent';
import {
createFiberFromTypeAndProps,
createFiberFromFragment,
createFiberFromOffscreen,
createWorkInProgress,
isSimpleFunctionComponent,
isFunctionClassComponent,
} from './ReactFiber';
import {
scheduleUpdateOnFiber,
renderDidSuspendDelayIfPossible,
markSkippedUpdateLanes,
markRenderDerivedCause,
getWorkInProgressRoot,
peekDeferredLane,
} from './ReactFiberWorkLoop';
import {enqueueConcurrentRenderForLane} from './ReactFiberConcurrentUpdates';
import {pushCacheProvider, CacheContext} from './ReactFiberCacheComponent';
import {
createCapturedValueFromError,
createCapturedValueAtFiber,
} from './ReactCapturedValue';
import {OffscreenVisible} from './ReactFiberOffscreenComponent';
import {
createClassErrorUpdate,
initializeClassErrorUpdate,
} from './ReactFiberThrow';
import {
getForksAtLevel,
isForkedChild,
pushTreeId,
pushMaterializedTreeId,
} from './ReactFiberTreeContext';
import {
requestCacheFromPool,
pushRootTransition,
getSuspendedCache,
pushTransition,
getOffscreenDeferredCache,
getPendingTransitions,
} from './ReactFiberTransition';
import {
getMarkerInstances,
pushMarkerInstance,
pushRootMarkerInstance,
TransitionTracingMarker,
} from './ReactFiberTracingMarkerComponent';
import {callComponentInDEV, callRenderInDEV} from './ReactFiberCallUserSpace';
import {resolveLazy} from './ReactFiberThenable';
// A special exception that's used to unwind the stack when an update flows
// into a dehydrated boundary.
export const SelectiveHydrationException: mixed = new Error(
"This is not a real error. It's an implementation detail of React's " +
"selective hydration feature. If this leaks into userspace, it's a bug in " +
'React. Please file an issue.',
);
let didReceiveUpdate: boolean = false;
let didWarnAboutBadClass;
let didWarnAboutContextTypeOnFunctionComponent;
let didWarnAboutContextTypes;
let didWarnAboutGetDerivedStateOnFunctionComponent;
export let didWarnAboutReassigningProps: boolean;
let didWarnAboutRevealOrder;
let didWarnAboutTailOptions;
let didWarnAboutClassNameOnViewTransition;
if (__DEV__) {
didWarnAboutBadClass = ({}: {[string]: boolean});
didWarnAboutContextTypeOnFunctionComponent = ({}: {[string]: boolean});
didWarnAboutContextTypes = ({}: {[string]: boolean});
didWarnAboutGetDerivedStateOnFunctionComponent = ({}: {[string]: boolean});
didWarnAboutReassigningProps = false;
didWarnAboutRevealOrder = ({}: {[string]: boolean});
didWarnAboutTailOptions = ({}: {[string]: boolean});
didWarnAboutClassNameOnViewTransition = ({}: {[string]: boolean});
}
export function reconcileChildren(
current: Fiber | null,
workInProgress: Fiber,
nextChildren: any,
renderLanes: Lanes,
) {
if (current === null) {
// If this is a fresh new component that hasn't been rendered yet, we
// won't update its child set by applying minimal side-effects. Instead,
// we will add them all to the child before it gets rendered. That means
// we can optimize this reconciliation pass by not tracking side-effects.
workInProgress.child = mountChildFibers(
workInProgress,
null,
nextChildren,
renderLanes,
);
} else {
// If the current child is the same as the work in progress, it means that
// we haven't yet started any work on these children. Therefore, we use
// the clone algorithm to create a copy of all the current children.
// If we had any progressed work already, that is invalid at this point so
// let's throw it out.
workInProgress.child = reconcileChildFibers(
workInProgress,
current.child,
nextChildren,
renderLanes,
);
}
}
function forceUnmountCurrentAndReconcile(
current: Fiber,
workInProgress: Fiber,
nextChildren: any,
renderLanes: Lanes,
) {
// This function is fork of reconcileChildren. It's used in cases where we
// want to reconcile without matching against the existing set. This has the
// effect of all current children being unmounted; even if the type and key
// are the same, the old child is unmounted and a new child is created.
//
// To do this, we're going to go through the reconcile algorithm twice. In
// the first pass, we schedule a deletion for all the current children by
// passing null.
workInProgress.child = reconcileChildFibers(
workInProgress,
current.child,
null,
renderLanes,
);
// In the second pass, we mount the new children. The trick here is that we
// pass null in place of where we usually pass the current child set. This has
// the effect of remounting all children regardless of whether their
// identities match.
workInProgress.child = reconcileChildFibers(
workInProgress,
null,
nextChildren,
renderLanes,
);
}
function updateForwardRef(
current: Fiber | null,
workInProgress: Fiber,
Component: any,
nextProps: any,
renderLanes: Lanes,
) {
// TODO: current can be non-null here even if the component
// hasn't yet mounted. This happens after the first render suspends.
// We'll need to figure out if this is fine or can cause issues.
const render = Component.render;
const ref = workInProgress.ref;
let propsWithoutRef;
if ('ref' in nextProps) {
// `ref` is just a prop now, but `forwardRef` expects it to not appear in
// the props object. This used to happen in the JSX runtime, but now we do
// it here.
propsWithoutRef = ({}: {[string]: any});
for (const key in nextProps) {
// Since `ref` should only appear in props via the JSX transform, we can
// assume that this is a plain object. So we don't need a
// hasOwnProperty check.
if (key !== 'ref') {
propsWithoutRef[key] = nextProps[key];
}
}
} else {
propsWithoutRef = nextProps;
}
// The rest is a fork of updateFunctionComponent
prepareToReadContext(workInProgress, renderLanes);
if (enableSchedulingProfiler) {
markComponentRenderStarted(workInProgress);
}
const nextChildren = renderWithHooks(
current,
workInProgress,
render,
propsWithoutRef,
ref,
renderLanes,
);
const hasId = checkDidRenderIdHook();
if (enableSchedulingProfiler) {
markComponentRenderStopped();
}
if (current !== null && !didReceiveUpdate) {
bailoutHooks(current, workInProgress, renderLanes);
return bailoutOnAlreadyFinishedWork(current, workInProgress, renderLanes);
}
if (getIsHydrating() && hasId) {
pushMaterializedTreeId(workInProgress);
}
// React DevTools reads this flag.
workInProgress.flags |= PerformedWork;
reconcileChildren(current, workInProgress, nextChildren, renderLanes);
return workInProgress.child;
}
function updateMemoComponent(
current: Fiber | null,
workInProgress: Fiber,
Component: any,
nextProps: any,
renderLanes: Lanes,
): null | Fiber {
if (current === null) {
const type = Component.type;
if (isSimpleFunctionComponent(type) && Component.compare === null) {
let resolvedType = type;
if (__DEV__) {
resolvedType = resolveFunctionForHotReloading(type);
}
// If this is a plain function component without default props,
// and with only the default shallow comparison, we upgrade it
// to a SimpleMemoComponent to allow fast path updates.
workInProgress.tag = SimpleMemoComponent;
workInProgress.type = resolvedType;
if (__DEV__) {
validateFunctionComponentInDev(workInProgress, type);
}
return updateSimpleMemoComponent(
current,
workInProgress,
resolvedType,
nextProps,
renderLanes,
);
}
const child = createFiberFromTypeAndProps(
Component.type,
null,
nextProps,
workInProgress,
workInProgress.mode,
renderLanes,
);
child.ref = workInProgress.ref;
child.return = workInProgress;
workInProgress.child = child;
return child;
}
const currentChild = ((current.child: any): Fiber); // This is always exactly one child
const hasScheduledUpdateOrContext = checkScheduledUpdateOrContext(
current,
renderLanes,
);
if (!hasScheduledUpdateOrContext) {
// This will be the props with resolved defaultProps,
// unlike current.memoizedProps which will be the unresolved ones.
const prevProps = currentChild.memoizedProps;
// Default to shallow comparison
let compare = Component.compare;
compare = compare !== null ? compare : shallowEqual;
if (compare(prevProps, nextProps) && current.ref === workInProgress.ref) {
return bailoutOnAlreadyFinishedWork(current, workInProgress, renderLanes);
}
}
// React DevTools reads this flag.
workInProgress.flags |= PerformedWork;
const newChild = createWorkInProgress(currentChild, nextProps);
newChild.ref = workInProgress.ref;
newChild.return = workInProgress;
workInProgress.child = newChild;
return newChild;
}
function updateSimpleMemoComponent(
current: Fiber | null,
workInProgress: Fiber,
Component: any,
nextProps: any,
renderLanes: Lanes,
): null | Fiber {
// TODO: current can be non-null here even if the component
// hasn't yet mounted. This happens when the inner render suspends.
// We'll need to figure out if this is fine or can cause issues.
if (current !== null) {
const prevProps = current.memoizedProps;
if (
shallowEqual(prevProps, nextProps) &&
current.ref === workInProgress.ref &&
// Prevent bailout if the implementation changed due to hot reload.
(__DEV__ ? workInProgress.type === current.type : true)
) {
didReceiveUpdate = false;
// The props are shallowly equal. Reuse the previous props object, like we
// would during a normal fiber bailout.
//
// We don't have strong guarantees that the props object is referentially
// equal during updates where we can't bail out anyway — like if the props
// are shallowly equal, but there's a local state or context update in the
// same batch.
//
// However, as a principle, we should aim to make the behavior consistent
// across different ways of memoizing a component. For example, React.memo
// has a different internal Fiber layout if you pass a normal function
// component (SimpleMemoComponent) versus if you pass a different type
// like forwardRef (MemoComponent). But this is an implementation detail.
// Wrapping a component in forwardRef (or React.lazy, etc) shouldn't
// affect whether the props object is reused during a bailout.
workInProgress.pendingProps = nextProps = prevProps;
if (!checkScheduledUpdateOrContext(current, renderLanes)) {
// The pending lanes were cleared at the beginning of beginWork. We're
// about to bail out, but there might be other lanes that weren't
// included in the current render. Usually, the priority level of the
// remaining updates is accumulated during the evaluation of the
// component (i.e. when processing the update queue). But since since
// we're bailing out early *without* evaluating the component, we need
// to account for it here, too. Reset to the value of the current fiber.
// NOTE: This only applies to SimpleMemoComponent, not MemoComponent,
// because a MemoComponent fiber does not have hooks or an update queue;
// rather, it wraps around an inner component, which may or may not
// contains hooks.
// TODO: Move the reset at in beginWork out of the common path so that
// this is no longer necessary.
workInProgress.lanes = current.lanes;
return bailoutOnAlreadyFinishedWork(
current,
workInProgress,
renderLanes,
);
} else if ((current.flags & ForceUpdateForLegacySuspense) !== NoFlags) {
// This is a special case that only exists for legacy mode.
// See https://github.com/facebook/react/pull/19216.
didReceiveUpdate = true;
}
}
}
return updateFunctionComponent(
current,
workInProgress,
Component,
nextProps,
renderLanes,
);
}
function updateOffscreenComponent(
current: Fiber | null,
workInProgress: Fiber,
renderLanes: Lanes,
nextProps: OffscreenProps,
) {
const nextChildren = nextProps.children;
const prevState: OffscreenState | null =
current !== null ? current.memoizedState : null;
if (current === null && workInProgress.stateNode === null) {
// We previously reset the work-in-progress.
// We need to create a new Offscreen instance.
const primaryChildInstance: OffscreenInstance = {
_visibility: OffscreenVisible,
_pendingMarkers: null,
_retryCache: null,
_transitions: null,
};
workInProgress.stateNode = primaryChildInstance;
}
if (
nextProps.mode === 'hidden' ||
(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: Lanes = 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 (
!disableLegacyMode &&
(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
const nextState: OffscreenState = {
baseLanes: NoLanes,
cachePool: null,
};
workInProgress.memoizedState = nextState;
// 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);
}
reuseHiddenContextOnStack(workInProgress);
pushOffscreenSuspenseHandler(workInProgress);
} else if (!includesSomeLane(renderLanes, (OffscreenLane: Lane))) {
// We're hidden, and we're not rendering at Offscreen. We will bail out
// and resume this tree later.
// Schedule this fiber to re-render at Offscreen priority
workInProgress.lanes = workInProgress.childLanes =
laneToLanes(OffscreenLane);
// Include the base lanes from the last render
const nextBaseLanes =
prevState !== null
? mergeLanes(prevState.baseLanes, renderLanes)
: renderLanes;
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.
// Rendering at offscreen, so we can clear the base lanes.
const nextState: OffscreenState = {
baseLanes: NoLanes,
cachePool: null,
};
workInProgress.memoizedState = nextState;
if (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
// there was a refresh.
const prevCachePool = prevState !== null ? prevState.cachePool : null;
// TODO: Consider if and how Offscreen pre-rendering should
// be attributed to the transition that spawned it
pushTransition(workInProgress, prevCachePool, null);
}
// 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.
if (prevState !== null) {
// We're going from hidden -> visible.
let prevCachePool = null;
// If the render that spawned this one accessed the cache pool, resume
// using the same cache. Unless the parent changed, since that means
// there was a refresh.
prevCachePool = prevState.cachePool;
let transitions = null;
if (enableTransitionTracing) {
// We have now gone from hidden to visible, so any transitions should
// be added to the stack to get added to any Offscreen/suspense children
const instance: OffscreenInstance | null = workInProgress.stateNode;
if (instance !== null && instance._transitions != null) {
transitions = Array.from(instance._transitions);
}
}
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.
// If the render that spawned this one accessed the cache pool, resume
// using the same cache. Unless the parent changed, since that means
// there was a refresh.
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);
reuseSuspenseHandlerOnStack(workInProgress);
}
}
reconcileChildren(current, workInProgress, nextChildren, renderLanes);
return workInProgress.child;
}
function bailoutOffscreenComponent(
current: Fiber | null,
workInProgress: Fiber,
): Fiber | null {
if (
(current === null || current.tag !== OffscreenComponent) &&
workInProgress.stateNode === null
) {
const primaryChildInstance: OffscreenInstance = {
_visibility: OffscreenVisible,
_pendingMarkers: null,
_retryCache: null,
_transitions: null,
};
workInProgress.stateNode = primaryChildInstance;
}
return workInProgress.sibling;
}
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: getOffscreenDeferredCache(),
};
workInProgress.memoizedState = nextState;
// 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 (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;
}
function updateLegacyHiddenComponent(
current: null | Fiber,
workInProgress: Fiber,
renderLanes: Lanes,
) {
const nextProps: LegacyHiddenProps = workInProgress.pendingProps;
// 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.
// This just works today because it has the same Props.
return updateOffscreenComponent(
current,
workInProgress,
renderLanes,
nextProps,
);
}
function mountActivityChildren(
workInProgress: Fiber,
nextProps: ActivityProps,
renderLanes: Lanes,
) {
if (__DEV__) {
const hiddenProp = (nextProps: any).hidden;
if (hiddenProp !== undefined) {
console.error(
'<Activity> doesn\'t accept a hidden prop. Use mode="hidden" instead.\n' +
'- <Activity %s>\n' +
'+ <Activity %s>',
hiddenProp === true
? 'hidden'
: hiddenProp === false
? 'hidden={false}'
: 'hidden={...}',
hiddenProp ? 'mode="hidden"' : 'mode="visible"',
);
}
}
const nextChildren = nextProps.children;
const nextMode = nextProps.mode;
const mode = workInProgress.mode;
const offscreenChildProps: OffscreenProps = {
mode: nextMode,
children: nextChildren,
};
const primaryChildFragment = mountWorkInProgressOffscreenFiber(
offscreenChildProps,
mode,
renderLanes,
);
primaryChildFragment.ref = workInProgress.ref;
workInProgress.child = primaryChildFragment;
primaryChildFragment.return = workInProgress;
return primaryChildFragment;
}
function retryActivityComponentWithoutHydrating(
current: Fiber,
workInProgress: Fiber,
renderLanes: Lanes,
) {
// Falling back to client rendering. Because this has performance
// implications, it's considered a recoverable error, even though the user
// likely won't observe anything wrong with the UI.
// This will add the old fiber to the deletion list
reconcileChildFibers(workInProgress, current.child, null, renderLanes);
// We're now not suspended nor dehydrated.
const nextProps: ActivityProps = workInProgress.pendingProps;
const primaryChildFragment = mountActivityChildren(
workInProgress,
nextProps,
renderLanes,
);
// Needs a placement effect because the parent (the Activity boundary) already
// mounted but this is a new fiber.
primaryChildFragment.flags |= Placement;
// If we're not going to hydrate we can't leave it dehydrated if something
// suspends. In that case we want that to bubble to the nearest parent boundary
// so we need to pop our own handler that we just pushed.
popSuspenseHandler(workInProgress);
workInProgress.memoizedState = null;
return primaryChildFragment;
}
function mountDehydratedActivityComponent(
workInProgress: Fiber,
activityInstance: ActivityInstance,
renderLanes: Lanes,
): null | Fiber {
// During the first pass, we'll bail out and not drill into the children.
// Instead, we'll leave the content in place and try to hydrate it later.
// We'll continue hydrating the rest at offscreen priority since we'll already
// be showing the right content coming from the server, it is no rush.
workInProgress.lanes = laneToLanes(OffscreenLane);
return null;
}
function updateDehydratedActivityComponent(
current: Fiber,
workInProgress: Fiber,
didSuspend: boolean,
nextProps: ActivityProps,
activityInstance: ActivityInstance,
activityState: ActivityState,
renderLanes: Lanes,
): null | Fiber {
// We'll handle suspending since if something suspends we can just leave
// it dehydrated. We push early and then pop if we enter non-dehydrated attempts.
pushDehydratedActivitySuspenseHandler(workInProgress);
if (!didSuspend) {
// This is the first render pass. Attempt to hydrate.
// We should never be hydrating at this point because it is the first pass,
// but after we've already committed once.
warnIfHydrating();
if (includesSomeLane(renderLanes, (OffscreenLane: Lane))) {
// If we're rendering Offscreen and we're entering the activity then it's possible
// that the only reason we rendered was because this boundary left work. Provide
// it as a cause if another one doesn't already exist.
markRenderDerivedCause(workInProgress);
}
if (
// TODO: Factoring is a little weird, since we check this right below, too.
!didReceiveUpdate
) {
// We need to check if any children have context before we decide to bail
// out, so propagate the changes now.
lazilyPropagateParentContextChanges(current, workInProgress, renderLanes);
}
// We use lanes to indicate that a child might depend on context, so if
// any context has changed, we need to treat is as if the input might have changed.
const hasContextChanged = includesSomeLane(renderLanes, current.childLanes);
if (didReceiveUpdate || hasContextChanged) {
// This boundary has changed since the first render. This means that we are now unable to
// hydrate it. We might still be able to hydrate it using a higher priority lane.
const root = getWorkInProgressRoot();
if (root !== null) {
const attemptHydrationAtLane = getBumpedLaneForHydration(
root,
renderLanes,
);
if (
attemptHydrationAtLane !== NoLane &&
attemptHydrationAtLane !== activityState.retryLane
) {
// Intentionally mutating since this render will get interrupted. This
// is one of the very rare times where we mutate the current tree
// during the render phase.
activityState.retryLane = attemptHydrationAtLane;
enqueueConcurrentRenderForLane(current, attemptHydrationAtLane);
scheduleUpdateOnFiber(root, current, attemptHydrationAtLane);
// Throw a special object that signals to the work loop that it should
// interrupt the current render.
//
// Because we're inside a React-only execution stack, we don't
// strictly need to throw here — we could instead modify some internal
// work loop state. But using an exception means we don't need to
// check for this case on every iteration of the work loop. So doing
// it this way moves the check out of the fast path.
throw SelectiveHydrationException;
} else {
// We have already tried to ping at a higher priority than we're rendering with
// so if we got here, we must have failed to hydrate at those levels. We must
// now give up. Instead, we're going to delete the whole subtree and instead inject
// a new real Activity boundary to take its place. This might suspend for a while
// and if it does we might still have an opportunity to hydrate before this pass
// commits.
}
}
// If we did not selectively hydrate, we'll continue rendering without
// hydrating. Mark this tree as suspended to prevent it from committing
// outside a transition.
//
// This path should only happen if the hydration lane already suspended.
renderDidSuspendDelayIfPossible();
return retryActivityComponentWithoutHydrating(
current,
workInProgress,
renderLanes,
);
} else {
// This is the first attempt.
reenterHydrationStateFromDehydratedActivityInstance(
workInProgress,
activityInstance,
activityState.treeContext,
);
const primaryChildFragment = mountActivityChildren(
workInProgress,
nextProps,
renderLanes,
);
// Mark the children as hydrating. This is a fast path to know whether this
// tree is part of a hydrating tree. This is used to determine if a child
// node has fully mounted yet, and for scheduling event replaying.
// Conceptually this is similar to Placement in that a new subtree is
// inserted into the React tree here. It just happens to not need DOM
// mutations because it already exists.
primaryChildFragment.flags |= Hydrating;
return primaryChildFragment;
}
} else {
// This is the second render pass. We already attempted to hydrated, but
// something either suspended or errored.
if (workInProgress.flags & ForceClientRender) {
// Something errored during hydration. Try again without hydrating.
// The error should've already been logged in throwException.
workInProgress.flags &= ~ForceClientRender;
return retryActivityComponentWithoutHydrating(
current,
workInProgress,
renderLanes,
);
} else if ((workInProgress.memoizedState: null | ActivityState) !== null) {
// Something suspended and we should still be in dehydrated mode.
// Leave the existing child in place.
workInProgress.child = current.child;
// The dehydrated completion pass expects this flag to be there
// but the normal offscreen pass doesn't.
workInProgress.flags |= DidCapture;
return null;
} else {
// We called retryActivityComponentWithoutHydrating and tried client rendering
// but now we suspended again. We should never arrive here because we should
// not have pushed a suspense handler during that second pass and it should
// instead have suspended above.
throw new Error(
'Client rendering an Activity suspended it again. This is a bug in React.',
);
}
}
}
function updateActivityComponent(
current: null | Fiber,
workInProgress: Fiber,
renderLanes: Lanes,
) {
const nextProps: ActivityProps = workInProgress.pendingProps;
// Check if the first pass suspended.
const didSuspend = (workInProgress.flags & DidCapture) !== NoFlags;
workInProgress.flags &= ~DidCapture;
if (current === null) {
// Initial mount
// Special path for hydration
// If we're currently hydrating, try to hydrate this boundary.
// Hidden Activity boundaries are not emitted on the server.
if (getIsHydrating()) {
if (nextProps.mode === 'hidden') {
// SSR doesn't render hidden Activity so it shouldn't hydrate,
// even at offscreen lane. Defer to a client rendered offscreen lane.
const primaryChildFragment = mountActivityChildren(
workInProgress,
nextProps,
renderLanes,
);
workInProgress.lanes = laneToLanes(OffscreenLane);
return bailoutOffscreenComponent(null, primaryChildFragment);
} else {
// We must push the suspense handler context *before* attempting to
// hydrate, to avoid a mismatch in case it errors.
pushDehydratedActivitySuspenseHandler(workInProgress);
const dehydrated: ActivityInstance =
claimNextHydratableActivityInstance(workInProgress);
return mountDehydratedActivityComponent(
workInProgress,
dehydrated,
renderLanes,
);
}
}
return mountActivityChildren(workInProgress, nextProps, renderLanes);
} else {
// This is an update.
// Special path for hydration
const prevState: null | ActivityState = current.memoizedState;
if (prevState !== null) {
const dehydrated = prevState.dehydrated;
return updateDehydratedActivityComponent(
current,
workInProgress,
didSuspend,
nextProps,
dehydrated,
prevState,
renderLanes,
);
}
const currentChild: Fiber = (current.child: any);
const nextChildren = nextProps.children;
const nextMode = nextProps.mode;
const offscreenChildProps: OffscreenProps = {
mode: nextMode,
children: nextChildren,
};
if (
includesSomeLane(renderLanes, (OffscreenLane: Lane)) &&
includesSomeLane(renderLanes, current.lanes)
) {
// If we're rendering Offscreen and we're entering the activity then it's possible
// that the only reason we rendered was because this boundary left work. Provide
// it as a cause if another one doesn't already exist.
markRenderDerivedCause(workInProgress);
}
const primaryChildFragment = updateWorkInProgressOffscreenFiber(
currentChild,
offscreenChildProps,
);
primaryChildFragment.ref = workInProgress.ref;
workInProgress.child = primaryChildFragment;
primaryChildFragment.return = workInProgress;
return primaryChildFragment;
}
}
function updateCacheComponent(
current: Fiber | null,
workInProgress: Fiber,
renderLanes: Lanes,
) {
prepareToReadContext(workInProgress, renderLanes);
const parentCache = readContext(CacheContext);
if (current === null) {
// Initial mount. Request a fresh cache from the pool.
const freshCache = requestCacheFromPool(renderLanes);
const initialState: CacheComponentState = {
parent: parentCache,
cache: freshCache,
};
workInProgress.memoizedState = initialState;
initializeUpdateQueue(workInProgress);
pushCacheProvider(workInProgress, freshCache);
} else {
// Check for updates
if (includesSomeLane(current.lanes, renderLanes)) {
cloneUpdateQueue(current, workInProgress);
processUpdateQueue(workInProgress, null, null, renderLanes);
suspendIfUpdateReadFromEntangledAsyncAction();
}
const prevState: CacheComponentState = current.memoizedState;
const nextState: CacheComponentState = workInProgress.memoizedState;
// Compare the new parent cache to the previous to see detect there was
// a refresh.
if (prevState.parent !== parentCache) {
// Refresh in parent. Update the parent.
const derivedState: CacheComponentState = {
parent: parentCache,
cache: parentCache,
};
// Copied from getDerivedStateFromProps implementation. Once the update
// queue is empty, persist the derived state onto the base state.
workInProgress.memoizedState = derivedState;
if (workInProgress.lanes === NoLanes) {
const updateQueue: UpdateQueue<any> = (workInProgress.updateQueue: any);
workInProgress.memoizedState = updateQueue.baseState = derivedState;
}
pushCacheProvider(workInProgress, parentCache);
// No need to propagate a context change because the refreshed parent
// already did.
} else {
// The parent didn't refresh. Now check if this cache did.
const nextCache = nextState.cache;
pushCacheProvider(workInProgress, nextCache);
if (nextCache !== prevState.cache) {
// This cache refreshed. Propagate a context change.
propagateContextChange(workInProgress, CacheContext, renderLanes);
}
}
}
const nextProps: CacheProps = workInProgress.pendingProps;
const nextChildren = nextProps.children;
reconcileChildren(current, workInProgress, nextChildren, renderLanes);
return workInProgress.child;
}
// This should only be called if the name changes
function updateTracingMarkerComponent(
current: Fiber | null,
workInProgress: Fiber,
renderLanes: Lanes,
) {
if (!enableTransitionTracing) {
return null;
}
const nextProps: TracingMarkerProps = workInProgress.pendingProps;
// TODO: (luna) Only update the tracing marker if it's newly rendered or it's name changed.
// A tracing marker is only associated with the transitions that rendered
// or updated it, so we can create a new set of transitions each time
if (current === null) {
const currentTransitions = getPendingTransitions();
if (currentTransitions !== null) {
const markerInstance: TracingMarkerInstance = {
tag: TransitionTracingMarker,
transitions: new Set(currentTransitions),
pendingBoundaries: null,
name: nextProps.name,
aborts: null,
};
workInProgress.stateNode = markerInstance;
// We call the marker complete callback when all child suspense boundaries resolve.
// We do this in the commit phase on Offscreen. If the marker has no child suspense
// boundaries, we need to schedule a passive effect to make sure we call the marker
// complete callback.
workInProgress.flags |= Passive;
}
} else {
if (__DEV__) {
if (current.memoizedProps.name !== nextProps.name) {
console.error(
'Changing the name of a tracing marker after mount is not supported. ' +
'To remount the tracing marker, pass it a new key.',
);
}
}
}
const instance: TracingMarkerInstance | null = workInProgress.stateNode;
if (instance !== null) {
pushMarkerInstance(workInProgress, instance);
}
const nextChildren = nextProps.children;
reconcileChildren(current, workInProgress, nextChildren, renderLanes);
return workInProgress.child;
}
function updateFragment(
current: Fiber | null,
workInProgress: Fiber,
renderLanes: Lanes,
) {
const nextChildren = workInProgress.pendingProps;
if (enableFragmentRefs) {
markRef(current, workInProgress);
}
reconcileChildren(current, workInProgress, nextChildren, renderLanes);
return workInProgress.child;
}
function updateMode(
current: Fiber | null,
workInProgress: Fiber,
renderLanes: Lanes,
) {
const nextChildren = workInProgress.pendingProps.children;
reconcileChildren(current, workInProgress, nextChildren, renderLanes);
return workInProgress.child;
}
function updateProfiler(
current: Fiber | null,
workInProgress: Fiber,
renderLanes: Lanes,
) {
if (enableProfilerTimer) {
workInProgress.flags |= Update;
if (enableProfilerCommitHooks) {
// Schedule a passive effect for this Profiler to call onPostCommit hooks.
// This effect should be scheduled even if there is no onPostCommit callback for this Profiler,
// because the effect is also where times bubble to parent Profilers.
workInProgress.flags |= Passive;
// Reset effect durations for the next eventual effect phase.
// These are reset during render to allow the DevTools commit hook a chance to read them,
const stateNode = workInProgress.stateNode;
stateNode.effectDuration = -0;
stateNode.passiveEffectDuration = -0;
}
}
const nextProps: ProfilerProps = workInProgress.pendingProps;
const nextChildren = nextProps.children;
reconcileChildren(current, workInProgress, nextChildren, renderLanes);
return workInProgress.child;
}
function markRef(current: Fiber | null, workInProgress: Fiber) {
// TODO: Check props.ref instead of fiber.ref when enableRefAsProp is on.
const ref = workInProgress.ref;
if (ref === null) {
if (current !== null && current.ref !== null) {
// Schedule a Ref effect
workInProgress.flags |= Ref | RefStatic;
}
} else {
if (typeof ref !== 'function' && typeof ref !== 'object') {
throw new Error(
'Expected ref to be a function, an object returned by React.createRef(), or undefined/null.',
);
}
if (current === null || current.ref !== ref) {
// Schedule a Ref effect
workInProgress.flags |= Ref | RefStatic;
}
}
}
function mountIncompleteFunctionComponent(
_current: null | Fiber,
workInProgress: Fiber,
Component: any,
nextProps: any,
renderLanes: Lanes,
) {
resetSuspendedCurrentOnMountInLegacyMode(_current, workInProgress);
workInProgress.tag = FunctionComponent;
return updateFunctionComponent(
null,
workInProgress,
Component,
nextProps,
renderLanes,
);
}
function updateFunctionComponent(
current: null | Fiber,
workInProgress: Fiber,
Component: any,
nextProps: any,
renderLanes: Lanes,
) {
if (__DEV__) {
if (
Component.prototype &&
typeof Component.prototype.render === 'function'
) {
const componentName = getComponentNameFromType(Component) || 'Unknown';
if (!didWarnAboutBadClass[componentName]) {
console.error(
"The <%s /> component appears to have a render method, but doesn't extend React.Component. " +
'This is likely to cause errors. Change %s to extend React.Component instead.',
componentName,
componentName,
);
didWarnAboutBadClass[componentName] = true;
}
}
if (workInProgress.mode & StrictLegacyMode) {
ReactStrictModeWarnings.recordLegacyContextWarning(workInProgress, null);
}
if (current === null) {
// Some validations were previously done in mountIndeterminateComponent however and are now run
// in updateFuntionComponent but only on mount
validateFunctionComponentInDev(workInProgress, workInProgress.type);
if (Component.contextTypes) {
const componentName = getComponentNameFromType(Component) || 'Unknown';
if (!didWarnAboutContextTypes[componentName]) {
didWarnAboutContextTypes[componentName] = true;
if (disableLegacyContext) {
console.error(
'%s uses the legacy contextTypes API which was removed in React 19. ' +
'Use React.createContext() with React.useContext() instead. ' +
'(https://react.dev/link/legacy-context)',
componentName,
);
} else {
console.error(
'%s uses the legacy contextTypes API which will be removed soon. ' +
'Use React.createContext() with React.useContext() instead. ' +
'(https://react.dev/link/legacy-context)',
componentName,
);
}
}
}
}
}
let context;
if (!disableLegacyContext && !disableLegacyContextForFunctionComponents) {
const unmaskedContext = getUnmaskedContext(workInProgress, Component, true);
context = getMaskedContext(workInProgress, unmaskedContext);
}
let nextChildren;
let hasId;
prepareToReadContext(workInProgress, renderLanes);
if (enableSchedulingProfiler) {
markComponentRenderStarted(workInProgress);
}
if (__DEV__) {
nextChildren = renderWithHooks(
current,
workInProgress,
Component,
nextProps,
context,
renderLanes,
);
hasId = checkDidRenderIdHook();
} else {
nextChildren = renderWithHooks(
current,
workInProgress,
Component,
nextProps,
context,
renderLanes,
);
hasId = checkDidRenderIdHook();
}
if (enableSchedulingProfiler) {
markComponentRenderStopped();
}
if (current !== null && !didReceiveUpdate) {
bailoutHooks(current, workInProgress, renderLanes);
return bailoutOnAlreadyFinishedWork(current, workInProgress, renderLanes);
}
if (getIsHydrating() && hasId) {
pushMaterializedTreeId(workInProgress);
}
// React DevTools reads this flag.
workInProgress.flags |= PerformedWork;
reconcileChildren(current, workInProgress, nextChildren, renderLanes);
return workInProgress.child;
}
export function replayFunctionComponent(
current: Fiber | null,
workInProgress: Fiber,
nextProps: any,
Component: any,
secondArg: any,
renderLanes: Lanes,
): Fiber | null {
// This function is used to replay a component that previously suspended,
// after its data resolves. It's a simplified version of
// updateFunctionComponent that reuses the hooks from the previous attempt.
prepareToReadContext(workInProgress, renderLanes);
if (enableSchedulingProfiler) {
markComponentRenderStarted(workInProgress);
}
const nextChildren = replaySuspendedComponentWithHooks(
current,
workInProgress,
Component,
nextProps,
secondArg,
);
const hasId = checkDidRenderIdHook();
if (enableSchedulingProfiler) {
markComponentRenderStopped();
}
if (current !== null && !didReceiveUpdate) {
bailoutHooks(current, workInProgress, renderLanes);
return bailoutOnAlreadyFinishedWork(current, workInProgress, renderLanes);
}
if (getIsHydrating() && hasId) {
pushMaterializedTreeId(workInProgress);
}
// React DevTools reads this flag.
workInProgress.flags |= PerformedWork;
reconcileChildren(current, workInProgress, nextChildren, renderLanes);
return workInProgress.child;
}
function updateClassComponent(
current: Fiber | null,
workInProgress: Fiber,
Component: any,
nextProps: any,
renderLanes: Lanes,
) {
if (__DEV__) {
// This is used by DevTools to force a boundary to error.
switch (shouldError(workInProgress)) {
case false: {
const instance = workInProgress.stateNode;
const ctor = workInProgress.type;
// TODO This way of resetting the error boundary state is a hack.
// Is there a better way to do this?
const tempInstance = new ctor(
workInProgress.memoizedProps,
instance.context,
);
const state = tempInstance.state;
instance.updater.enqueueSetState(instance, state, null);
break;
}
case true: {
workInProgress.flags |= DidCapture;
workInProgress.flags |= ShouldCapture;
// eslint-disable-next-line react-internal/prod-error-codes
const error = new Error('Simulated error coming from DevTools');
const lane = pickArbitraryLane(renderLanes);
workInProgress.lanes = mergeLanes(workInProgress.lanes, lane);
// Schedule the error boundary to re-render using updated state
const root: FiberRoot | null = getWorkInProgressRoot();
if (root === null) {
throw new Error(
'Expected a work-in-progress root. This is a bug in React. Please file an issue.',
);
}
const update = createClassErrorUpdate(lane);
initializeClassErrorUpdate(
update,
root,
workInProgress,
createCapturedValueAtFiber(error, workInProgress),
);
enqueueCapturedUpdate(workInProgress, update);
break;
}
}
}
// Push context providers early to prevent context stack mismatches.
// During mounting we don't know the child context yet as the instance doesn't exist.
// We will invalidate the child context in finishClassComponent() right after rendering.
let hasContext;
if (isLegacyContextProvider(Component)) {
hasContext = true;
pushLegacyContextProvider(workInProgress);
} else {
hasContext = false;
}
prepareToReadContext(workInProgress, renderLanes);
const instance = workInProgress.stateNode;
let shouldUpdate;
if (instance === null) {
resetSuspendedCurrentOnMountInLegacyMode(current, workInProgress);
// In the initial pass we might need to construct the instance.
constructClassInstance(workInProgress, Component, nextProps);
mountClassInstance(workInProgress, Component, nextProps, renderLanes);
shouldUpdate = true;
} else if (current === null) {
// In a resume, we'll already have an instance we can reuse.
shouldUpdate = resumeMountClassInstance(
workInProgress,
Component,
nextProps,
renderLanes,
);
} else {
shouldUpdate = updateClassInstance(
current,
workInProgress,
Component,
nextProps,
renderLanes,
);
}
const nextUnitOfWork = finishClassComponent(
current,
workInProgress,
Component,
shouldUpdate,
hasContext,
renderLanes,
);
if (__DEV__) {
const inst = workInProgress.stateNode;
if (shouldUpdate && inst.props !== nextProps) {
if (!didWarnAboutReassigningProps) {
console.error(
'It looks like %s is reassigning its own `this.props` while rendering. ' +
'This is not supported and can lead to confusing bugs.',
getComponentNameFromFiber(workInProgress) || 'a component',
);
}
didWarnAboutReassigningProps = true;
}
}
return nextUnitOfWork;
}
function finishClassComponent(
current: Fiber | null,
workInProgress: Fiber,
Component: any,
shouldUpdate: boolean,
hasContext: boolean,
renderLanes: Lanes,
) {
// Refs should update even if shouldComponentUpdate returns false
markRef(current, workInProgress);
const didCaptureError = (workInProgress.flags & DidCapture) !== NoFlags;
if (!shouldUpdate && !didCaptureError) {
// Context providers should defer to sCU for rendering
if (hasContext) {
invalidateContextProvider(workInProgress, Component, false);
}
return bailoutOnAlreadyFinishedWork(current, workInProgress, renderLanes);
}
const instance = workInProgress.stateNode;
// Rerender
if (__DEV__) {
setCurrentFiber(workInProgress);
}
let nextChildren;
if (
didCaptureError &&
typeof Component.getDerivedStateFromError !== 'function'
) {
// If we captured an error, but getDerivedStateFromError is not defined,
// unmount all the children. componentDidCatch will schedule an update to
// re-render a fallback. This is temporary until we migrate everyone to
// the new API.
// TODO: Warn in a future release.
nextChildren = null;
if (enableProfilerTimer) {
stopProfilerTimerIfRunning(workInProgress);
}
} else {
if (enableSchedulingProfiler) {
markComponentRenderStarted(workInProgress);
}
if (__DEV__) {
nextChildren = callRenderInDEV(instance);
if (workInProgress.mode & StrictLegacyMode) {
setIsStrictModeForDevtools(true);
try {
callRenderInDEV(instance);
} finally {
setIsStrictModeForDevtools(false);
}
}
} else {
nextChildren = instance.render();
}
if (enableSchedulingProfiler) {
markComponentRenderStopped();
}
}
// React DevTools reads this flag.
workInProgress.flags |= PerformedWork;
if (current !== null && didCaptureError) {
// If we're recovering from an error, reconcile without reusing any of
// the existing children. Conceptually, the normal children and the children
// that are shown on error are two different sets, so we shouldn't reuse
// normal children even if their identities match.
forceUnmountCurrentAndReconcile(
current,
workInProgress,
nextChildren,
renderLanes,
);
} else {
reconcileChildren(current, workInProgress, nextChildren, renderLanes);
}
// Memoize state using the values we just used to render.
// TODO: Restructure so we never read values from the instance.
workInProgress.memoizedState = instance.state;
// The context might have changed so we need to recalculate it.
if (hasContext) {
invalidateContextProvider(workInProgress, Component, true);
}
return workInProgress.child;
}
function pushHostRootContext(workInProgress: Fiber) {
const root = (workInProgress.stateNode: FiberRoot);
if (root.pendingContext) {
pushTopLevelContextObject(
workInProgress,
root.pendingContext,
root.pendingContext !== root.context,
);
} else if (root.context) {
// Should always be set
pushTopLevelContextObject(workInProgress, root.context, false);
}
pushHostContainer(workInProgress, root.containerInfo);
}
function updateHostRoot(
current: null | Fiber,
workInProgress: Fiber,
renderLanes: Lanes,
) {
pushHostRootContext(workInProgress);
if (current === null) {
throw new Error('Should have a current fiber. This is a bug in React.');
}
const nextProps = workInProgress.pendingProps;
const prevState: RootState = workInProgress.memoizedState;
const prevChildren = prevState.element;
cloneUpdateQueue(current, workInProgress);
processUpdateQueue(workInProgress, nextProps, null, renderLanes);
const nextState: RootState = workInProgress.memoizedState;
const root: FiberRoot = workInProgress.stateNode;
pushRootTransition(workInProgress, root, renderLanes);
if (enableTransitionTracing) {
pushRootMarkerInstance(workInProgress);
}
const nextCache: Cache = nextState.cache;
pushCacheProvider(workInProgress, nextCache);
if (nextCache !== prevState.cache) {
// The root cache refreshed.
propagateContextChange(workInProgress, CacheContext, renderLanes);
}
// This would ideally go inside processUpdateQueue, but because it suspends,
// it needs to happen after the `pushCacheProvider` call above to avoid a
// context stack mismatch. A bit unfortunate.
suspendIfUpdateReadFromEntangledAsyncAction();
// Caution: React DevTools currently depends on this property
// being called "element".
const nextChildren = nextState.element;
if (supportsHydration && prevState.isDehydrated) {
// This is a hydration root whose shell has not yet hydrated. We should
// attempt to hydrate.
// Flip isDehydrated to false to indicate that when this render
// finishes, the root will no longer be dehydrated.
const overrideState: RootState = {
element: nextChildren,
isDehydrated: false,
cache: nextState.cache,
};
const updateQueue: UpdateQueue<RootState> =
(workInProgress.updateQueue: any);
// `baseState` can always be the last state because the root doesn't
// have reducer functions so it doesn't need rebasing.
updateQueue.baseState = overrideState;
workInProgress.memoizedState = overrideState;
if (workInProgress.flags & ForceClientRender) {
// Something errored during a previous attempt to hydrate the shell, so we
// forced a client render. We should have a recoverable error already scheduled.
return mountHostRootWithoutHydrating(
current,
workInProgress,
nextChildren,
renderLanes,
);
} else if (nextChildren !== prevChildren) {
const recoverableError = createCapturedValueAtFiber<mixed>(
new Error(
'This root received an early update, before anything was able ' +
'hydrate. Switched the entire root to client rendering.',
),
workInProgress,
);
queueHydrationError(recoverableError);
return mountHostRootWithoutHydrating(
current,
workInProgress,
nextChildren,
renderLanes,
);
} else {
// The outermost shell has not hydrated yet. Start hydrating.
enterHydrationState(workInProgress);
const child = mountChildFibers(
workInProgress,
null,
nextChildren,
renderLanes,
);
workInProgress.child = child;
let node = child;
while (node) {
// Mark each child as hydrating. This is a fast path to know whether this
// tree is part of a hydrating tree. This is used to determine if a child
// node has fully mounted yet, and for scheduling event replaying.
// Conceptually this is similar to Placement in that a new subtree is
// inserted into the React tree here. It just happens to not need DOM
// mutations because it already exists.
node.flags = (node.flags & ~Placement) | Hydrating;
node = node.sibling;
}
}
} else {
// Root is not dehydrated. Either this is a client-only root, or it
// already hydrated.
resetHydrationState();
if (nextChildren === prevChildren) {
return bailoutOnAlreadyFinishedWork(current, workInProgress, renderLanes);
}
reconcileChildren(current, workInProgress, nextChildren, renderLanes);
}
return workInProgress.child;
}
function mountHostRootWithoutHydrating(
current: Fiber,
workInProgress: Fiber,
nextChildren: ReactNodeList,
renderLanes: Lanes,
) {
// Revert to client rendering.
resetHydrationState();
workInProgress.flags |= ForceClientRender;
reconcileChildren(current, workInProgress, nextChildren, renderLanes);
return workInProgress.child;
}
function updateHostComponent(
current: Fiber | null,
workInProgress: Fiber,
renderLanes: Lanes,
) {
if (current === null) {
tryToClaimNextHydratableInstance(workInProgress);
}
pushHostContext(workInProgress);
const type = workInProgress.type;
const nextProps = workInProgress.pendingProps;
const prevProps = current !== null ? current.memoizedProps : null;
let nextChildren = nextProps.children;
const isDirectTextChild = shouldSetTextContent(type, nextProps);
if (isDirectTextChild) {
// We special case a direct text child of a host node. This is a common
// case. We won't handle it as a reified child. We will instead handle
// this in the host environment that also has access to this prop. That
// avoids allocating another HostText fiber and traversing it.
nextChildren = null;
} else if (prevProps !== null && shouldSetTextContent(type, prevProps)) {
// If we're switching from a direct text child to a normal child, or to
// empty, we need to schedule the text content to be reset.
workInProgress.flags |= ContentReset;
}
const memoizedState = workInProgress.memoizedState;
if (memoizedState !== null) {
// This fiber has been upgraded to a stateful component. The only way
// happens currently is for form actions. We use hooks to track the
// pending and error state of the form.
//
// Once a fiber is upgraded to be stateful, it remains stateful for the
// rest of its lifetime.
const newState = renderTransitionAwareHostComponentWithHooks(
current,
workInProgress,
renderLanes,
);
// If the transition state changed, propagate the change to all the
// descendents. We use Context as an implementation detail for this.
//
// This is intentionally set here instead of pushHostContext because
// pushHostContext gets called before we process the state hook, to avoid
// a state mismatch in the event that something suspends.
//
// NOTE: This assumes that there cannot be nested transition providers,
// because the only renderer that implements this feature is React DOM,
// and forms cannot be nested. If we did support nested providers, then
// we would need to push a context value even for host fibers that
// haven't been upgraded yet.
if (isPrimaryRenderer) {
HostTransitionContext._currentValue = newState;
} else {
HostTransitionContext._currentValue2 = newState;
}
}
markRef(current, workInProgress);
reconcileChildren(current, workInProgress, nextChildren, renderLanes);
return workInProgress.child;
}
function updateHostHoistable(
current: null | Fiber,
workInProgress: Fiber,
renderLanes: Lanes,
) {
markRef(current, workInProgress);
if (current === null) {
const resource = getResource(
workInProgress.type,
null,
workInProgress.pendingProps,
null,
);
if (resource) {
workInProgress.memoizedState = resource;
} else {
if (!getIsHydrating()) {
// This is not a Resource Hoistable and we aren't hydrating so we construct the instance.
workInProgress.stateNode = createHoistableInstance(
workInProgress.type,
workInProgress.pendingProps,
getRootHostContainer(),
workInProgress,
);
}
}
} else {
// Get Resource may or may not return a resource. either way we stash the result
// on memoized state.
workInProgress.memoizedState = getResource(
workInProgress.type,
current.memoizedProps,
workInProgress.pendingProps,
current.memoizedState,
);
}
// Resources never have reconciler managed children. It is possible for
// the host implementation of getResource to consider children in the
// resource construction but they will otherwise be discarded. In practice
// this precludes all but the simplest children and Host specific warnings
// should be implemented to warn when children are passsed when otherwise not
// expected
return null;
}
function updateHostSingleton(
current: Fiber | null,
workInProgress: Fiber,
renderLanes: Lanes,
) {
pushHostContext(workInProgress);
if (current === null) {
claimHydratableSingleton(workInProgress);
}
const nextChildren = workInProgress.pendingProps.children;
reconcileChildren(current, workInProgress, nextChildren, renderLanes);
markRef(current, workInProgress);
if (current === null) {
// We mark Singletons with a static flag to more efficiently manage their
// ownership of the singleton host instance when in offscreen trees including Suspense
workInProgress.flags |= LayoutStatic;
}
return workInProgress.child;
}
function updateHostText(current: null | Fiber, workInProgress: Fiber) {
if (current === null) {
tryToClaimNextHydratableTextInstance(workInProgress);
}
// Nothing to do here. This is terminal. We'll do the completion step
// immediately after.
return null;
}
function mountLazyComponent(
_current: null | Fiber,
workInProgress: Fiber,
elementType: any,
renderLanes: Lanes,
) {
resetSuspendedCurrentOnMountInLegacyMode(_current, workInProgress);
const props = workInProgress.pendingProps;
const lazyComponent: LazyComponentType<any, any> = elementType;
let Component = resolveLazy(lazyComponent);
// Store the unwrapped component in the type.
workInProgress.type = Component;
if (typeof Component === 'function') {
if (isFunctionClassComponent(Component)) {
const resolvedProps = resolveClassComponentProps(Component, props);
workInProgress.tag = ClassComponent;
if (__DEV__) {
workInProgress.type = Component =
resolveClassForHotReloading(Component);
}
return updateClassComponent(
null,
workInProgress,
Component,
resolvedProps,
renderLanes,
);
} else {
workInProgress.tag = FunctionComponent;
if (__DEV__) {
validateFunctionComponentInDev(workInProgress, Component);
workInProgress.type = Component =
resolveFunctionForHotReloading(Component);
}
return updateFunctionComponent(
null,
workInProgress,
Component,
props,
renderLanes,
);
}
} else if (Component !== undefined && Component !== null) {
const $$typeof = Component.$$typeof;
if ($$typeof === REACT_FORWARD_REF_TYPE) {
workInProgress.tag = ForwardRef;
if (__DEV__) {
workInProgress.type = Component =
resolveForwardRefForHotReloading(Component);
}
return updateForwardRef(
null,
workInProgress,
Component,
props,
renderLanes,
);
} else if ($$typeof === REACT_MEMO_TYPE) {
workInProgress.tag = MemoComponent;
return updateMemoComponent(
null,
workInProgress,
Component,
props,
renderLanes,
);
}
}
let hint = '';
if (__DEV__) {
if (
Component !== null &&
typeof Component === 'object' &&
Component.$$typeof === REACT_LAZY_TYPE
) {
hint = ' Did you wrap a component in React.lazy() more than once?';
}
}
const loggedComponent = getComponentNameFromType(Component) || Component;
// This message intentionally doesn't mention ForwardRef or MemoComponent
// because the fact that it's a separate type of work is an
// implementation detail.
throw new Error(
`Element type is invalid. Received a promise that resolves to: ${loggedComponent}. ` +
`Lazy element type must resolve to a class or function.${hint}`,
);
}
function mountIncompleteClassComponent(
_current: null | Fiber,
workInProgress: Fiber,
Component: any,
nextProps: any,
renderLanes: Lanes,
) {
resetSuspendedCurrentOnMountInLegacyMode(_current, workInProgress);
// Promote the fiber to a class and try rendering again.
workInProgress.tag = ClassComponent;
// The rest of this function is a fork of `updateClassComponent`
// Push context providers early to prevent context stack mismatches.
// During mounting we don't know the child context yet as the instance doesn't exist.
// We will invalidate the child context in finishClassComponent() right after rendering.
let hasContext;
if (isLegacyContextProvider(Component)) {
hasContext = true;
pushLegacyContextProvider(workInProgress);
} else {
hasContext = false;
}
prepareToReadContext(workInProgress, renderLanes);
constructClassInstance(workInProgress, Component, nextProps);
mountClassInstance(workInProgress, Component, nextProps, renderLanes);
return finishClassComponent(
null,
workInProgress,
Component,
true,
hasContext,
renderLanes,
);
}
function validateFunctionComponentInDev(workInProgress: Fiber, Component: any) {
if (__DEV__) {
if (Component && Component.childContextTypes) {
console.error(
'childContextTypes cannot be defined on a function component.\n' +
' %s.childContextTypes = ...',
Component.displayName || Component.name || 'Component',
);
}
if (typeof Component.getDerivedStateFromProps === 'function') {
const componentName = getComponentNameFromType(Component) || 'Unknown';
if (!didWarnAboutGetDerivedStateOnFunctionComponent[componentName]) {
console.error(
'%s: Function components do not support getDerivedStateFromProps.',
componentName,
);
didWarnAboutGetDerivedStateOnFunctionComponent[componentName] = true;
}
}
if (
typeof Component.contextType === 'object' &&
Component.contextType !== null
) {
const componentName = getComponentNameFromType(Component) || 'Unknown';
if (!didWarnAboutContextTypeOnFunctionComponent[componentName]) {
console.error(
'%s: Function components do not support contextType.',
componentName,
);
didWarnAboutContextTypeOnFunctionComponent[componentName] = true;
}
}
}
}
const SUSPENDED_MARKER: SuspenseState = {
dehydrated: null,
treeContext: null,
retryLane: NoLane,
hydrationErrors: null,
};
function mountSuspenseOffscreenState(renderLanes: Lanes): OffscreenState {
return {
baseLanes: renderLanes,
cachePool: getSuspendedCache(),
};
}
function updateSuspenseOffscreenState(
prevOffscreenState: OffscreenState,
renderLanes: Lanes,
): OffscreenState {
let cachePool: SpawnedCachePool | null = null;
const prevCachePool: SpawnedCachePool | null = prevOffscreenState.cachePool;
if (prevCachePool !== null) {
const parentCache = isPrimaryRenderer
? CacheContext._currentValue
: CacheContext._currentValue2;
if (prevCachePool.parent !== parentCache) {
// Detected a refresh in the parent. This overrides any previously
// suspended cache.
cachePool = {
parent: parentCache,
pool: parentCache,
};
} else {
// We can reuse the cache from last time. The only thing that would have
// overridden it is a parent refresh, which we checked for above.
cachePool = prevCachePool;
}
} else {
// If there's no previous cache pool, grab the current one.
cachePool = getSuspendedCache();
}
return {
baseLanes: mergeLanes(prevOffscreenState.baseLanes, renderLanes),
cachePool,
};
}
// TODO: Probably should inline this back
function shouldRemainOnFallback(
current: null | Fiber,
workInProgress: Fiber,
renderLanes: Lanes,
) {
// If we're already showing a fallback, there are cases where we need to
// remain on that fallback regardless of whether the content has resolved.
// For example, SuspenseList coordinates when nested content appears.
// TODO: For compatibility with offscreen prerendering, this should also check
// whether the current fiber (if it exists) was visible in the previous tree.
if (current !== null) {
const suspenseState: SuspenseState = current.memoizedState;
if (suspenseState === null) {
// Currently showing content. Don't hide it, even if ForceSuspenseFallback
// is true. More precise name might be "ForceRemainSuspenseFallback".
// Note: This is a factoring smell. Can't remain on a fallback if there's
// no fallback to remain on.
return false;
}
}
// Not currently showing content. Consult the Suspense context.
const suspenseContext: SuspenseContext = suspenseStackCursor.current;
return hasSuspenseListContext(
suspenseContext,
(ForceSuspenseFallback: SuspenseContext),
);
}
function getRemainingWorkInPrimaryTree(
current: Fiber | null,
primaryTreeDidDefer: boolean,
renderLanes: Lanes,
) {
let remainingLanes =
current !== null ? removeLanes(current.childLanes, renderLanes) : NoLanes;
if (primaryTreeDidDefer) {
// A useDeferredValue hook spawned a deferred task inside the primary tree.
// Ensure that we retry this component at the deferred priority.
// TODO: We could make this a per-subtree value instead of a global one.
// Would need to track it on the context stack somehow, similar to what
// we'd have to do for resumable contexts.
remainingLanes = mergeLanes(remainingLanes, peekDeferredLane());
}
return remainingLanes;
}
function updateSuspenseComponent(
current: null | Fiber,
workInProgress: Fiber,
renderLanes: Lanes,
) {
const nextProps: SuspenseProps = workInProgress.pendingProps;
// This is used by DevTools to force a boundary to suspend.
if (__DEV__) {
if (shouldSuspend(workInProgress)) {
workInProgress.flags |= DidCapture;
}
}
let showFallback = false;
const didSuspend = (workInProgress.flags & DidCapture) !== NoFlags;
if (
didSuspend ||
shouldRemainOnFallback(current, workInProgress, renderLanes)
) {
// Something in this boundary's subtree already suspended. Switch to
// rendering the fallback children.
showFallback = true;
workInProgress.flags &= ~DidCapture;
}
// Check if the primary children spawned a deferred task (useDeferredValue)
// during the first pass.
const didPrimaryChildrenDefer = (workInProgress.flags & DidDefer) !== NoFlags;
workInProgress.flags &= ~DidDefer;
// 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.
//
// First, Legacy Mode has different semantics for backwards compatibility. The
// primary tree will commit in an inconsistent state, so when we do the
// second pass to render the fallback, we do some exceedingly, uh, clever
// hacks to make that not totally break. Like transferring effects and
// deletions from hidden tree. In Concurrent Mode, it's much simpler,
// because we bailout on the primary tree completely and leave it in its old
// state, no effects. Same as what we do for Offscreen (except that
// Offscreen doesn't have the first render pass).
//
// Second is hydration. During hydration, the Suspense fiber has a slightly
// different layout, where the child points to a dehydrated fragment, which
// contains the DOM rendered by the server.
//
// Third, even if you set all that aside, Suspense is like error boundaries in
// that we first we try to render one tree, and if that fails, we render again
// and switch to a different tree. Like a try/catch block. So we have to track
// which branch we're currently rendering. Ideally we would model this using
// a stack.
if (current === null) {
// Initial mount
// Special path for hydration
// If we're currently hydrating, try to hydrate this boundary.
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);
}
// This throws if we fail to hydrate.
const dehydrated: SuspenseInstance =
claimNextHydratableSuspenseInstance(workInProgress);
return mountDehydratedSuspenseComponent(
workInProgress,
dehydrated,
renderLanes,
);
}
const nextPrimaryChildren = nextProps.children;
const nextFallbackChildren = nextProps.fallback;
if (showFallback) {
pushFallbackTreeSuspenseHandler(workInProgress);
mountSuspenseFallbackChildren(
workInProgress,
nextPrimaryChildren,
nextFallbackChildren,
renderLanes,
);
const primaryChildFragment: Fiber = (workInProgress.child: any);
primaryChildFragment.memoizedState =
mountSuspenseOffscreenState(renderLanes);
primaryChildFragment.childLanes = getRemainingWorkInPrimaryTree(
current,
didPrimaryChildrenDefer,
renderLanes,
);
workInProgress.memoizedState = SUSPENDED_MARKER;
if (enableTransitionTracing) {
const currentTransitions = getPendingTransitions();
if (currentTransitions !== null) {
const parentMarkerInstances = getMarkerInstances();
const offscreenQueue: OffscreenQueue | null =
(primaryChildFragment.updateQueue: any);
if (offscreenQueue === null) {
const newOffscreenQueue: OffscreenQueue = {
transitions: currentTransitions,
markerInstances: parentMarkerInstances,
retryQueue: null,
};
primaryChildFragment.updateQueue = newOffscreenQueue;
} else {
offscreenQueue.transitions = currentTransitions;
offscreenQueue.markerInstances = parentMarkerInstances;
}
}
}
return bailoutOffscreenComponent(null, primaryChildFragment);
} else if (
enableCPUSuspense &&
typeof nextProps.unstable_expectedLoadTime === 'number'
) {
// 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);
mountSuspenseFallbackChildren(
workInProgress,
nextPrimaryChildren,
nextFallbackChildren,
renderLanes,
);
const primaryChildFragment: Fiber = (workInProgress.child: any);
primaryChildFragment.memoizedState =
mountSuspenseOffscreenState(renderLanes);
primaryChildFragment.childLanes = getRemainingWorkInPrimaryTree(
current,
didPrimaryChildrenDefer,
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
// not part of the same transition once the transition has committed. If
// it's sync, we still want to yield so that it can be painted.
// Conceptually, this is really the same as pinging. We can use any
// RetryLane even if it's the one currently rendering since we're leaving
// it behind on this node.
workInProgress.lanes = SomeRetryLane;
return bailoutOffscreenComponent(null, primaryChildFragment);
} else {
pushPrimaryTreeSuspenseHandler(workInProgress);
return mountSuspensePrimaryChildren(
workInProgress,
nextPrimaryChildren,
renderLanes,
);
}
} else {
// This is an update.
// Special path for hydration
const prevState: null | SuspenseState = current.memoizedState;
if (prevState !== null) {
const dehydrated = prevState.dehydrated;
if (dehydrated !== null) {
return updateDehydratedSuspenseComponent(
current,
workInProgress,
didSuspend,
didPrimaryChildrenDefer,
nextProps,
dehydrated,
prevState,
renderLanes,
);
}
}
if (showFallback) {
pushFallbackTreeSuspenseHandler(workInProgress);
const nextFallbackChildren = nextProps.fallback;
const nextPrimaryChildren = nextProps.children;
updateSuspenseFallbackChildren(
current,
workInProgress,
nextPrimaryChildren,
nextFallbackChildren,
renderLanes,
);
const primaryChildFragment: Fiber = (workInProgress.child: any);
const prevOffscreenState: OffscreenState | null = (current.child: any)
.memoizedState;
primaryChildFragment.memoizedState =
prevOffscreenState === null
? mountSuspenseOffscreenState(renderLanes)
: updateSuspenseOffscreenState(prevOffscreenState, renderLanes);
if (enableTransitionTracing) {
const currentTransitions = getPendingTransitions();
if (currentTransitions !== null) {
const parentMarkerInstances = getMarkerInstances();
const offscreenQueue: OffscreenQueue | null =
(primaryChildFragment.updateQueue: any);
const currentOffscreenQueue: OffscreenQueue | null =
(current.updateQueue: any);
if (offscreenQueue === null) {
const newOffscreenQueue: OffscreenQueue = {
transitions: currentTransitions,
markerInstances: parentMarkerInstances,
retryQueue: 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,
retryQueue:
currentOffscreenQueue !== null
? currentOffscreenQueue.retryQueue
: null,
};
primaryChildFragment.updateQueue = newOffscreenQueue;
} else {
offscreenQueue.transitions = currentTransitions;
offscreenQueue.markerInstances = parentMarkerInstances;
}
}
}
primaryChildFragment.childLanes = getRemainingWorkInPrimaryTree(
current,
didPrimaryChildrenDefer,
renderLanes,
);
workInProgress.memoizedState = SUSPENDED_MARKER;
return bailoutOffscreenComponent(current.child, primaryChildFragment);
} else {
if (
prevState !== null &&
includesOnlyRetries(renderLanes) &&
includesSomeLane(renderLanes, current.lanes)
) {
// If we're rendering Retry lanes and we're entering the primary content then it's possible
// that the only reason we rendered was because we left this boundary to be warmed up but
// nothing else scheduled an update. If so, use it as the cause of the render.
markRenderDerivedCause(workInProgress);
}
pushPrimaryTreeSuspenseHandler(workInProgress);
const nextPrimaryChildren = nextProps.children;
const primaryChildFragment = updateSuspensePrimaryChildren(
current,
workInProgress,
nextPrimaryChildren,
renderLanes,
);
workInProgress.memoizedState = null;
return primaryChildFragment;
}
}
}
function mountSuspensePrimaryChildren(
workInProgress: Fiber,
primaryChildren: $FlowFixMe,
renderLanes: Lanes,
) {
const mode = workInProgress.mode;
const primaryChildProps: OffscreenProps = {
mode: 'visible',
children: primaryChildren,
};
const primaryChildFragment = mountWorkInProgressOffscreenFiber(
primaryChildProps,
mode,
renderLanes,
);
primaryChildFragment.return = workInProgress;
workInProgress.child = primaryChildFragment;
return primaryChildFragment;
}
function mountSuspenseFallbackChildren(
workInProgress: Fiber,
primaryChildren: $FlowFixMe,
fallbackChildren: $FlowFixMe,
renderLanes: Lanes,
) {
const mode = workInProgress.mode;
const progressedPrimaryFragment: Fiber | null = workInProgress.child;
const primaryChildProps: OffscreenProps = {
mode: 'hidden',
children: primaryChildren,
};
let primaryChildFragment;
let fallbackChildFragment;
if (
!disableLegacyMode &&
(mode & ConcurrentMode) === NoMode &&
progressedPrimaryFragment !== null
) {
// In legacy mode, we commit the primary tree as if it successfully
// completed, even though it's in an inconsistent state.
primaryChildFragment = progressedPrimaryFragment;
primaryChildFragment.childLanes = NoLanes;
primaryChildFragment.pendingProps = primaryChildProps;
if (enableProfilerTimer && workInProgress.mode & ProfileMode) {
// Reset the durations from the first pass so they aren't included in the
// final amounts. This seems counterintuitive, since we're intentionally
// not measuring part of the render phase, but this makes it match what we
// do in Concurrent Mode.
primaryChildFragment.actualDuration = -0;
primaryChildFragment.actualStartTime = -1.1;
primaryChildFragment.selfBaseDuration = -0;
primaryChildFragment.treeBaseDuration = -0;
}
fallbackChildFragment = createFiberFromFragment(
fallbackChildren,
mode,
renderLanes,
null,
);
} else {
primaryChildFragment = mountWorkInProgressOffscreenFiber(
primaryChildProps,
mode,
NoLanes,
);
fallbackChildFragment = createFiberFromFragment(
fallbackChildren,
mode,
renderLanes,
null,
);
}
primaryChildFragment.return = workInProgress;
fallbackChildFragment.return = workInProgress;
primaryChildFragment.sibling = fallbackChildFragment;
workInProgress.child = primaryChildFragment;
return fallbackChildFragment;
}
function mountWorkInProgressOffscreenFiber(
offscreenProps: OffscreenProps,
mode: TypeOfMode,
renderLanes: Lanes,
) {
// The props argument to `createFiberFromOffscreen` is `any` typed, so we use
// this wrapper function to constrain it.
return createFiberFromOffscreen(offscreenProps, mode, NoLanes, null);
}
function updateWorkInProgressOffscreenFiber(
current: Fiber,
offscreenProps: OffscreenProps,
) {
// The props argument to `createWorkInProgress` is `any` typed, so we use this
// wrapper function to constrain it.
return createWorkInProgress(current, offscreenProps);
}
function updateSuspensePrimaryChildren(
current: Fiber,
workInProgress: Fiber,
primaryChildren: $FlowFixMe,
renderLanes: Lanes,
) {
const currentPrimaryChildFragment: Fiber = (current.child: any);
const currentFallbackChildFragment: Fiber | null =
currentPrimaryChildFragment.sibling;
const primaryChildFragment = updateWorkInProgressOffscreenFiber(
currentPrimaryChildFragment,
{
mode: 'visible',
children: primaryChildren,
},
);
if (!disableLegacyMode && (workInProgress.mode & ConcurrentMode) === NoMode) {
primaryChildFragment.lanes = renderLanes;
}
primaryChildFragment.return = workInProgress;
primaryChildFragment.sibling = null;
if (currentFallbackChildFragment !== null) {
// Delete the fallback child fragment
const deletions = workInProgress.deletions;
if (deletions === null) {
workInProgress.deletions = [currentFallbackChildFragment];
workInProgress.flags |= ChildDeletion;
} else {
deletions.push(currentFallbackChildFragment);
}
}
workInProgress.child = primaryChildFragment;
return primaryChildFragment;
}
function updateSuspenseFallbackChildren(
current: Fiber,
workInProgress: Fiber,
primaryChildren: $FlowFixMe,
fallbackChildren: $FlowFixMe,
renderLanes: Lanes,
) {
const mode = workInProgress.mode;
const currentPrimaryChildFragment: Fiber = (current.child: any);
const currentFallbackChildFragment: Fiber | null =
currentPrimaryChildFragment.sibling;
const primaryChildProps: OffscreenProps = {
mode: 'hidden',
children: primaryChildren,
};
let primaryChildFragment;
if (
// In legacy mode, we commit the primary tree as if it successfully
// completed, even though it's in an inconsistent state.
!disableLegacyMode &&
(mode & ConcurrentMode) === NoMode &&
// Make sure we're on the second pass, i.e. the primary child fragment was
// already cloned. In legacy mode, the only case where this isn't true is
// when DevTools forces us to display a fallback; we skip the first render
// pass entirely and go straight to rendering the fallback. (In Concurrent
// Mode, SuspenseList can also trigger this scenario, but this is a legacy-
// only codepath.)
workInProgress.child !== currentPrimaryChildFragment
) {
const progressedPrimaryFragment: Fiber = (workInProgress.child: any);
primaryChildFragment = progressedPrimaryFragment;
primaryChildFragment.childLanes = NoLanes;
primaryChildFragment.pendingProps = primaryChildProps;
if (enableProfilerTimer && workInProgress.mode & ProfileMode) {
// Reset the durations from the first pass so they aren't included in the
// final amounts. This seems counterintuitive, since we're intentionally
// not measuring part of the render phase, but this makes it match what we
// do in Concurrent Mode.
primaryChildFragment.actualDuration = -0;
primaryChildFragment.actualStartTime = -1.1;
primaryChildFragment.selfBaseDuration =
currentPrimaryChildFragment.selfBaseDuration;
primaryChildFragment.treeBaseDuration =
currentPrimaryChildFragment.treeBaseDuration;
}
// The fallback fiber was added as a deletion during the first pass.
// However, since we're going to remain on the fallback, we no longer want
// to delete it.
workInProgress.deletions = null;
} else {
primaryChildFragment = updateWorkInProgressOffscreenFiber(
currentPrimaryChildFragment,
primaryChildProps,
);
// Since we're reusing a current tree, we need to reuse the flags, too.
// (We don't do this in legacy mode, because in legacy mode we don't re-use
// the current tree; see previous branch.)
primaryChildFragment.subtreeFlags =
currentPrimaryChildFragment.subtreeFlags & StaticMask;
}
let fallbackChildFragment;
if (currentFallbackChildFragment !== null) {
fallbackChildFragment = createWorkInProgress(
currentFallbackChildFragment,
fallbackChildren,
);
} else {
fallbackChildFragment = createFiberFromFragment(
fallbackChildren,
mode,
renderLanes,
null,
);
// Needs a placement effect because the parent (the Suspense boundary) already
// mounted but this is a new fiber.
fallbackChildFragment.flags |= Placement;
}
fallbackChildFragment.return = workInProgress;
primaryChildFragment.return = workInProgress;
primaryChildFragment.sibling = fallbackChildFragment;
workInProgress.child = primaryChildFragment;
return bailoutOffscreenComponent(null, primaryChildFragment);
}
function retrySuspenseComponentWithoutHydrating(
current: Fiber,
workInProgress: Fiber,
renderLanes: Lanes,
) {
// Falling back to client rendering. Because this has performance
// implications, it's considered a recoverable error, even though the user
// likely won't observe anything wrong with the UI.
// This will add the old fiber to the deletion list
reconcileChildFibers(workInProgress, current.child, null, renderLanes);
// We're now not suspended nor dehydrated.
const nextProps = workInProgress.pendingProps;
const primaryChildren = nextProps.children;
const primaryChildFragment = mountSuspensePrimaryChildren(
workInProgress,
primaryChildren,
renderLanes,
);
// Needs a placement effect because the parent (the Suspense boundary) already
// mounted but this is a new fiber.
primaryChildFragment.flags |= Placement;
workInProgress.memoizedState = null;
return primaryChildFragment;
}
function mountSuspenseFallbackAfterRetryWithoutHydrating(
current: Fiber,
workInProgress: Fiber,
primaryChildren: $FlowFixMe,
fallbackChildren: $FlowFixMe,
renderLanes: Lanes,
) {
const fiberMode = workInProgress.mode;
const primaryChildProps: OffscreenProps = {
mode: 'visible',
children: primaryChildren,
};
const primaryChildFragment = mountWorkInProgressOffscreenFiber(
primaryChildProps,
fiberMode,
NoLanes,
);
const fallbackChildFragment = createFiberFromFragment(
fallbackChildren,
fiberMode,
renderLanes,
null,
);
// Needs a placement effect because the parent (the Suspense
// boundary) already mounted but this is a new fiber.
fallbackChildFragment.flags |= Placement;
primaryChildFragment.return = workInProgress;
fallbackChildFragment.return = workInProgress;
primaryChildFragment.sibling = fallbackChildFragment;
workInProgress.child = primaryChildFragment;
if (disableLegacyMode || (workInProgress.mode & ConcurrentMode) !== NoMode) {
// We will have dropped the effect list which contains the
// deletion. We need to reconcile to delete the current child.
reconcileChildFibers(workInProgress, current.child, null, renderLanes);
}
return fallbackChildFragment;
}
function mountDehydratedSuspenseComponent(
workInProgress: Fiber,
suspenseInstance: SuspenseInstance,
renderLanes: Lanes,
): null | Fiber {
// During the first pass, we'll bail out and not drill into the children.
// Instead, we'll leave the content in place and try to hydrate it later.
if (isSuspenseInstanceFallback(suspenseInstance)) {
// This is a client-only boundary. Since we won't get any content from the server
// for this, we need to schedule that at a higher priority based on when it would
// have timed out. In theory we could render it in this pass but it would have the
// wrong priority associated with it and will prevent hydration of parent path.
// Instead, we'll leave work left on it to render it in a separate commit.
// Schedule a normal pri update to render this content.
workInProgress.lanes = laneToLanes(
enableHydrationLaneScheduling ? DefaultLane : DefaultHydrationLane,
);
} else {
// We'll continue hydrating the rest at offscreen priority since we'll already
// be showing the right content coming from the server, it is no rush.
workInProgress.lanes = laneToLanes(OffscreenLane);
}
return null;
}
function updateDehydratedSuspenseComponent(
current: Fiber,
workInProgress: Fiber,
didSuspend: boolean,
didPrimaryChildrenDefer: boolean,
nextProps: SuspenseProps,
suspenseInstance: SuspenseInstance,
suspenseState: SuspenseState,
renderLanes: Lanes,
): 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.
warnIfHydrating();
if (includesSomeLane(renderLanes, (OffscreenLane: Lane))) {
// If we're rendering Offscreen and we're entering the activity then it's possible
// that the only reason we rendered was because this boundary left work. Provide
// it as a cause if another one doesn't already exist.
markRenderDerivedCause(workInProgress);
}
if (isSuspenseInstanceFallback(suspenseInstance)) {
// This boundary is in a permanent fallback state. In this case, we'll never
// get an update and we'll never be able to hydrate the final content. Let's just try the
// client side render instead.
let digest: ?string;
let message;
let stack = null;
let componentStack = null;
if (__DEV__) {
({digest, message, stack, componentStack} =
getSuspenseInstanceFallbackErrorDetails(suspenseInstance));
} else {
({digest} = getSuspenseInstanceFallbackErrorDetails(suspenseInstance));
}
// TODO: Figure out a better signal than encoding a magic digest value.
if (!enablePostpone || digest !== 'POSTPONE') {
let error: Error;
if (__DEV__ && message) {
// eslint-disable-next-line react-internal/prod-error-codes
error = new Error(message);
} else {
error = new Error(
'The server could not finish this Suspense boundary, likely ' +
'due to an error during server rendering. ' +
'Switched to client rendering.',
);
}
// Replace the stack with the server stack
error.stack = (__DEV__ && stack) || '';
(error: any).digest = digest;
const capturedValue = createCapturedValueFromError(
error,
componentStack === undefined ? null : componentStack,
);
queueHydrationError(capturedValue);
}
return retrySuspenseComponentWithoutHydrating(
current,
workInProgress,
renderLanes,
);
}
if (
// TODO: Factoring is a little weird, since we check this right below, too.
!didReceiveUpdate
) {
// We need to check if any children have context before we decide to bail
// out, so propagate the changes now.
lazilyPropagateParentContextChanges(current, workInProgress, renderLanes);
}
// We use lanes to indicate that a child might depend on context, so if
// any context has changed, we need to treat is as if the input might have changed.
const hasContextChanged = includesSomeLane(renderLanes, current.childLanes);
if (didReceiveUpdate || hasContextChanged) {
// This boundary has changed since the first render. This means that we are now unable to
// hydrate it. We might still be able to hydrate it using a higher priority lane.
const root = getWorkInProgressRoot();
if (root !== null) {
const attemptHydrationAtLane = getBumpedLaneForHydration(
root,
renderLanes,
);
if (
attemptHydrationAtLane !== NoLane &&
attemptHydrationAtLane !== suspenseState.retryLane
) {
// Intentionally mutating since this render will get interrupted. This
// is one of the very rare times where we mutate the current tree
// during the render phase.
suspenseState.retryLane = attemptHydrationAtLane;
enqueueConcurrentRenderForLane(current, attemptHydrationAtLane);
scheduleUpdateOnFiber(root, current, attemptHydrationAtLane);
// Throw a special object that signals to the work loop that it should
// interrupt the current render.
//
// Because we're inside a React-only execution stack, we don't
// strictly need to throw here — we could instead modify some internal
// work loop state. But using an exception means we don't need to
// check for this case on every iteration of the work loop. So doing
// it this way moves the check out of the fast path.
throw SelectiveHydrationException;
} else {
// We have already tried to ping at a higher priority than we're rendering with
// so if we got here, we must have failed to hydrate at those levels. We must
// now give up. Instead, we're going to delete the whole subtree and instead inject
// a new real Suspense boundary to take its place, which may render content
// or fallback. This might suspend for a while and if it does we might still have
// an opportunity to hydrate before this pass commits.
}
}
// If we did not selectively hydrate, we'll continue rendering without
// hydrating. Mark this tree as suspended to prevent it from committing
// outside a transition.
//
// This path should only happen if the hydration lane already suspended.
if (isSuspenseInstancePending(suspenseInstance)) {
// This is a dehydrated suspense instance. We don't need to suspend
// because we're already showing a fallback.
// TODO: The Fizz runtime might still stream in completed HTML, out-of-
// band. Should we fix this? There's a version of this bug that happens
// during client rendering, too. Needs more consideration.
} else {
renderDidSuspendDelayIfPossible();
}
return retrySuspenseComponentWithoutHydrating(
current,
workInProgress,
renderLanes,
);
} else if (isSuspenseInstancePending(suspenseInstance)) {
// This component is still pending more data from the server, so we can't hydrate its
// content. We treat it as if this component suspended itself. It might seem as if
// we could just try to render it client-side instead. However, this will perform a
// lot of unnecessary work and is unlikely to complete since it often will suspend
// on missing data anyway. Additionally, the server might be able to render more
// than we can on the client yet. In that case we'd end up with more fallback states
// on the client than if we just leave it alone. If the server times out or errors
// these should update this boundary to the permanent Fallback state instead.
// Mark it as having captured (i.e. suspended).
// Also Mark it as requiring retry.
workInProgress.flags |= DidCapture | Callback;
// Leave the child in place. I.e. the dehydrated fragment.
workInProgress.child = current.child;
return null;
} else {
// This is the first attempt.
reenterHydrationStateFromDehydratedSuspenseInstance(
workInProgress,
suspenseInstance,
suspenseState.treeContext,
);
const primaryChildren = nextProps.children;
const primaryChildFragment = mountSuspensePrimaryChildren(
workInProgress,
primaryChildren,
renderLanes,
);
// Mark the children as hydrating. This is a fast path to know whether this
// tree is part of a hydrating tree. This is used to determine if a child
// node has fully mounted yet, and for scheduling event replaying.
// Conceptually this is similar to Placement in that a new subtree is
// inserted into the React tree here. It just happens to not need DOM
// mutations because it already exists.
primaryChildFragment.flags |= Hydrating;
return primaryChildFragment;
}
} else {
// This is the second render pass. We already attempted to hydrated, but
// something either suspended or errored.
if (workInProgress.flags & ForceClientRender) {
// Something errored during hydration. Try again without hydrating.
// The error should've already been logged in throwException.
pushPrimaryTreeSuspenseHandler(workInProgress);
workInProgress.flags &= ~ForceClientRender;
return retrySuspenseComponentWithoutHydrating(
current,
workInProgress,
renderLanes,
);
} 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.
workInProgress.flags |= DidCapture;
return null;
} 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;
mountSuspenseFallbackAfterRetryWithoutHydrating(
current,
workInProgress,
nextPrimaryChildren,
nextFallbackChildren,
renderLanes,
);
const primaryChildFragment: Fiber = (workInProgress.child: any);
primaryChildFragment.memoizedState =
mountSuspenseOffscreenState(renderLanes);
primaryChildFragment.childLanes = getRemainingWorkInPrimaryTree(
current,
didPrimaryChildrenDefer,
renderLanes,
);
workInProgress.memoizedState = SUSPENDED_MARKER;
return bailoutOffscreenComponent(null, primaryChildFragment);
}
}
}
function scheduleSuspenseWorkOnFiber(
fiber: Fiber,
renderLanes: Lanes,
propagationRoot: Fiber,
) {
fiber.lanes = mergeLanes(fiber.lanes, renderLanes);
const alternate = fiber.alternate;
if (alternate !== null) {
alternate.lanes = mergeLanes(alternate.lanes, renderLanes);
}
scheduleContextWorkOnParentPath(fiber.return, renderLanes, propagationRoot);
}
function propagateSuspenseContextChange(
workInProgress: Fiber,
firstChild: null | Fiber,
renderLanes: Lanes,
): void {
// Mark any Suspense boundaries with fallbacks as having work to do.
// If they were previously forced into fallbacks, they may now be able
// to unblock.
let node = firstChild;
while (node !== null) {
if (node.tag === SuspenseComponent) {
const state: SuspenseState | null = node.memoizedState;
if (state !== null) {
scheduleSuspenseWorkOnFiber(node, renderLanes, workInProgress);
}
} else if (node.tag === SuspenseListComponent) {
// If the tail is hidden there might not be an Suspense boundaries
// to schedule work on. In this case we have to schedule it on the
// list itself.
// We don't have to traverse to the children of the list since
// the list will propagate the change when it rerenders.
scheduleSuspenseWorkOnFiber(node, renderLanes, workInProgress);
} else if (node.child !== null) {
node.child.return = node;
node = node.child;
continue;
}
if (node === workInProgress) {
return;
}
// $FlowFixMe[incompatible-use] found when upgrading Flow
while (node.sibling === null) {
// $FlowFixMe[incompatible-use] found when upgrading Flow
if (node.return === null || node.return === workInProgress) {
return;
}
node = node.return;
}
// $FlowFixMe[incompatible-use] found when upgrading Flow
node.sibling.return = node.return;
node = node.sibling;
}
}
function findLastContentRow(firstChild: null | Fiber): null | Fiber {
// This is going to find the last row among these children that is already
// showing content on the screen, as opposed to being in fallback state or
// new. If a row has multiple Suspense boundaries, any of them being in the
// fallback state, counts as the whole row being in a fallback state.
// Note that the "rows" will be workInProgress, but any nested children
// will still be current since we haven't rendered them yet. The mounted
// order may not be the same as the new order. We use the new order.
let row = firstChild;
let lastContentRow: null | Fiber = null;
while (row !== null) {
const currentRow = row.alternate;
// New rows can't be content rows.
if (currentRow !== null && findFirstSuspended(currentRow) === null) {
lastContentRow = row;
}
row = row.sibling;
}
return lastContentRow;
}
function validateRevealOrder(revealOrder: SuspenseListRevealOrder) {
if (__DEV__) {
const cacheKey = revealOrder == null ? 'null' : revealOrder;
if (
revealOrder !== 'forwards' &&
revealOrder !== 'unstable_legacy-backwards' &&
revealOrder !== 'together' &&
revealOrder !== 'independent' &&
!didWarnAboutRevealOrder[cacheKey]
) {
didWarnAboutRevealOrder[cacheKey] = true;
if (revealOrder == null) {
console.error(
'The default for the <SuspenseList revealOrder="..."> prop is changing. ' +
'To be future compatible you must explictly specify either ' +
'"independent" (the current default), "together", "forwards" or "legacy_unstable-backwards".',
);
} else if (revealOrder === 'backwards') {
console.error(
'The rendering order of <SuspenseList revealOrder="backwards"> is changing. ' +
'To be future compatible you must specify revealOrder="legacy_unstable-backwards" instead.',
);
} else if (typeof revealOrder === 'string') {
switch (revealOrder.toLowerCase()) {
case 'together':
case 'forwards':
case 'backwards':
case 'independent': {
console.error(
'"%s" is not a valid value for revealOrder on <SuspenseList />. ' +
'Use lowercase "%s" instead.',
revealOrder,
revealOrder.toLowerCase(),
);
break;
}
case 'forward':
case 'backward': {
console.error(
'"%s" is not a valid value for revealOrder on <SuspenseList />. ' +
'React uses the -s suffix in the spelling. Use "%ss" instead.',
revealOrder,
revealOrder.toLowerCase(),
);
break;
}
default:
console.error(
'"%s" is not a supported revealOrder on <SuspenseList />. ' +
'Did you mean "independent", "together", "forwards" or "backwards"?',
revealOrder,
);
break;
}
} else {
console.error(
'%s is not a supported value for revealOrder on <SuspenseList />. ' +
'Did you mean "independent", "together", "forwards" or "backwards"?',
revealOrder,
);
}
}
}
}
function validateTailOptions(
tailMode: SuspenseListTailMode,
revealOrder: SuspenseListRevealOrder,
) {
if (__DEV__) {
const cacheKey = tailMode == null ? 'null' : tailMode;
if (!didWarnAboutTailOptions[cacheKey]) {
if (tailMode == null) {
if (
revealOrder === 'forwards' ||
revealOrder === 'backwards' ||
revealOrder === 'unstable_legacy-backwards'
) {
didWarnAboutTailOptions[cacheKey] = true;
console.error(
'The default for the <SuspenseList tail="..."> prop is changing. ' +
'To be future compatible you must explictly specify either ' +
'"visible" (the current default), "collapsed" or "hidden".',
);
}
} else if (
tailMode !== 'visible' &&
tailMode !== 'collapsed' &&
tailMode !== 'hidden'
) {
didWarnAboutTailOptions[cacheKey] = true;
console.error(
'"%s" is not a supported value for tail on <SuspenseList />. ' +
'Did you mean "visible", "collapsed" or "hidden"?',
tailMode,
);
} else if (
revealOrder !== 'forwards' &&
revealOrder !== 'backwards' &&
revealOrder !== 'unstable_legacy-backwards'
) {
didWarnAboutTailOptions[cacheKey] = true;
console.error(
'<SuspenseList tail="%s" /> is only valid if revealOrder is ' +
'"forwards" or "backwards". ' +
'Did you mean to specify revealOrder="forwards"?',
tailMode,
);
}
}
}
}
function initSuspenseListRenderState(
workInProgress: Fiber,
isBackwards: boolean,
tail: null | Fiber,
lastContentRow: null | Fiber,
tailMode: SuspenseListTailMode,
treeForkCount: number,
): void {
const renderState: null | SuspenseListRenderState =
workInProgress.memoizedState;
if (renderState === null) {
workInProgress.memoizedState = ({
isBackwards: isBackwards,
rendering: null,
renderingStartTime: 0,
last: lastContentRow,
tail: tail,
tailMode: tailMode,
treeForkCount: treeForkCount,
}: SuspenseListRenderState);
} else {
// We can reuse the existing object from previous renders.
renderState.isBackwards = isBackwards;
renderState.rendering = null;
renderState.renderingStartTime = 0;
renderState.last = lastContentRow;
renderState.tail = tail;
renderState.tailMode = tailMode;
renderState.treeForkCount = treeForkCount;
}
}
// This can end up rendering this component multiple passes.
// The first pass splits the children fibers into two sets. A head and tail.
// We first render the head. If anything is in fallback state, we do another
// pass through beginWork to rerender all children (including the tail) with
// the force suspend context. If the first render didn't have anything in
// in fallback state. Then we render each row in the tail one-by-one.
// That happens in the completeWork phase without going back to beginWork.
function updateSuspenseListComponent(
current: Fiber | null,
workInProgress: Fiber,
renderLanes: Lanes,
) {
const nextProps: SuspenseListProps = workInProgress.pendingProps;
const revealOrder: SuspenseListRevealOrder = nextProps.revealOrder;
const tailMode: SuspenseListTailMode = nextProps.tail;
const newChildren = nextProps.children;
let suspenseContext: SuspenseContext = suspenseStackCursor.current;
const shouldForceFallback = hasSuspenseListContext(
suspenseContext,
(ForceSuspenseFallback: SuspenseContext),
);
if (shouldForceFallback) {
suspenseContext = setShallowSuspenseListContext(
suspenseContext,
ForceSuspenseFallback,
);
workInProgress.flags |= DidCapture;
} else {
suspenseContext = setDefaultShallowSuspenseListContext(suspenseContext);
}
pushSuspenseListContext(workInProgress, suspenseContext);
validateRevealOrder(revealOrder);
validateTailOptions(tailMode, revealOrder);
validateSuspenseListChildren(newChildren, revealOrder);
reconcileChildren(current, workInProgress, newChildren, renderLanes);
// Read how many children forks this set pushed so we can push it every time we retry.
const treeForkCount = getIsHydrating() ? getForksAtLevel(workInProgress) : 0;
if (!shouldForceFallback) {
const didSuspendBefore =
current !== null && (current.flags & DidCapture) !== NoFlags;
if (didSuspendBefore) {
// If we previously forced a fallback, we need to schedule work
// on any nested boundaries to let them know to try to render
// again. This is the same as context updating.
propagateSuspenseContextChange(
workInProgress,
workInProgress.child,
renderLanes,
);
}
}
if (!disableLegacyMode && (workInProgress.mode & ConcurrentMode) === NoMode) {
// In legacy mode, SuspenseList doesn't work so we just
// use make it a noop by treating it as the default revealOrder.
workInProgress.memoizedState = null;
} else {
switch (revealOrder) {
case 'forwards': {
const lastContentRow = findLastContentRow(workInProgress.child);
let tail;
if (lastContentRow === null) {
// The whole list is part of the tail.
// TODO: We could fast path by just rendering the tail now.
tail = workInProgress.child;
workInProgress.child = null;
} else {
// Disconnect the tail rows after the content row.
// We're going to render them separately later.
tail = lastContentRow.sibling;
lastContentRow.sibling = null;
}
initSuspenseListRenderState(
workInProgress,
false, // isBackwards
tail,
lastContentRow,
tailMode,
treeForkCount,
);
break;
}
case 'backwards':
case 'unstable_legacy-backwards': {
// We're going to find the first row that has existing content.
// At the same time we're going to reverse the list of everything
// we pass in the meantime. That's going to be our tail in reverse
// order.
let tail = null;
let row = workInProgress.child;
workInProgress.child = null;
while (row !== null) {
const currentRow = row.alternate;
// New rows can't be content rows.
if (currentRow !== null && findFirstSuspended(currentRow) === null) {
// This is the beginning of the main content.
workInProgress.child = row;
break;
}
const nextRow = row.sibling;
row.sibling = tail;
tail = row;
row = nextRow;
}
// TODO: If workInProgress.child is null, we can continue on the tail immediately.
initSuspenseListRenderState(
workInProgress,
true, // isBackwards
tail,
null, // last
tailMode,
treeForkCount,
);
break;
}
case 'together': {
initSuspenseListRenderState(
workInProgress,
false, // isBackwards
null, // tail
null, // last
undefined,
treeForkCount,
);
break;
}
default: {
// The default reveal order is the same as not having
// a boundary.
workInProgress.memoizedState = null;
}
}
}
return workInProgress.child;
}
function updateViewTransition(
current: Fiber | null,
workInProgress: Fiber,
renderLanes: Lanes,
) {
const pendingProps: ViewTransitionProps = workInProgress.pendingProps;
if (pendingProps.name != null && pendingProps.name !== 'auto') {
// Explicitly named boundary. We track it so that we can pair it up with another explicit
// boundary if we get deleted.
workInProgress.flags |=
current === null
? ViewTransitionNamedMount | ViewTransitionNamedStatic
: ViewTransitionNamedStatic;
} else {
// The server may have used useId to auto-assign a generated name for this boundary.
// We push a materialization to ensure child ids line up with the server.
if (getIsHydrating()) {
pushMaterializedTreeId(workInProgress);
}
}
if (__DEV__) {
// $FlowFixMe[prop-missing]
if (pendingProps.className !== undefined) {
const example =
typeof pendingProps.className === 'string'
? JSON.stringify(pendingProps.className)
: '{...}';
if (!didWarnAboutClassNameOnViewTransition[example]) {
didWarnAboutClassNameOnViewTransition[example] = true;
console.error(
'<ViewTransition> doesn\'t accept a "className" prop. It has been renamed to "default".\n' +
'- <ViewTransition className=%s>\n' +
'+ <ViewTransition default=%s>',
example,
example,
);
}
}
}
if (current !== null && current.memoizedProps.name !== pendingProps.name) {
// If the name changes, we schedule a ref effect to create a new ref instance.
workInProgress.flags |= Ref | RefStatic;
} else {
markRef(current, workInProgress);
}
const nextChildren = pendingProps.children;
reconcileChildren(current, workInProgress, nextChildren, renderLanes);
return workInProgress.child;
}
function updatePortalComponent(
current: Fiber | null,
workInProgress: Fiber,
renderLanes: Lanes,
) {
pushHostContainer(workInProgress, workInProgress.stateNode.containerInfo);
const nextChildren = workInProgress.pendingProps;
if (current === null) {
// Portals are special because we don't append the children during mount
// but at commit. Therefore we need to track insertions which the normal
// flow doesn't do during mount. This doesn't happen at the root because
// the root always starts with a "current" with a null child.
// TODO: Consider unifying this with how the root works.
workInProgress.child = reconcileChildFibers(
workInProgress,
null,
nextChildren,
renderLanes,
);
} else {
reconcileChildren(current, workInProgress, nextChildren, renderLanes);
}
return workInProgress.child;
}
let hasWarnedAboutUsingNoValuePropOnContextProvider = false;
function updateContextProvider(
current: Fiber | null,
workInProgress: Fiber,
renderLanes: Lanes,
) {
const context: ReactContext<any> = workInProgress.type;
const newProps = workInProgress.pendingProps;
const newValue = newProps.value;
if (__DEV__) {
if (!('value' in newProps)) {
if (!hasWarnedAboutUsingNoValuePropOnContextProvider) {
hasWarnedAboutUsingNoValuePropOnContextProvider = true;
console.error(
'The `value` prop is required for the `<Context.Provider>`. Did you misspell it or forget to pass it?',
);
}
}
}
pushProvider(workInProgress, context, newValue);
const newChildren = newProps.children;
reconcileChildren(current, workInProgress, newChildren, renderLanes);
return workInProgress.child;
}
function updateContextConsumer(
current: Fiber | null,
workInProgress: Fiber,
renderLanes: Lanes,
) {
const consumerType: ReactConsumerType<any> = workInProgress.type;
const context: ReactContext<any> = consumerType._context;
const newProps = workInProgress.pendingProps;
const render = newProps.children;
if (__DEV__) {
if (typeof render !== 'function') {
console.error(
'A context consumer was rendered with multiple children, or a child ' +
"that isn't a function. A context consumer expects a single child " +
'that is a function. If you did pass a function, make sure there ' +
'is no trailing or leading whitespace around it.',
);
}
}
prepareToReadContext(workInProgress, renderLanes);
const newValue = readContext(context);
if (enableSchedulingProfiler) {
markComponentRenderStarted(workInProgress);
}
let newChildren;
if (__DEV__) {
newChildren = callComponentInDEV(render, newValue, undefined);
} else {
newChildren = render(newValue);
}
if (enableSchedulingProfiler) {
markComponentRenderStopped();
}
// React DevTools reads this flag.
workInProgress.flags |= PerformedWork;
reconcileChildren(current, workInProgress, newChildren, renderLanes);
return workInProgress.child;
}
function updateScopeComponent(
current: null | Fiber,
workInProgress: Fiber,
renderLanes: Lanes,
) {
const nextProps = workInProgress.pendingProps;
const nextChildren = nextProps.children;
markRef(current, workInProgress);
reconcileChildren(current, workInProgress, nextChildren, renderLanes);
return workInProgress.child;
}
export function markWorkInProgressReceivedUpdate() {
didReceiveUpdate = true;
}
export function checkIfWorkInProgressReceivedUpdate(): boolean {
return didReceiveUpdate;
}
function resetSuspendedCurrentOnMountInLegacyMode(
current: null | Fiber,
workInProgress: Fiber,
) {
if (!disableLegacyMode && (workInProgress.mode & ConcurrentMode) === NoMode) {
if (current !== null) {
// A lazy component only mounts if it suspended inside a non-
// concurrent tree, in an inconsistent state. We want to treat it like
// a new mount, even though an empty version of it already committed.
// Disconnect the alternate pointers.
current.alternate = null;
workInProgress.alternate = null;
// Since this is conceptually a new fiber, schedule a Placement effect
workInProgress.flags |= Placement;
}
}
}
function bailoutOnAlreadyFinishedWork(
current: Fiber | null,
workInProgress: Fiber,
renderLanes: Lanes,
): Fiber | null {
if (current !== null) {
// Reuse previous dependencies
workInProgress.dependencies = current.dependencies;
}
if (enableProfilerTimer) {
// Don't update "base" render times for bailouts.
stopProfilerTimerIfRunning(workInProgress);
}
markSkippedUpdateLanes(workInProgress.lanes);
// Check if the children have any pending work.
if (!includesSomeLane(renderLanes, workInProgress.childLanes)) {
// The children don't have any work either. We can skip them.
// TODO: Once we add back resuming, we should check if the children are
// a work-in-progress set. If so, we need to transfer their effects.
if (current !== null) {
// Before bailing out, check if there are any context changes in
// the children.
lazilyPropagateParentContextChanges(current, workInProgress, renderLanes);
if (!includesSomeLane(renderLanes, workInProgress.childLanes)) {
return null;
}
} else {
return null;
}
}
// This fiber doesn't have work, but its subtree does. Clone the child
// fibers and continue.
cloneChildFibers(current, workInProgress);
return workInProgress.child;
}
function remountFiber(
current: Fiber,
oldWorkInProgress: Fiber,
newWorkInProgress: Fiber,
): Fiber | null {
if (__DEV__) {
const returnFiber = oldWorkInProgress.return;
if (returnFiber === null) {
// eslint-disable-next-line react-internal/prod-error-codes
throw new Error('Cannot swap the root fiber.');
}
// Disconnect from the old current.
// It will get deleted.
current.alternate = null;
oldWorkInProgress.alternate = null;
// Connect to the new tree.
newWorkInProgress.index = oldWorkInProgress.index;
newWorkInProgress.sibling = oldWorkInProgress.sibling;
newWorkInProgress.return = oldWorkInProgress.return;
newWorkInProgress.ref = oldWorkInProgress.ref;
if (__DEV__) {
newWorkInProgress._debugInfo = oldWorkInProgress._debugInfo;
}
// Replace the child/sibling pointers above it.
if (oldWorkInProgress === returnFiber.child) {
returnFiber.child = newWorkInProgress;
} else {
let prevSibling = returnFiber.child;
if (prevSibling === null) {
// eslint-disable-next-line react-internal/prod-error-codes
throw new Error('Expected parent to have a child.');
}
// $FlowFixMe[incompatible-use] found when upgrading Flow
while (prevSibling.sibling !== oldWorkInProgress) {
// $FlowFixMe[incompatible-use] found when upgrading Flow
prevSibling = prevSibling.sibling;
if (prevSibling === null) {
// eslint-disable-next-line react-internal/prod-error-codes
throw new Error('Expected to find the previous sibling.');
}
}
// $FlowFixMe[incompatible-use] found when upgrading Flow
prevSibling.sibling = newWorkInProgress;
}
// Delete the old fiber and place the new one.
// Since the old fiber is disconnected, we have to schedule it manually.
const deletions = returnFiber.deletions;
if (deletions === null) {
returnFiber.deletions = [current];
returnFiber.flags |= ChildDeletion;
} else {
deletions.push(current);
}
newWorkInProgress.flags |= Placement;
// Restart work from the new fiber.
return newWorkInProgress;
} else {
throw new Error(
'Did not expect this call in production. ' +
'This is a bug in React. Please file an issue.',
);
}
}
function checkScheduledUpdateOrContext(
current: Fiber,
renderLanes: Lanes,
): boolean {
// Before performing an early bailout, we must check if there are pending
// updates or context.
const updateLanes = current.lanes;
if (includesSomeLane(updateLanes, renderLanes)) {
return true;
}
// No pending update, but because context is propagated lazily, we need
// to check for a context change before we bail out.
const dependencies = current.dependencies;
if (dependencies !== null && checkIfContextChanged(dependencies)) {
return true;
}
return false;
}
function attemptEarlyBailoutIfNoScheduledUpdate(
current: Fiber,
workInProgress: Fiber,
renderLanes: Lanes,
) {
// This fiber does not have any pending work. Bailout without entering
// the begin phase. There's still some bookkeeping we that needs to be done
// in this optimized path, mostly pushing stuff onto the stack.
switch (workInProgress.tag) {
case HostRoot: {
pushHostRootContext(workInProgress);
const root: FiberRoot = workInProgress.stateNode;
pushRootTransition(workInProgress, root, renderLanes);
if (enableTransitionTracing) {
pushRootMarkerInstance(workInProgress);
}
const cache: Cache = current.memoizedState.cache;
pushCacheProvider(workInProgress, cache);
resetHydrationState();
break;
}
case HostSingleton:
case HostComponent:
pushHostContext(workInProgress);
break;
case ClassComponent: {
const Component = workInProgress.type;
if (isLegacyContextProvider(Component)) {
pushLegacyContextProvider(workInProgress);
}
break;
}
case HostPortal:
pushHostContainer(workInProgress, workInProgress.stateNode.containerInfo);
break;
case ContextProvider: {
const newValue = workInProgress.memoizedProps.value;
const context: ReactContext<any> = workInProgress.type;
pushProvider(workInProgress, context, newValue);
break;
}
case Profiler:
if (enableProfilerTimer) {
// Profiler should only call onRender when one of its descendants actually rendered.
const hasChildWork = includesSomeLane(
renderLanes,
workInProgress.childLanes,
);
if (hasChildWork) {
workInProgress.flags |= Update;
}
if (enableProfilerCommitHooks) {
// Schedule a passive effect for this Profiler to call onPostCommit hooks.
// This effect should be scheduled even if there is no onPostCommit callback for this Profiler,
// because the effect is also where times bubble to parent Profilers.
workInProgress.flags |= Passive;
// Reset effect durations for the next eventual effect phase.
// These are reset during render to allow the DevTools commit hook a chance to read them,
const stateNode = workInProgress.stateNode;
stateNode.effectDuration = -0;
stateNode.passiveEffectDuration = -0;
}
}
break;
case ActivityComponent: {
const state: ActivityState | null = workInProgress.memoizedState;
if (state !== null) {
// We're dehydrated so we're not going to render the children. This is just
// to maintain push/pop symmetry.
// We know that this component will suspend again because if it has
// been unsuspended it has committed as a hydrated Activity component.
// If it needs to be retried, it should have work scheduled on it.
workInProgress.flags |= DidCapture;
pushDehydratedActivitySuspenseHandler(workInProgress);
return null;
}
break;
}
case SuspenseComponent: {
const state: SuspenseState | null = workInProgress.memoizedState;
if (state !== null) {
if (state.dehydrated !== null) {
// 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.
workInProgress.flags |= DidCapture;
// We should never render the children of a dehydrated boundary until we
// upgrade it. We return null instead of bailoutOnAlreadyFinishedWork.
return null;
}
// If this boundary is currently timed out, we need to decide
// whether to retry the primary children, or to skip over it and
// go straight to the fallback. Check the priority of the primary
// child fragment.
const primaryChildFragment: Fiber = (workInProgress.child: any);
const primaryChildLanes = primaryChildFragment.childLanes;
if (includesSomeLane(renderLanes, primaryChildLanes)) {
// The primary children have pending work. Use the normal path
// to attempt to render the primary children again.
return updateSuspenseComponent(current, workInProgress, renderLanes);
} else {
// The primary child fragment does not have pending work marked
// on it
pushPrimaryTreeSuspenseHandler(workInProgress);
// The primary children do not have pending work with sufficient
// priority. Bailout.
const child = bailoutOnAlreadyFinishedWork(
current,
workInProgress,
renderLanes,
);
if (child !== null) {
// The fallback children have pending work. Skip over the
// primary children and work on the fallback.
return child.sibling;
} else {
// Note: We can return `null` here because we already checked
// whether there were nested context consumers, via the call to
// `bailoutOnAlreadyFinishedWork` above.
return null;
}
}
} else {
pushPrimaryTreeSuspenseHandler(workInProgress);
}
break;
}
case SuspenseListComponent: {
const didSuspendBefore = (current.flags & DidCapture) !== NoFlags;
let hasChildWork = includesSomeLane(
renderLanes,
workInProgress.childLanes,
);
if (!hasChildWork) {
// Context changes may not have been propagated yet. We need to do
// that now, before we can decide whether to bail out.
// TODO: We use `childLanes` as a heuristic for whether there is
// remaining work in a few places, including
// `bailoutOnAlreadyFinishedWork` and
// `updateDehydratedSuspenseComponent`. We should maybe extract this
// into a dedicated function.
lazilyPropagateParentContextChanges(
current,
workInProgress,
renderLanes,
);
hasChildWork = includesSomeLane(renderLanes, workInProgress.childLanes);
}
if (didSuspendBefore) {
if (hasChildWork) {
// If something was in fallback state last time, and we have all the
// same children then we're still in progressive loading state.
// Something might get unblocked by state updates or retries in the
// tree which will affect the tail. So we need to use the normal
// path to compute the correct tail.
return updateSuspenseListComponent(
current,
workInProgress,
renderLanes,
);
}
// If none of the children had any work, that means that none of
// them got retried so they'll still be blocked in the same way
// as before. We can fast bail out.
workInProgress.flags |= DidCapture;
}
// If nothing suspended before and we're rendering the same children,
// then the tail doesn't matter. Anything new that suspends will work
// in the "together" mode, so we can continue from the state we had.
const renderState = workInProgress.memoizedState;
if (renderState !== null) {
// Reset to the "together" mode in case we've started a different
// update in the past but didn't complete it.
renderState.rendering = null;
renderState.tail = null;
renderState.lastEffect = null;
}
pushSuspenseListContext(workInProgress, suspenseStackCursor.current);
if (hasChildWork) {
break;
} else {
// If none of the children had any work, that means that none of
// them got retried so they'll still be blocked in the same way
// as before. We can fast bail out.
return null;
}
}
case OffscreenComponent: {
// Need to check if the tree still needs to be deferred. This is
// almost identical to the logic used in the normal update path,
// so we'll just enter that. The only difference is we'll bail out
// at the next level instead of this one, because the child props
// have not changed. Which is fine.
// TODO: Probably should refactor `beginWork` to split the bailout
// path from the normal path. I'm tempted to do a labeled break here
// but I won't :)
workInProgress.lanes = NoLanes;
return updateOffscreenComponent(
current,
workInProgress,
renderLanes,
workInProgress.pendingProps,
);
}
case CacheComponent: {
const cache: Cache = current.memoizedState.cache;
pushCacheProvider(workInProgress, cache);
break;
}
case TracingMarkerComponent: {
if (enableTransitionTracing) {
const instance: TracingMarkerInstance | null = workInProgress.stateNode;
if (instance !== null) {
pushMarkerInstance(workInProgress, instance);
}
break;
}
// Fallthrough
}
case LegacyHiddenComponent: {
if (enableLegacyHidden) {
workInProgress.lanes = NoLanes;
return updateLegacyHiddenComponent(
current,
workInProgress,
renderLanes,
);
}
// Fallthrough
}
}
return bailoutOnAlreadyFinishedWork(current, workInProgress, renderLanes);
}
function beginWork(
current: Fiber | null,
workInProgress: Fiber,
renderLanes: Lanes,
): Fiber | null {
if (__DEV__) {
if (workInProgress._debugNeedsRemount && current !== null) {
// This will restart the begin phase with a new fiber.
const copiedFiber = createFiberFromTypeAndProps(
workInProgress.type,
workInProgress.key,
workInProgress.pendingProps,
workInProgress._debugOwner || null,
workInProgress.mode,
workInProgress.lanes,
);
copiedFiber._debugStack = workInProgress._debugStack;
copiedFiber._debugTask = workInProgress._debugTask;
return remountFiber(current, workInProgress, copiedFiber);
}
}
if (current !== null) {
const oldProps = current.memoizedProps;
const newProps = workInProgress.pendingProps;
if (
oldProps !== newProps ||
hasLegacyContextChanged() ||
// Force a re-render if the implementation changed due to hot reload:
(__DEV__ ? workInProgress.type !== current.type : false)
) {
// If props or context changed, mark the fiber as having performed work.
// This may be unset if the props are determined to be equal later (memo).
didReceiveUpdate = true;
} else {
// Neither props nor legacy context changes. Check if there's a pending
// update or context change.
const hasScheduledUpdateOrContext = checkScheduledUpdateOrContext(
current,
renderLanes,
);
if (
!hasScheduledUpdateOrContext &&
// If this is the second pass of an error or suspense boundary, there
// may not be work scheduled on `current`, so we check for this flag.
(workInProgress.flags & DidCapture) === NoFlags
) {
// No pending updates or context. Bail out now.
didReceiveUpdate = false;
return attemptEarlyBailoutIfNoScheduledUpdate(
current,
workInProgress,
renderLanes,
);
}
if ((current.flags & ForceUpdateForLegacySuspense) !== NoFlags) {
// This is a special case that only exists for legacy mode.
// See https://github.com/facebook/react/pull/19216.
didReceiveUpdate = true;
} else {
// An update was scheduled on this fiber, but there are no new props
// nor legacy context. Set this to false. If an update queue or context
// consumer produces a changed value, it will set this to true. Otherwise,
// the component will assume the children have not changed and bail out.
didReceiveUpdate = false;
}
}
} else {
didReceiveUpdate = false;
if (getIsHydrating() && isForkedChild(workInProgress)) {
// Check if this child belongs to a list of muliple children in
// its parent.
//
// In a true multi-threaded implementation, we would render children on
// parallel threads. This would represent the beginning of a new render
// thread for this subtree.
//
// We only use this for id generation during hydration, which is why the
// logic is located in this special branch.
const slotIndex = workInProgress.index;
const numberOfForks = getForksAtLevel(workInProgress);
pushTreeId(workInProgress, numberOfForks, slotIndex);
}
}
// Before entering the begin phase, clear pending update priority.
// TODO: This assumes that we're about to evaluate the component and process
// the update queue. However, there's an exception: SimpleMemoComponent
// sometimes bails out later in the begin phase. This indicates that we should
// move this assignment out of the common path and into each branch.
workInProgress.lanes = NoLanes;
switch (workInProgress.tag) {
case LazyComponent: {
const elementType = workInProgress.elementType;
return mountLazyComponent(
current,
workInProgress,
elementType,
renderLanes,
);
}
case FunctionComponent: {
const Component = workInProgress.type;
return updateFunctionComponent(
current,
workInProgress,
Component,
workInProgress.pendingProps,
renderLanes,
);
}
case ClassComponent: {
const Component = workInProgress.type;
const unresolvedProps = workInProgress.pendingProps;
const resolvedProps = resolveClassComponentProps(
Component,
unresolvedProps,
);
return updateClassComponent(
current,
workInProgress,
Component,
resolvedProps,
renderLanes,
);
}
case HostRoot:
return updateHostRoot(current, workInProgress, renderLanes);
case HostHoistable:
if (supportsResources) {
return updateHostHoistable(current, workInProgress, renderLanes);
}
// Fall through
case HostSingleton:
if (supportsSingletons) {
return updateHostSingleton(current, workInProgress, renderLanes);
}
// Fall through
case HostComponent:
return updateHostComponent(current, workInProgress, renderLanes);
case HostText:
return updateHostText(current, workInProgress);
case SuspenseComponent:
return updateSuspenseComponent(current, workInProgress, renderLanes);
case HostPortal:
return updatePortalComponent(current, workInProgress, renderLanes);
case ForwardRef: {
return updateForwardRef(
current,
workInProgress,
workInProgress.type,
workInProgress.pendingProps,
renderLanes,
);
}
case Fragment:
return updateFragment(current, workInProgress, renderLanes);
case Mode:
return updateMode(current, workInProgress, renderLanes);
case Profiler:
return updateProfiler(current, workInProgress, renderLanes);
case ContextProvider:
return updateContextProvider(current, workInProgress, renderLanes);
case ContextConsumer:
return updateContextConsumer(current, workInProgress, renderLanes);
case MemoComponent: {
return updateMemoComponent(
current,
workInProgress,
workInProgress.type,
workInProgress.pendingProps,
renderLanes,
);
}
case SimpleMemoComponent: {
return updateSimpleMemoComponent(
current,
workInProgress,
workInProgress.type,
workInProgress.pendingProps,
renderLanes,
);
}
case IncompleteClassComponent: {
if (disableLegacyMode) {
break;
}
const Component = workInProgress.type;
const unresolvedProps = workInProgress.pendingProps;
const resolvedProps = resolveClassComponentProps(
Component,
unresolvedProps,
);
return mountIncompleteClassComponent(
current,
workInProgress,
Component,
resolvedProps,
renderLanes,
);
}
case IncompleteFunctionComponent: {
if (disableLegacyMode) {
break;
}
const Component = workInProgress.type;
const unresolvedProps = workInProgress.pendingProps;
const resolvedProps = resolveClassComponentProps(
Component,
unresolvedProps,
);
return mountIncompleteFunctionComponent(
current,
workInProgress,
Component,
resolvedProps,
renderLanes,
);
}
case SuspenseListComponent: {
return updateSuspenseListComponent(current, workInProgress, renderLanes);
}
case ScopeComponent: {
if (enableScopeAPI) {
return updateScopeComponent(current, workInProgress, renderLanes);
}
break;
}
case ActivityComponent: {
return updateActivityComponent(current, workInProgress, renderLanes);
}
case OffscreenComponent: {
return updateOffscreenComponent(
current,
workInProgress,
renderLanes,
workInProgress.pendingProps,
);
}
case LegacyHiddenComponent: {
if (enableLegacyHidden) {
return updateLegacyHiddenComponent(
current,
workInProgress,
renderLanes,
);
}
break;
}
case CacheComponent: {
return updateCacheComponent(current, workInProgress, renderLanes);
}
case TracingMarkerComponent: {
if (enableTransitionTracing) {
return updateTracingMarkerComponent(
current,
workInProgress,
renderLanes,
);
}
break;
}
case ViewTransitionComponent: {
if (enableViewTransition) {
return updateViewTransition(current, workInProgress, renderLanes);
}
break;
}
case Throw: {
// This represents a Component that threw in the reconciliation phase.
// So we'll rethrow here. This might be a Thenable.
throw workInProgress.pendingProps;
}
}
throw new Error(
`Unknown unit of work tag (${workInProgress.tag}). This error is likely caused by a bug in ` +
'React. Please file an issue.',
);
}
export {beginWork};