mirror of
https://github.com/zebrajr/react.git
synced 2025-12-06 12:20:20 +01:00
This is a partial revert of #33094. It's true that we don't need the server and client ViewTransition names to line up. However the server does need to be able to generate deterministic names for itself. The cheapest way to do that is using the useId algorithm. When it's used by the server, the client needs to also materialize an ID even if it doesn't use it.
4424 lines
148 KiB
JavaScript
4424 lines
148 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,
|
|
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,
|
|
SuspenseListTailMode,
|
|
} 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,
|
|
enableRenderableContext,
|
|
disableLegacyMode,
|
|
disableDefaultPropsExceptForClasses,
|
|
enableHydrationLaneScheduling,
|
|
enableViewTransition,
|
|
enableFragmentRefs,
|
|
} from 'shared/ReactFeatureFlags';
|
|
import isArray from 'shared/isArray';
|
|
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,
|
|
getIteratorFn,
|
|
} from 'shared/ReactSymbols';
|
|
import {setCurrentFiber} from './ReactCurrentFiber';
|
|
import {
|
|
resolveFunctionForHotReloading,
|
|
resolveForwardRefForHotReloading,
|
|
resolveClassForHotReloading,
|
|
} from './ReactFiberHotReloading';
|
|
|
|
import {
|
|
mountChildFibers,
|
|
reconcileChildFibers,
|
|
cloneChildFibers,
|
|
} from './ReactChildFiber';
|
|
import {
|
|
processUpdateQueue,
|
|
cloneUpdateQueue,
|
|
initializeUpdateQueue,
|
|
enqueueCapturedUpdate,
|
|
suspendIfUpdateReadFromEntangledAsyncAction,
|
|
} from './ReactFiberClassUpdateQueue';
|
|
import {
|
|
NoLane,
|
|
NoLanes,
|
|
OffscreenLane,
|
|
DefaultLane,
|
|
DefaultHydrationLane,
|
|
SomeRetryLane,
|
|
includesSomeLane,
|
|
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 './ReactFiberContext';
|
|
import {
|
|
getIsHydrating,
|
|
enterHydrationState,
|
|
reenterHydrationStateFromDehydratedActivityInstance,
|
|
reenterHydrationStateFromDehydratedSuspenseInstance,
|
|
resetHydrationState,
|
|
claimHydratableSingleton,
|
|
tryToClaimNextHydratableInstance,
|
|
tryToClaimNextHydratableTextInstance,
|
|
claimNextHydratableActivityInstance,
|
|
claimNextHydratableSuspenseInstance,
|
|
warnIfHydrating,
|
|
queueHydrationError,
|
|
} from './ReactFiberHydrationContext';
|
|
import {
|
|
constructClassInstance,
|
|
mountClassInstance,
|
|
resumeMountClassInstance,
|
|
updateClassInstance,
|
|
resolveClassComponentProps,
|
|
} from './ReactFiberClassComponent';
|
|
import {resolveDefaultPropsOnNonClassComponent} from './ReactFiberLazyComponent';
|
|
import {
|
|
createFiberFromTypeAndProps,
|
|
createFiberFromFragment,
|
|
createFiberFromOffscreen,
|
|
createWorkInProgress,
|
|
isSimpleFunctionComponent,
|
|
isFunctionClassComponent,
|
|
} from './ReactFiber';
|
|
import {
|
|
scheduleUpdateOnFiber,
|
|
renderDidSuspendDelayIfPossible,
|
|
markSkippedUpdateLanes,
|
|
getWorkInProgressRoot,
|
|
peekDeferredLane,
|
|
} from './ReactFiberWorkLoop';
|
|
import {enqueueConcurrentRenderForLane} from './ReactFiberConcurrentUpdates';
|
|
import {pushCacheProvider, CacheContext} from './ReactFiberCacheComponent';
|
|
import {
|
|
createCapturedValueFromError,
|
|
createCapturedValueAtFiber,
|
|
} from './ReactCapturedValue';
|
|
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 {
|
|
callLazyInitInDEV,
|
|
callComponentInDEV,
|
|
callRenderInDEV,
|
|
} from './ReactFiberCallUserSpace';
|
|
|
|
// 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 didWarnAboutDefaultPropsOnFunctionComponent;
|
|
let didWarnAboutClassNameOnViewTransition;
|
|
|
|
if (__DEV__) {
|
|
didWarnAboutBadClass = ({}: {[string]: boolean});
|
|
didWarnAboutContextTypeOnFunctionComponent = ({}: {[string]: boolean});
|
|
didWarnAboutContextTypes = ({}: {[string]: boolean});
|
|
didWarnAboutGetDerivedStateOnFunctionComponent = ({}: {[string]: boolean});
|
|
didWarnAboutReassigningProps = false;
|
|
didWarnAboutRevealOrder = ({}: {[empty]: boolean});
|
|
didWarnAboutTailOptions = ({}: {[string]: boolean});
|
|
didWarnAboutDefaultPropsOnFunctionComponent = ({}: {[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 &&
|
|
// SimpleMemoComponent codepath doesn't resolve outer props either.
|
|
(disableDefaultPropsExceptForClasses ||
|
|
Component.defaultProps === undefined)
|
|
) {
|
|
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,
|
|
);
|
|
}
|
|
if (!disableDefaultPropsExceptForClasses) {
|
|
if (__DEV__) {
|
|
if (Component.defaultProps !== undefined) {
|
|
const componentName = getComponentNameFromType(type) || 'Unknown';
|
|
if (!didWarnAboutDefaultPropsOnFunctionComponent[componentName]) {
|
|
console.error(
|
|
'%s: Support for defaultProps will be removed from memo components ' +
|
|
'in a future major release. Use JavaScript default parameters instead.',
|
|
componentName,
|
|
);
|
|
didWarnAboutDefaultPropsOnFunctionComponent[componentName] = true;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
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 (
|
|
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 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 (
|
|
// 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.
|
|
mountActivityChildren(workInProgress, nextProps, renderLanes);
|
|
workInProgress.lanes = laneToLanes(OffscreenLane);
|
|
return null;
|
|
} 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,
|
|
};
|
|
|
|
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 = 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;
|
|
if (__DEV__) {
|
|
Component = callLazyInitInDEV(lazyComponent);
|
|
} else {
|
|
const payload = lazyComponent._payload;
|
|
const init = lazyComponent._init;
|
|
Component = init(payload);
|
|
}
|
|
// Store the unwrapped component in the type.
|
|
workInProgress.type = Component;
|
|
|
|
if (typeof Component === 'function') {
|
|
if (isFunctionClassComponent(Component)) {
|
|
const resolvedProps = resolveClassComponentProps(Component, props, false);
|
|
workInProgress.tag = ClassComponent;
|
|
if (__DEV__) {
|
|
workInProgress.type = Component =
|
|
resolveClassForHotReloading(Component);
|
|
}
|
|
return updateClassComponent(
|
|
null,
|
|
workInProgress,
|
|
Component,
|
|
resolvedProps,
|
|
renderLanes,
|
|
);
|
|
} else {
|
|
const resolvedProps = disableDefaultPropsExceptForClasses
|
|
? props
|
|
: resolveDefaultPropsOnNonClassComponent(Component, props);
|
|
workInProgress.tag = FunctionComponent;
|
|
if (__DEV__) {
|
|
validateFunctionComponentInDev(workInProgress, Component);
|
|
workInProgress.type = Component =
|
|
resolveFunctionForHotReloading(Component);
|
|
}
|
|
return updateFunctionComponent(
|
|
null,
|
|
workInProgress,
|
|
Component,
|
|
resolvedProps,
|
|
renderLanes,
|
|
);
|
|
}
|
|
} else if (Component !== undefined && Component !== null) {
|
|
const $$typeof = Component.$$typeof;
|
|
if ($$typeof === REACT_FORWARD_REF_TYPE) {
|
|
const resolvedProps = disableDefaultPropsExceptForClasses
|
|
? props
|
|
: resolveDefaultPropsOnNonClassComponent(Component, props);
|
|
workInProgress.tag = ForwardRef;
|
|
if (__DEV__) {
|
|
workInProgress.type = Component =
|
|
resolveForwardRefForHotReloading(Component);
|
|
}
|
|
return updateForwardRef(
|
|
null,
|
|
workInProgress,
|
|
Component,
|
|
resolvedProps,
|
|
renderLanes,
|
|
);
|
|
} else if ($$typeof === REACT_MEMO_TYPE) {
|
|
const resolvedProps = disableDefaultPropsExceptForClasses
|
|
? props
|
|
: resolveDefaultPropsOnNonClassComponent(Component, props);
|
|
workInProgress.tag = MemoComponent;
|
|
return updateMemoComponent(
|
|
null,
|
|
workInProgress,
|
|
Component,
|
|
disableDefaultPropsExceptForClasses
|
|
? resolvedProps
|
|
: resolveDefaultPropsOnNonClassComponent(
|
|
Component.type,
|
|
resolvedProps,
|
|
), // The inner type can have defaults too
|
|
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 (
|
|
!disableDefaultPropsExceptForClasses &&
|
|
Component.defaultProps !== undefined
|
|
) {
|
|
const componentName = getComponentNameFromType(Component) || 'Unknown';
|
|
|
|
if (!didWarnAboutDefaultPropsOnFunctionComponent[componentName]) {
|
|
console.error(
|
|
'%s: Support for defaultProps will be removed from function components ' +
|
|
'in a future major release. Use JavaScript default parameters instead.',
|
|
componentName,
|
|
);
|
|
didWarnAboutDefaultPropsOnFunctionComponent[componentName] = true;
|
|
}
|
|
}
|
|
|
|
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);
|
|
|
|
const fallbackFragment = 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 fallbackFragment;
|
|
} 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);
|
|
const fallbackFragment = 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 fallbackFragment;
|
|
} 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;
|
|
const fallbackChildFragment = 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 fallbackChildFragment;
|
|
} else {
|
|
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 fallbackChildFragment;
|
|
}
|
|
|
|
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 (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;
|
|
const fallbackChildFragment =
|
|
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 fallbackChildFragment;
|
|
}
|
|
}
|
|
}
|
|
|
|
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;
|
|
}
|
|
|
|
type SuspenseListRevealOrder = 'forwards' | 'backwards' | 'together' | void;
|
|
|
|
function validateRevealOrder(revealOrder: SuspenseListRevealOrder) {
|
|
if (__DEV__) {
|
|
if (
|
|
revealOrder !== undefined &&
|
|
revealOrder !== 'forwards' &&
|
|
revealOrder !== 'backwards' &&
|
|
revealOrder !== 'together' &&
|
|
!didWarnAboutRevealOrder[revealOrder]
|
|
) {
|
|
didWarnAboutRevealOrder[revealOrder] = true;
|
|
if (typeof revealOrder === 'string') {
|
|
switch (revealOrder.toLowerCase()) {
|
|
case 'together':
|
|
case 'forwards':
|
|
case 'backwards': {
|
|
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 "together", "forwards" or "backwards"?',
|
|
revealOrder,
|
|
);
|
|
break;
|
|
}
|
|
} else {
|
|
console.error(
|
|
'%s is not a supported value for revealOrder on <SuspenseList />. ' +
|
|
'Did you mean "together", "forwards" or "backwards"?',
|
|
revealOrder,
|
|
);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
function validateTailOptions(
|
|
tailMode: SuspenseListTailMode,
|
|
revealOrder: SuspenseListRevealOrder,
|
|
) {
|
|
if (__DEV__) {
|
|
if (tailMode !== undefined && !didWarnAboutTailOptions[tailMode]) {
|
|
if (tailMode !== 'collapsed' && tailMode !== 'hidden') {
|
|
didWarnAboutTailOptions[tailMode] = true;
|
|
console.error(
|
|
'"%s" is not a supported value for tail on <SuspenseList />. ' +
|
|
'Did you mean "collapsed" or "hidden"?',
|
|
tailMode,
|
|
);
|
|
} else if (revealOrder !== 'forwards' && revealOrder !== 'backwards') {
|
|
didWarnAboutTailOptions[tailMode] = true;
|
|
console.error(
|
|
'<SuspenseList tail="%s" /> is only valid if revealOrder is ' +
|
|
'"forwards" or "backwards". ' +
|
|
'Did you mean to specify revealOrder="forwards"?',
|
|
tailMode,
|
|
);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
function validateSuspenseListNestedChild(childSlot: mixed, index: number) {
|
|
if (__DEV__) {
|
|
const isAnArray = isArray(childSlot);
|
|
const isIterable =
|
|
!isAnArray && typeof getIteratorFn(childSlot) === 'function';
|
|
if (isAnArray || isIterable) {
|
|
const type = isAnArray ? 'array' : 'iterable';
|
|
console.error(
|
|
'A nested %s was passed to row #%s in <SuspenseList />. Wrap it in ' +
|
|
'an additional SuspenseList to configure its revealOrder: ' +
|
|
'<SuspenseList revealOrder=...> ... ' +
|
|
'<SuspenseList revealOrder=...>{%s}</SuspenseList> ... ' +
|
|
'</SuspenseList>',
|
|
type,
|
|
index,
|
|
type,
|
|
);
|
|
return false;
|
|
}
|
|
}
|
|
return true;
|
|
}
|
|
|
|
function validateSuspenseListChildren(
|
|
children: mixed,
|
|
revealOrder: SuspenseListRevealOrder,
|
|
) {
|
|
if (__DEV__) {
|
|
if (
|
|
(revealOrder === 'forwards' || revealOrder === 'backwards') &&
|
|
children !== undefined &&
|
|
children !== null &&
|
|
children !== false
|
|
) {
|
|
if (isArray(children)) {
|
|
for (let i = 0; i < children.length; i++) {
|
|
if (!validateSuspenseListNestedChild(children[i], i)) {
|
|
return;
|
|
}
|
|
}
|
|
} else {
|
|
const iteratorFn = getIteratorFn(children);
|
|
if (typeof iteratorFn === 'function') {
|
|
const childrenIterator = iteratorFn.call(children);
|
|
if (childrenIterator) {
|
|
let step = childrenIterator.next();
|
|
let i = 0;
|
|
for (; !step.done; step = childrenIterator.next()) {
|
|
if (!validateSuspenseListNestedChild(step.value, i)) {
|
|
return;
|
|
}
|
|
i++;
|
|
}
|
|
}
|
|
} else {
|
|
console.error(
|
|
'A single row was passed to a <SuspenseList revealOrder="%s" />. ' +
|
|
'This is not useful since it needs multiple rows. ' +
|
|
'Did you mean to pass multiple children or an array?',
|
|
revealOrder,
|
|
);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
function initSuspenseListRenderState(
|
|
workInProgress: Fiber,
|
|
isBackwards: boolean,
|
|
tail: null | Fiber,
|
|
lastContentRow: null | Fiber,
|
|
tailMode: SuspenseListTailMode,
|
|
): void {
|
|
const renderState: null | SuspenseListRenderState =
|
|
workInProgress.memoizedState;
|
|
if (renderState === null) {
|
|
workInProgress.memoizedState = ({
|
|
isBackwards: isBackwards,
|
|
rendering: null,
|
|
renderingStartTime: 0,
|
|
last: lastContentRow,
|
|
tail: tail,
|
|
tailMode: tailMode,
|
|
}: 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;
|
|
}
|
|
}
|
|
|
|
// 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 = workInProgress.pendingProps;
|
|
const revealOrder: SuspenseListRevealOrder = nextProps.revealOrder;
|
|
const tailMode: SuspenseListTailMode = nextProps.tail;
|
|
const newChildren = nextProps.children;
|
|
|
|
validateRevealOrder(revealOrder);
|
|
validateTailOptions(tailMode, revealOrder);
|
|
validateSuspenseListChildren(newChildren, revealOrder);
|
|
|
|
reconcileChildren(current, workInProgress, newChildren, renderLanes);
|
|
|
|
let suspenseContext: SuspenseContext = suspenseStackCursor.current;
|
|
|
|
const shouldForceFallback = hasSuspenseListContext(
|
|
suspenseContext,
|
|
(ForceSuspenseFallback: SuspenseContext),
|
|
);
|
|
if (shouldForceFallback) {
|
|
suspenseContext = setShallowSuspenseListContext(
|
|
suspenseContext,
|
|
ForceSuspenseFallback,
|
|
);
|
|
workInProgress.flags |= DidCapture;
|
|
} else {
|
|
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,
|
|
);
|
|
}
|
|
suspenseContext = setDefaultShallowSuspenseListContext(suspenseContext);
|
|
}
|
|
pushSuspenseListContext(workInProgress, suspenseContext);
|
|
|
|
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,
|
|
);
|
|
break;
|
|
}
|
|
case '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,
|
|
);
|
|
break;
|
|
}
|
|
case 'together': {
|
|
initSuspenseListRenderState(
|
|
workInProgress,
|
|
false, // isBackwards
|
|
null, // tail
|
|
null, // last
|
|
undefined,
|
|
);
|
|
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,
|
|
) {
|
|
let context: ReactContext<any>;
|
|
if (enableRenderableContext) {
|
|
context = workInProgress.type;
|
|
} else {
|
|
context = workInProgress.type._context;
|
|
}
|
|
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,
|
|
) {
|
|
let context: ReactContext<any>;
|
|
if (enableRenderableContext) {
|
|
const consumerType: ReactConsumerType<any> = workInProgress.type;
|
|
context = consumerType._context;
|
|
} else {
|
|
context = workInProgress.type;
|
|
if (__DEV__) {
|
|
if ((context: any)._context !== undefined) {
|
|
context = (context: any)._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;
|
|
let context: ReactContext<any>;
|
|
if (enableRenderableContext) {
|
|
context = workInProgress.type;
|
|
} else {
|
|
context = workInProgress.type._context;
|
|
}
|
|
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;
|
|
const unresolvedProps = workInProgress.pendingProps;
|
|
const resolvedProps =
|
|
disableDefaultPropsExceptForClasses ||
|
|
workInProgress.elementType === Component
|
|
? unresolvedProps
|
|
: resolveDefaultPropsOnNonClassComponent(Component, unresolvedProps);
|
|
return updateFunctionComponent(
|
|
current,
|
|
workInProgress,
|
|
Component,
|
|
resolvedProps,
|
|
renderLanes,
|
|
);
|
|
}
|
|
case ClassComponent: {
|
|
const Component = workInProgress.type;
|
|
const unresolvedProps = workInProgress.pendingProps;
|
|
const resolvedProps = resolveClassComponentProps(
|
|
Component,
|
|
unresolvedProps,
|
|
workInProgress.elementType === Component,
|
|
);
|
|
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: {
|
|
const type = workInProgress.type;
|
|
const unresolvedProps = workInProgress.pendingProps;
|
|
const resolvedProps =
|
|
disableDefaultPropsExceptForClasses ||
|
|
workInProgress.elementType === type
|
|
? unresolvedProps
|
|
: resolveDefaultPropsOnNonClassComponent(type, unresolvedProps);
|
|
return updateForwardRef(
|
|
current,
|
|
workInProgress,
|
|
type,
|
|
resolvedProps,
|
|
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: {
|
|
const type = workInProgress.type;
|
|
const unresolvedProps = workInProgress.pendingProps;
|
|
// Resolve outer props first, then resolve inner props.
|
|
let resolvedProps = disableDefaultPropsExceptForClasses
|
|
? unresolvedProps
|
|
: resolveDefaultPropsOnNonClassComponent(type, unresolvedProps);
|
|
resolvedProps = disableDefaultPropsExceptForClasses
|
|
? resolvedProps
|
|
: resolveDefaultPropsOnNonClassComponent(type.type, resolvedProps);
|
|
return updateMemoComponent(
|
|
current,
|
|
workInProgress,
|
|
type,
|
|
resolvedProps,
|
|
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,
|
|
workInProgress.elementType === Component,
|
|
);
|
|
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,
|
|
workInProgress.elementType === Component,
|
|
);
|
|
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};
|