react/packages/react-reconciler/src/ReactFiberCompleteWork.js

1683 lines
59 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 {Fiber, FiberRoot} from './ReactInternalTypes';
import type {RootState} from './ReactFiberRoot';
import type {Lanes, Lane} from './ReactFiberLane';
import type {
ReactScopeInstance,
ReactContext,
Wakeable,
} from 'shared/ReactTypes';
import type {
Instance,
Type,
Props,
Container,
ChildSet,
} from './ReactFiberHostConfig';
import type {
SuspenseState,
SuspenseListRenderState,
} from './ReactFiberSuspenseComponent';
import {isOffscreenManual} from './ReactFiberOffscreenComponent';
import type {OffscreenState} from './ReactFiberOffscreenComponent';
import type {TracingMarkerInstance} from './ReactFiberTracingMarkerComponent';
import type {Cache} from './ReactFiberCacheComponent';
import {
enableLegacyHidden,
enableHostSingletons,
enableSuspenseCallback,
enableScopeAPI,
enableProfilerTimer,
enableCache,
enableTransitionTracing,
enableFloat,
} from 'shared/ReactFeatureFlags';
import {resetWorkInProgressVersions as resetMutableSourceWorkInProgressVersions} from './ReactMutableSource';
import {now} from './Scheduler';
import {
IndeterminateComponent,
FunctionComponent,
ClassComponent,
HostRoot,
HostComponent,
HostResource,
HostSingleton,
HostText,
HostPortal,
ContextProvider,
ContextConsumer,
ForwardRef,
Fragment,
Mode,
Profiler,
SuspenseComponent,
SuspenseListComponent,
MemoComponent,
SimpleMemoComponent,
LazyComponent,
IncompleteClassComponent,
ScopeComponent,
OffscreenComponent,
LegacyHiddenComponent,
CacheComponent,
TracingMarkerComponent,
} from './ReactWorkTags';
import {NoMode, ConcurrentMode, ProfileMode} from './ReactTypeOfMode';
import {
Ref,
RefStatic,
Placement,
Update,
Visibility,
NoFlags,
DidCapture,
Snapshot,
ChildDeletion,
StaticMask,
MutationMask,
Passive,
Incomplete,
ShouldCapture,
ForceClientRender,
} from './ReactFiberFlags';
import {
createInstance,
createTextInstance,
resolveSingletonInstance,
appendInitialChild,
finalizeInitialChildren,
prepareUpdate,
supportsMutation,
supportsPersistence,
supportsResources,
supportsSingletons,
cloneInstance,
cloneHiddenInstance,
cloneHiddenTextInstance,
createContainerChildSet,
appendChildToContainerChildSet,
finalizeContainerChildren,
preparePortalMount,
prepareScopeUpdate,
} from './ReactFiberHostConfig';
import {
getRootHostContainer,
popHostContext,
getHostContext,
popHostContainer,
} from './ReactFiberHostContext';
import {
suspenseStackCursor,
popSuspenseListContext,
popSuspenseHandler,
pushSuspenseListContext,
setShallowSuspenseListContext,
ForceSuspenseFallback,
setDefaultShallowSuspenseListContext,
} from './ReactFiberSuspenseContext';
import {popHiddenContext} from './ReactFiberHiddenContext';
import {findFirstSuspended} from './ReactFiberSuspenseComponent';
import {
isContextProvider as isLegacyContextProvider,
popContext as popLegacyContext,
popTopLevelContextObject as popTopLevelLegacyContextObject,
} from './ReactFiberContext';
import {popProvider} from './ReactFiberNewContext';
import {
prepareToHydrateHostInstance,
prepareToHydrateHostTextInstance,
prepareToHydrateHostSuspenseInstance,
warnIfUnhydratedTailNodes,
popHydrationState,
resetHydrationState,
getIsHydrating,
hasUnhydratedTailNodes,
upgradeHydrationErrorsToRecoverable,
} from './ReactFiberHydrationContext';
import {
renderHasNotSuspendedYet,
getRenderTargetTime,
getWorkInProgressTransitions,
} from './ReactFiberWorkLoop';
import {
OffscreenLane,
SomeRetryLane,
NoLanes,
includesSomeLane,
mergeLanes,
} from './ReactFiberLane';
import {resetChildFibers} from './ReactChildFiber';
import {createScopeInstance} from './ReactFiberScope';
import {transferActualDuration} from './ReactProfilerTimer';
import {popCacheProvider} from './ReactFiberCacheComponent';
import {popTreeContext} from './ReactFiberTreeContext';
import {popRootTransition, popTransition} from './ReactFiberTransition';
import {
popMarkerInstance,
popRootMarkerInstance,
} from './ReactFiberTracingMarkerComponent';
function markUpdate(workInProgress: Fiber) {
// Tag the fiber with an update effect. This turns a Placement into
// a PlacementAndUpdate.
workInProgress.flags |= Update;
}
function markRef(workInProgress: Fiber) {
workInProgress.flags |= Ref | RefStatic;
}
function hadNoMutationsEffects(current: null | Fiber, completedWork: Fiber) {
const didBailout = current !== null && current.child === completedWork.child;
if (didBailout) {
return true;
}
if ((completedWork.flags & ChildDeletion) !== NoFlags) {
return false;
}
// TODO: If we move the `hadNoMutationsEffects` call after `bubbleProperties`
// then we only have to check the `completedWork.subtreeFlags`.
let child = completedWork.child;
while (child !== null) {
if (
(child.flags & MutationMask) !== NoFlags ||
(child.subtreeFlags & MutationMask) !== NoFlags
) {
return false;
}
child = child.sibling;
}
return true;
}
let appendAllChildren: (
parent: Instance,
workInProgress: Fiber,
needsVisibilityToggle: boolean,
isHidden: boolean,
) => void;
let updateHostContainer;
let updateHostComponent;
let updateHostText;
if (supportsMutation) {
// Mutation mode
appendAllChildren = function (
parent: Instance,
workInProgress: Fiber,
needsVisibilityToggle: boolean,
isHidden: boolean,
) {
// We only have the top Fiber that was created but we need recurse down its
// children to find all the terminal nodes.
let node = workInProgress.child;
while (node !== null) {
if (node.tag === HostComponent || node.tag === HostText) {
appendInitialChild(parent, node.stateNode);
} else if (
node.tag === HostPortal ||
(enableHostSingletons && supportsSingletons
? node.tag === HostSingleton
: false)
) {
// If we have a portal child, then we don't want to traverse
// down its children. Instead, we'll get insertions from each child in
// the portal directly.
// If we have a HostSingleton it will be placed independently
} 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;
}
};
updateHostContainer = function (
current: null | Fiber,
workInProgress: Fiber,
) {
// Noop
};
updateHostComponent = function (
current: Fiber,
workInProgress: Fiber,
type: Type,
newProps: Props,
) {
// If we have an alternate, that means this is an update and we need to
// schedule a side-effect to do the updates.
const oldProps = current.memoizedProps;
if (oldProps === newProps) {
// In mutation mode, this is sufficient for a bailout because
// we won't touch this node even if children changed.
return;
}
// If we get updated because one of our children updated, we don't
// have newProps so we'll have to reuse them.
// TODO: Split the update API as separate for the props vs. children.
// Even better would be if children weren't special cased at all tho.
const instance: Instance = workInProgress.stateNode;
const currentHostContext = getHostContext();
// TODO: Experiencing an error where oldProps is null. Suggests a host
// component is hitting the resume path. Figure out why. Possibly
// related to `hidden`.
const updatePayload = prepareUpdate(
instance,
type,
oldProps,
newProps,
currentHostContext,
);
// TODO: Type this specific to this type of component.
workInProgress.updateQueue = (updatePayload: any);
// If the update payload indicates that there is a change or if there
// is a new ref we mark this as an update. All the work is done in commitWork.
if (updatePayload) {
markUpdate(workInProgress);
}
};
updateHostText = function (
current: Fiber,
workInProgress: Fiber,
oldText: string,
newText: string,
) {
// If the text differs, mark it as an update. All the work in done in commitWork.
if (oldText !== newText) {
markUpdate(workInProgress);
}
};
} else if (supportsPersistence) {
// Persistent host tree mode
appendAllChildren = function (
parent: Instance,
workInProgress: Fiber,
needsVisibilityToggle: boolean,
isHidden: boolean,
) {
// We only have the top Fiber that was created but we need recurse down its
// children to find all the terminal nodes.
let node = workInProgress.child;
while (node !== null) {
// eslint-disable-next-line no-labels
branches: if (node.tag === HostComponent) {
let instance = node.stateNode;
if (needsVisibilityToggle && isHidden) {
// This child is inside a timed out tree. Hide it.
const props = node.memoizedProps;
const type = node.type;
instance = cloneHiddenInstance(instance, type, props, node);
}
appendInitialChild(parent, instance);
} else if (node.tag === HostText) {
let instance = node.stateNode;
if (needsVisibilityToggle && isHidden) {
// This child is inside a timed out tree. Hide it.
const text = node.memoizedProps;
instance = cloneHiddenTextInstance(instance, text, node);
}
appendInitialChild(parent, instance);
} else if (node.tag === HostPortal) {
// If we have a portal child, then we don't want to traverse
// down its children. Instead, we'll get insertions from each child in
// the portal directly.
} else if (
node.tag === OffscreenComponent &&
node.memoizedState !== null
) {
// The children in this boundary are hidden. Toggle their visibility
// before appending.
const child = node.child;
if (child !== null) {
child.return = node;
}
appendAllChildren(parent, node, true, true);
} else if (node.child !== null) {
node.child.return = node;
node = node.child;
continue;
}
node = (node: Fiber);
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;
}
};
// An unfortunate fork of appendAllChildren because we have two different parent types.
const appendAllChildrenToContainer = function (
containerChildSet: ChildSet,
workInProgress: Fiber,
needsVisibilityToggle: boolean,
isHidden: boolean,
) {
// We only have the top Fiber that was created but we need recurse down its
// children to find all the terminal nodes.
let node = workInProgress.child;
while (node !== null) {
// eslint-disable-next-line no-labels
branches: if (node.tag === HostComponent) {
let instance = node.stateNode;
if (needsVisibilityToggle && isHidden) {
// This child is inside a timed out tree. Hide it.
const props = node.memoizedProps;
const type = node.type;
instance = cloneHiddenInstance(instance, type, props, node);
}
appendChildToContainerChildSet(containerChildSet, instance);
} else if (node.tag === HostText) {
let instance = node.stateNode;
if (needsVisibilityToggle && isHidden) {
// This child is inside a timed out tree. Hide it.
const text = node.memoizedProps;
instance = cloneHiddenTextInstance(instance, text, node);
}
appendChildToContainerChildSet(containerChildSet, instance);
} else if (node.tag === HostPortal) {
// If we have a portal child, then we don't want to traverse
// down its children. Instead, we'll get insertions from each child in
// the portal directly.
} else if (
node.tag === OffscreenComponent &&
node.memoizedState !== null
) {
// The children in this boundary are hidden. Toggle their visibility
// before appending.
const child = node.child;
if (child !== null) {
child.return = node;
}
// If Offscreen is not in manual mode, detached tree is hidden from user space.
const _needsVisibilityToggle = !isOffscreenManual(node);
appendAllChildrenToContainer(
containerChildSet,
node,
_needsVisibilityToggle,
true,
);
} else if (node.child !== null) {
node.child.return = node;
node = node.child;
continue;
}
node = (node: Fiber);
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;
}
};
updateHostContainer = function (
current: null | Fiber,
workInProgress: Fiber,
) {
const portalOrRoot: {
containerInfo: Container,
pendingChildren: ChildSet,
...
} = workInProgress.stateNode;
const childrenUnchanged = hadNoMutationsEffects(current, workInProgress);
if (childrenUnchanged) {
// No changes, just reuse the existing instance.
} else {
const container = portalOrRoot.containerInfo;
const newChildSet = createContainerChildSet(container);
// If children might have changed, we have to add them all to the set.
appendAllChildrenToContainer(newChildSet, workInProgress, false, false);
portalOrRoot.pendingChildren = newChildSet;
// Schedule an update on the container to swap out the container.
markUpdate(workInProgress);
finalizeContainerChildren(container, newChildSet);
}
};
updateHostComponent = function (
current: Fiber,
workInProgress: Fiber,
type: Type,
newProps: Props,
) {
const currentInstance = current.stateNode;
const oldProps = current.memoizedProps;
// If there are no effects associated with this node, then none of our children had any updates.
// This guarantees that we can reuse all of them.
const childrenUnchanged = hadNoMutationsEffects(current, workInProgress);
if (childrenUnchanged && oldProps === newProps) {
// No changes, just reuse the existing instance.
// Note that this might release a previous clone.
workInProgress.stateNode = currentInstance;
return;
}
const recyclableInstance: Instance = workInProgress.stateNode;
const currentHostContext = getHostContext();
let updatePayload = null;
if (oldProps !== newProps) {
updatePayload = prepareUpdate(
recyclableInstance,
type,
oldProps,
newProps,
currentHostContext,
);
}
if (childrenUnchanged && updatePayload === null) {
// No changes, just reuse the existing instance.
// Note that this might release a previous clone.
workInProgress.stateNode = currentInstance;
return;
}
const newInstance = cloneInstance(
currentInstance,
updatePayload,
type,
oldProps,
newProps,
workInProgress,
childrenUnchanged,
recyclableInstance,
);
if (
finalizeInitialChildren(newInstance, type, newProps, currentHostContext)
) {
markUpdate(workInProgress);
}
workInProgress.stateNode = newInstance;
if (childrenUnchanged) {
// If there are no other effects in this tree, we need to flag this node as having one.
// Even though we're not going to use it for anything.
// Otherwise parents won't know that there are new children to propagate upwards.
markUpdate(workInProgress);
} else {
// If children might have changed, we have to add them all to the set.
appendAllChildren(newInstance, workInProgress, false, false);
}
};
updateHostText = function (
current: Fiber,
workInProgress: Fiber,
oldText: string,
newText: string,
) {
if (oldText !== newText) {
// If the text content differs, we'll create a new text instance for it.
const rootContainerInstance = getRootHostContainer();
const currentHostContext = getHostContext();
workInProgress.stateNode = createTextInstance(
newText,
rootContainerInstance,
currentHostContext,
workInProgress,
);
// We'll have to mark it as having an effect, even though we won't use the effect for anything.
// This lets the parents know that at least one of their children has changed.
markUpdate(workInProgress);
} else {
workInProgress.stateNode = current.stateNode;
}
};
} else {
// No host operations
updateHostContainer = function (
current: null | Fiber,
workInProgress: Fiber,
) {
// Noop
};
updateHostComponent = function (
current: Fiber,
workInProgress: Fiber,
type: Type,
newProps: Props,
) {
// Noop
};
updateHostText = function (
current: Fiber,
workInProgress: Fiber,
oldText: string,
newText: string,
) {
// Noop
};
}
function cutOffTailIfNeeded(
renderState: SuspenseListRenderState,
hasRenderedATailFallback: boolean,
) {
if (getIsHydrating()) {
// If we're hydrating, we should consume as many items as we can
// so we don't leave any behind.
return;
}
switch (renderState.tailMode) {
case 'hidden': {
// Any insertions at the end of the tail list after this point
// should be invisible. If there are already mounted boundaries
// anything before them are not considered for collapsing.
// Therefore we need to go through the whole tail to find if
// there are any.
let tailNode = renderState.tail;
let lastTailNode = null;
while (tailNode !== null) {
if (tailNode.alternate !== null) {
lastTailNode = tailNode;
}
tailNode = tailNode.sibling;
}
// Next we're simply going to delete all insertions after the
// last rendered item.
if (lastTailNode === null) {
// All remaining items in the tail are insertions.
renderState.tail = null;
} else {
// Detach the insertion after the last node that was already
// inserted.
lastTailNode.sibling = null;
}
break;
}
case 'collapsed': {
// Any insertions at the end of the tail list after this point
// should be invisible. If there are already mounted boundaries
// anything before them are not considered for collapsing.
// Therefore we need to go through the whole tail to find if
// there are any.
let tailNode = renderState.tail;
let lastTailNode = null;
while (tailNode !== null) {
if (tailNode.alternate !== null) {
lastTailNode = tailNode;
}
tailNode = tailNode.sibling;
}
// Next we're simply going to delete all insertions after the
// last rendered item.
if (lastTailNode === null) {
// All remaining items in the tail are insertions.
if (!hasRenderedATailFallback && renderState.tail !== null) {
// We suspended during the head. We want to show at least one
// row at the tail. So we'll keep on and cut off the rest.
renderState.tail.sibling = null;
} else {
renderState.tail = null;
}
} else {
// Detach the insertion after the last node that was already
// inserted.
lastTailNode.sibling = null;
}
break;
}
}
}
function bubbleProperties(completedWork: Fiber) {
const didBailout =
completedWork.alternate !== null &&
completedWork.alternate.child === completedWork.child;
let newChildLanes = NoLanes;
let subtreeFlags = NoFlags;
if (!didBailout) {
// Bubble up the earliest expiration time.
if (enableProfilerTimer && (completedWork.mode & ProfileMode) !== NoMode) {
// In profiling mode, resetChildExpirationTime is also used to reset
// profiler durations.
let actualDuration = completedWork.actualDuration;
let treeBaseDuration = ((completedWork.selfBaseDuration: any): number);
let child = completedWork.child;
while (child !== null) {
newChildLanes = mergeLanes(
newChildLanes,
mergeLanes(child.lanes, child.childLanes),
);
subtreeFlags |= child.subtreeFlags;
subtreeFlags |= child.flags;
// When a fiber is cloned, its actualDuration is reset to 0. This value will
// only be updated if work is done on the fiber (i.e. it doesn't bailout).
// When work is done, it should bubble to the parent's actualDuration. If
// the fiber has not been cloned though, (meaning no work was done), then
// this value will reflect the amount of time spent working on a previous
// render. In that case it should not bubble. We determine whether it was
// cloned by comparing the child pointer.
// $FlowFixMe[unsafe-addition] addition with possible null/undefined value
actualDuration += child.actualDuration;
// $FlowFixMe[unsafe-addition] addition with possible null/undefined value
treeBaseDuration += child.treeBaseDuration;
child = child.sibling;
}
completedWork.actualDuration = actualDuration;
completedWork.treeBaseDuration = treeBaseDuration;
} else {
let child = completedWork.child;
while (child !== null) {
newChildLanes = mergeLanes(
newChildLanes,
mergeLanes(child.lanes, child.childLanes),
);
subtreeFlags |= child.subtreeFlags;
subtreeFlags |= child.flags;
// Update the return pointer so the tree is consistent. This is a code
// smell because it assumes the commit phase is never concurrent with
// the render phase. Will address during refactor to alternate model.
child.return = completedWork;
child = child.sibling;
}
}
completedWork.subtreeFlags |= subtreeFlags;
} else {
// Bubble up the earliest expiration time.
if (enableProfilerTimer && (completedWork.mode & ProfileMode) !== NoMode) {
// In profiling mode, resetChildExpirationTime is also used to reset
// profiler durations.
let treeBaseDuration = ((completedWork.selfBaseDuration: any): number);
let child = completedWork.child;
while (child !== null) {
newChildLanes = mergeLanes(
newChildLanes,
mergeLanes(child.lanes, child.childLanes),
);
// "Static" flags share the lifetime of the fiber/hook they belong to,
// so we should bubble those up even during a bailout. All the other
// flags have a lifetime only of a single render + commit, so we should
// ignore them.
subtreeFlags |= child.subtreeFlags & StaticMask;
subtreeFlags |= child.flags & StaticMask;
// $FlowFixMe[unsafe-addition] addition with possible null/undefined value
treeBaseDuration += child.treeBaseDuration;
child = child.sibling;
}
completedWork.treeBaseDuration = treeBaseDuration;
} else {
let child = completedWork.child;
while (child !== null) {
newChildLanes = mergeLanes(
newChildLanes,
mergeLanes(child.lanes, child.childLanes),
);
// "Static" flags share the lifetime of the fiber/hook they belong to,
// so we should bubble those up even during a bailout. All the other
// flags have a lifetime only of a single render + commit, so we should
// ignore them.
subtreeFlags |= child.subtreeFlags & StaticMask;
subtreeFlags |= child.flags & StaticMask;
// Update the return pointer so the tree is consistent. This is a code
// smell because it assumes the commit phase is never concurrent with
// the render phase. Will address during refactor to alternate model.
child.return = completedWork;
child = child.sibling;
}
}
completedWork.subtreeFlags |= subtreeFlags;
}
completedWork.childLanes = newChildLanes;
return didBailout;
}
function completeDehydratedSuspenseBoundary(
current: Fiber | null,
workInProgress: Fiber,
nextState: SuspenseState | null,
): boolean {
if (
hasUnhydratedTailNodes() &&
(workInProgress.mode & ConcurrentMode) !== NoMode &&
(workInProgress.flags & DidCapture) === NoFlags
) {
warnIfUnhydratedTailNodes(workInProgress);
resetHydrationState();
workInProgress.flags |= ForceClientRender | Incomplete | ShouldCapture;
return false;
}
const wasHydrated = popHydrationState(workInProgress);
if (nextState !== null && nextState.dehydrated !== null) {
// We might be inside a hydration state the first time we're picking up this
// Suspense boundary, and also after we've reentered it for further hydration.
if (current === null) {
if (!wasHydrated) {
throw new Error(
'A dehydrated suspense component was completed without a hydrated node. ' +
'This is probably a bug in React.',
);
}
prepareToHydrateHostSuspenseInstance(workInProgress);
bubbleProperties(workInProgress);
if (enableProfilerTimer) {
if ((workInProgress.mode & ProfileMode) !== NoMode) {
const isTimedOutSuspense = nextState !== null;
if (isTimedOutSuspense) {
// Don't count time spent in a timed out Suspense subtree as part of the base duration.
const primaryChildFragment = workInProgress.child;
if (primaryChildFragment !== null) {
// $FlowFixMe Flow doesn't support type casting in combination with the -= operator
workInProgress.treeBaseDuration -=
((primaryChildFragment.treeBaseDuration: any): number);
}
}
}
}
return false;
} else {
// We might have reentered this boundary to hydrate it. If so, we need to reset the hydration
// state since we're now exiting out of it. popHydrationState doesn't do that for us.
resetHydrationState();
if ((workInProgress.flags & DidCapture) === NoFlags) {
// This boundary did not suspend so it's now hydrated and unsuspended.
workInProgress.memoizedState = null;
}
// If nothing suspended, we need to schedule an effect to mark this boundary
// as having hydrated so events know that they're free to be invoked.
// It's also a signal to replay events and the suspense callback.
// If something suspended, schedule an effect to attach retry listeners.
// So we might as well always mark this.
workInProgress.flags |= Update;
bubbleProperties(workInProgress);
if (enableProfilerTimer) {
if ((workInProgress.mode & ProfileMode) !== NoMode) {
const isTimedOutSuspense = nextState !== null;
if (isTimedOutSuspense) {
// Don't count time spent in a timed out Suspense subtree as part of the base duration.
const primaryChildFragment = workInProgress.child;
if (primaryChildFragment !== null) {
// $FlowFixMe Flow doesn't support type casting in combination with the -= operator
workInProgress.treeBaseDuration -=
((primaryChildFragment.treeBaseDuration: any): number);
}
}
}
}
return false;
}
} else {
// Successfully completed this tree. If this was a forced client render,
// there may have been recoverable errors during first hydration
// attempt. If so, add them to a queue so we can log them in the
// commit phase.
upgradeHydrationErrorsToRecoverable();
// Fall through to normal Suspense path
return true;
}
}
function completeWork(
current: Fiber | null,
workInProgress: Fiber,
renderLanes: Lanes,
): Fiber | null {
const newProps = workInProgress.pendingProps;
// Note: This intentionally doesn't check if we're hydrating because comparing
// to the current tree provider fiber is just as fast and less error-prone.
// Ideally we would have a special version of the work loop only
// for hydration.
popTreeContext(workInProgress);
switch (workInProgress.tag) {
case IndeterminateComponent:
case LazyComponent:
case SimpleMemoComponent:
case FunctionComponent:
case ForwardRef:
case Fragment:
case Mode:
case Profiler:
case ContextConsumer:
case MemoComponent:
bubbleProperties(workInProgress);
return null;
case ClassComponent: {
const Component = workInProgress.type;
if (isLegacyContextProvider(Component)) {
popLegacyContext(workInProgress);
}
bubbleProperties(workInProgress);
return null;
}
case HostRoot: {
const fiberRoot = (workInProgress.stateNode: FiberRoot);
if (enableTransitionTracing) {
const transitions = getWorkInProgressTransitions();
// We set the Passive flag here because if there are new transitions,
// we will need to schedule callbacks and process the transitions,
// which we do in the passive phase
if (transitions !== null) {
workInProgress.flags |= Passive;
}
}
if (enableCache) {
let previousCache: Cache | null = null;
if (current !== null) {
previousCache = current.memoizedState.cache;
}
const cache: Cache = workInProgress.memoizedState.cache;
if (cache !== previousCache) {
// Run passive effects to retain/release the cache.
workInProgress.flags |= Passive;
}
popCacheProvider(workInProgress, cache);
}
if (enableTransitionTracing) {
popRootMarkerInstance(workInProgress);
}
popRootTransition(workInProgress, fiberRoot, renderLanes);
popHostContainer(workInProgress);
popTopLevelLegacyContextObject(workInProgress);
resetMutableSourceWorkInProgressVersions();
if (fiberRoot.pendingContext) {
fiberRoot.context = fiberRoot.pendingContext;
fiberRoot.pendingContext = null;
}
if (current === null || current.child === null) {
// If we hydrated, pop so that we can delete any remaining children
// that weren't hydrated.
const wasHydrated = popHydrationState(workInProgress);
if (wasHydrated) {
// If we hydrated, then we'll need to schedule an update for
// the commit side-effects on the root.
markUpdate(workInProgress);
} else {
if (current !== null) {
const prevState: RootState = current.memoizedState;
if (
// Check if this is a client root
!prevState.isDehydrated ||
// Check if we reverted to client rendering (e.g. due to an error)
(workInProgress.flags & ForceClientRender) !== NoFlags
) {
// Schedule an effect to clear this container at the start of the
// next commit. This handles the case of React rendering into a
// container with previous children. It's also safe to do for
// updates too, because current.child would only be null if the
// previous render was null (so the container would already
// be empty).
workInProgress.flags |= Snapshot;
// If this was a forced client render, there may have been
// recoverable errors during first hydration attempt. If so, add
// them to a queue so we can log them in the commit phase.
upgradeHydrationErrorsToRecoverable();
}
}
}
}
updateHostContainer(current, workInProgress);
bubbleProperties(workInProgress);
if (enableTransitionTracing) {
if ((workInProgress.subtreeFlags & Visibility) !== NoFlags) {
// If any of our suspense children toggle visibility, this means that
// the pending boundaries array needs to be updated, which we only
// do in the passive phase.
workInProgress.flags |= Passive;
}
}
return null;
}
case HostResource: {
if (enableFloat && supportsResources) {
popHostContext(workInProgress);
const currentRef = current ? current.ref : null;
if (currentRef !== workInProgress.ref) {
markRef(workInProgress);
}
if (
current === null ||
current.memoizedState !== workInProgress.memoizedState
) {
// The workInProgress resource is different than the current one or the current
// one does not exist
markUpdate(workInProgress);
}
bubbleProperties(workInProgress);
return null;
}
}
// eslint-disable-next-line-no-fallthrough
case HostSingleton: {
if (enableHostSingletons && supportsSingletons) {
popHostContext(workInProgress);
const rootContainerInstance = getRootHostContainer();
const type = workInProgress.type;
if (current !== null && workInProgress.stateNode != null) {
updateHostComponent(current, workInProgress, type, newProps);
if (current.ref !== workInProgress.ref) {
markRef(workInProgress);
}
} else {
if (!newProps) {
if (workInProgress.stateNode === null) {
throw new Error(
'We must have new props for new mounts. This error is likely ' +
'caused by a bug in React. Please file an issue.',
);
}
// This can happen when we abort work.
bubbleProperties(workInProgress);
return null;
}
const currentHostContext = getHostContext();
const wasHydrated = popHydrationState(workInProgress);
if (wasHydrated) {
// We ignore the boolean indicating there is an updateQueue because
// it is used only to set text children and HostSingletons do not
// use them.
prepareToHydrateHostInstance(workInProgress, currentHostContext);
} else {
workInProgress.stateNode = resolveSingletonInstance(
type,
newProps,
rootContainerInstance,
currentHostContext,
true,
);
markUpdate(workInProgress);
}
if (workInProgress.ref !== null) {
// If there is a ref on a host node we need to schedule a callback
markRef(workInProgress);
}
}
bubbleProperties(workInProgress);
return null;
}
}
// eslint-disable-next-line-no-fallthrough
case HostComponent: {
popHostContext(workInProgress);
const type = workInProgress.type;
if (current !== null && workInProgress.stateNode != null) {
updateHostComponent(current, workInProgress, type, newProps);
if (current.ref !== workInProgress.ref) {
markRef(workInProgress);
}
} else {
if (!newProps) {
if (workInProgress.stateNode === null) {
throw new Error(
'We must have new props for new mounts. This error is likely ' +
'caused by a bug in React. Please file an issue.',
);
}
// This can happen when we abort work.
bubbleProperties(workInProgress);
return null;
}
const currentHostContext = getHostContext();
// TODO: Move createInstance to beginWork and keep it on a context
// "stack" as the parent. Then append children as we go in beginWork
// or completeWork depending on whether we want to add them top->down or
// bottom->up. Top->down is faster in IE11.
const wasHydrated = popHydrationState(workInProgress);
if (wasHydrated) {
// TODO: Move this and createInstance step into the beginPhase
// to consolidate.
if (
prepareToHydrateHostInstance(workInProgress, currentHostContext)
) {
// If changes to the hydrated node need to be applied at the
// commit-phase we mark this as such.
markUpdate(workInProgress);
}
} else {
const rootContainerInstance = getRootHostContainer();
const instance = createInstance(
type,
newProps,
rootContainerInstance,
currentHostContext,
workInProgress,
);
appendAllChildren(instance, workInProgress, false, false);
workInProgress.stateNode = instance;
// Certain renderers require commit-time effects for initial mount.
// (eg DOM renderer supports auto-focus for certain elements).
// Make sure such renderers get scheduled for later work.
if (
finalizeInitialChildren(
instance,
type,
newProps,
currentHostContext,
)
) {
markUpdate(workInProgress);
}
}
if (workInProgress.ref !== null) {
// If there is a ref on a host node we need to schedule a callback
markRef(workInProgress);
}
}
bubbleProperties(workInProgress);
return null;
}
case HostText: {
const newText = newProps;
if (current && workInProgress.stateNode != null) {
const oldText = current.memoizedProps;
// If we have an alternate, that means this is an update and we need
// to schedule a side-effect to do the updates.
updateHostText(current, workInProgress, oldText, newText);
} else {
if (typeof newText !== 'string') {
if (workInProgress.stateNode === null) {
throw new Error(
'We must have new props for new mounts. This error is likely ' +
'caused by a bug in React. Please file an issue.',
);
}
// This can happen when we abort work.
}
const rootContainerInstance = getRootHostContainer();
const currentHostContext = getHostContext();
const wasHydrated = popHydrationState(workInProgress);
if (wasHydrated) {
if (prepareToHydrateHostTextInstance(workInProgress)) {
markUpdate(workInProgress);
}
} else {
workInProgress.stateNode = createTextInstance(
newText,
rootContainerInstance,
currentHostContext,
workInProgress,
);
}
}
bubbleProperties(workInProgress);
return null;
}
case SuspenseComponent: {
popSuspenseHandler(workInProgress);
const nextState: null | SuspenseState = workInProgress.memoizedState;
// Special path for dehydrated boundaries. We may eventually move this
// to its own fiber type so that we can add other kinds of hydration
// boundaries that aren't associated with a Suspense tree. In anticipation
// of such a refactor, all the hydration logic is contained in
// this branch.
if (
current === null ||
(current.memoizedState !== null &&
current.memoizedState.dehydrated !== null)
) {
const fallthroughToNormalSuspensePath =
completeDehydratedSuspenseBoundary(
current,
workInProgress,
nextState,
);
if (!fallthroughToNormalSuspensePath) {
if (workInProgress.flags & ShouldCapture) {
// Special case. There were remaining unhydrated nodes. We treat
// this as a mismatch. Revert to client rendering.
return workInProgress;
} else {
// Did not finish hydrating, either because this is the initial
// render or because something suspended.
return null;
}
}
// Continue with the normal Suspense path.
}
if ((workInProgress.flags & DidCapture) !== NoFlags) {
// Something suspended. Re-render with the fallback children.
workInProgress.lanes = renderLanes;
// Do not reset the effect list.
if (
enableProfilerTimer &&
(workInProgress.mode & ProfileMode) !== NoMode
) {
transferActualDuration(workInProgress);
}
// Don't bubble properties in this case.
return workInProgress;
}
const nextDidTimeout = nextState !== null;
const prevDidTimeout =
current !== null &&
(current.memoizedState: null | SuspenseState) !== null;
if (enableCache && nextDidTimeout) {
const offscreenFiber: Fiber = (workInProgress.child: any);
let previousCache: Cache | null = null;
if (
offscreenFiber.alternate !== null &&
offscreenFiber.alternate.memoizedState !== null &&
offscreenFiber.alternate.memoizedState.cachePool !== null
) {
previousCache = offscreenFiber.alternate.memoizedState.cachePool.pool;
}
let cache: Cache | null = null;
if (
offscreenFiber.memoizedState !== null &&
offscreenFiber.memoizedState.cachePool !== null
) {
cache = offscreenFiber.memoizedState.cachePool.pool;
}
if (cache !== previousCache) {
// Run passive effects to retain/release the cache.
offscreenFiber.flags |= Passive;
}
}
// If the suspended state of the boundary changes, we need to schedule
// a passive effect, which is when we process the transitions
if (nextDidTimeout !== prevDidTimeout) {
if (enableTransitionTracing) {
const offscreenFiber: Fiber = (workInProgress.child: any);
offscreenFiber.flags |= Passive;
}
// If the suspended state of the boundary changes, we need to schedule
// an effect to toggle the subtree's visibility. When we switch from
// fallback -> primary, the inner Offscreen fiber schedules this effect
// as part of its normal complete phase. But when we switch from
// primary -> fallback, the inner Offscreen fiber does not have a complete
// phase. So we need to schedule its effect here.
//
// We also use this flag to connect/disconnect the effects, but the same
// logic applies: when re-connecting, the Offscreen fiber's complete
// phase will handle scheduling the effect. It's only when the fallback
// is active that we have to do anything special.
if (nextDidTimeout) {
const offscreenFiber: Fiber = (workInProgress.child: any);
offscreenFiber.flags |= Visibility;
}
}
const wakeables: Set<Wakeable> | null = (workInProgress.updateQueue: any);
if (wakeables !== null) {
// Schedule an effect to attach a retry listener to the promise.
// TODO: Move to passive phase
workInProgress.flags |= Update;
}
if (
enableSuspenseCallback &&
workInProgress.updateQueue !== null &&
workInProgress.memoizedProps.suspenseCallback != null
) {
// Always notify the callback
// TODO: Move to passive phase
workInProgress.flags |= Update;
}
bubbleProperties(workInProgress);
if (enableProfilerTimer) {
if ((workInProgress.mode & ProfileMode) !== NoMode) {
if (nextDidTimeout) {
// Don't count time spent in a timed out Suspense subtree as part of the base duration.
const primaryChildFragment = workInProgress.child;
if (primaryChildFragment !== null) {
// $FlowFixMe Flow doesn't support type casting in combination with the -= operator
workInProgress.treeBaseDuration -=
((primaryChildFragment.treeBaseDuration: any): number);
}
}
}
}
return null;
}
case HostPortal:
popHostContainer(workInProgress);
updateHostContainer(current, workInProgress);
if (current === null) {
preparePortalMount(workInProgress.stateNode.containerInfo);
}
bubbleProperties(workInProgress);
return null;
case ContextProvider:
// Pop provider fiber
const context: ReactContext<any> = workInProgress.type._context;
popProvider(context, workInProgress);
bubbleProperties(workInProgress);
return null;
case IncompleteClassComponent: {
// Same as class component case. I put it down here so that the tags are
// sequential to ensure this switch is compiled to a jump table.
const Component = workInProgress.type;
if (isLegacyContextProvider(Component)) {
popLegacyContext(workInProgress);
}
bubbleProperties(workInProgress);
return null;
}
case SuspenseListComponent: {
popSuspenseListContext(workInProgress);
const renderState: null | SuspenseListRenderState =
workInProgress.memoizedState;
if (renderState === null) {
// We're running in the default, "independent" mode.
// We don't do anything in this mode.
bubbleProperties(workInProgress);
return null;
}
let didSuspendAlready = (workInProgress.flags & DidCapture) !== NoFlags;
const renderedTail = renderState.rendering;
if (renderedTail === null) {
// We just rendered the head.
if (!didSuspendAlready) {
// This is the first pass. We need to figure out if anything is still
// suspended in the rendered set.
// If new content unsuspended, but there's still some content that
// didn't. Then we need to do a second pass that forces everything
// to keep showing their fallbacks.
// We might be suspended if something in this render pass suspended, or
// something in the previous committed pass suspended. Otherwise,
// there's no chance so we can skip the expensive call to
// findFirstSuspended.
const cannotBeSuspended =
renderHasNotSuspendedYet() &&
(current === null || (current.flags & DidCapture) === NoFlags);
if (!cannotBeSuspended) {
let row = workInProgress.child;
while (row !== null) {
const suspended = findFirstSuspended(row);
if (suspended !== null) {
didSuspendAlready = true;
workInProgress.flags |= DidCapture;
cutOffTailIfNeeded(renderState, false);
// If this is a newly suspended tree, it might not get committed as
// part of the second pass. In that case nothing will subscribe to
// its thenables. Instead, we'll transfer its thenables to the
// SuspenseList so that it can retry if they resolve.
// There might be multiple of these in the list but since we're
// going to wait for all of them anyway, it doesn't really matter
// which ones gets to ping. In theory we could get clever and keep
// track of how many dependencies remain but it gets tricky because
// in the meantime, we can add/remove/change items and dependencies.
// We might bail out of the loop before finding any but that
// doesn't matter since that means that the other boundaries that
// we did find already has their listeners attached.
const newThenables = suspended.updateQueue;
if (newThenables !== null) {
workInProgress.updateQueue = newThenables;
workInProgress.flags |= Update;
}
// Rerender the whole list, but this time, we'll force fallbacks
// to stay in place.
// Reset the effect flags before doing the second pass since that's now invalid.
// Reset the child fibers to their original state.
workInProgress.subtreeFlags = NoFlags;
resetChildFibers(workInProgress, renderLanes);
// Set up the Suspense List Context to force suspense and
// immediately rerender the children.
pushSuspenseListContext(
workInProgress,
setShallowSuspenseListContext(
suspenseStackCursor.current,
ForceSuspenseFallback,
),
);
// Don't bubble properties in this case.
return workInProgress.child;
}
row = row.sibling;
}
}
if (renderState.tail !== null && now() > getRenderTargetTime()) {
// We have already passed our CPU deadline but we still have rows
// left in the tail. We'll just give up further attempts to render
// the main content and only render fallbacks.
workInProgress.flags |= DidCapture;
didSuspendAlready = true;
cutOffTailIfNeeded(renderState, false);
// 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;
}
} else {
cutOffTailIfNeeded(renderState, false);
}
// Next we're going to render the tail.
} else {
// Append the rendered row to the child list.
if (!didSuspendAlready) {
const suspended = findFirstSuspended(renderedTail);
if (suspended !== null) {
workInProgress.flags |= DidCapture;
didSuspendAlready = true;
// Ensure we transfer the update queue to the parent so that it doesn't
// get lost if this row ends up dropped during a second pass.
const newThenables = suspended.updateQueue;
if (newThenables !== null) {
workInProgress.updateQueue = newThenables;
workInProgress.flags |= Update;
}
cutOffTailIfNeeded(renderState, true);
// This might have been modified.
if (
renderState.tail === null &&
renderState.tailMode === 'hidden' &&
!renderedTail.alternate &&
!getIsHydrating() // We don't cut it if we're hydrating.
) {
// We're done.
bubbleProperties(workInProgress);
return null;
}
} else if (
// The time it took to render last row is greater than the remaining
// time we have to render. So rendering one more row would likely
// exceed it.
now() * 2 - renderState.renderingStartTime >
getRenderTargetTime() &&
renderLanes !== OffscreenLane
) {
// We have now passed our CPU deadline and we'll just give up further
// attempts to render the main content and only render fallbacks.
// The assumption is that this is usually faster.
workInProgress.flags |= DidCapture;
didSuspendAlready = true;
cutOffTailIfNeeded(renderState, false);
// 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;
}
}
if (renderState.isBackwards) {
// The effect list of the backwards tail will have been added
// to the end. This breaks the guarantee that life-cycles fire in
// sibling order but that isn't a strong guarantee promised by React.
// Especially since these might also just pop in during future commits.
// Append to the beginning of the list.
renderedTail.sibling = workInProgress.child;
workInProgress.child = renderedTail;
} else {
const previousSibling = renderState.last;
if (previousSibling !== null) {
previousSibling.sibling = renderedTail;
} else {
workInProgress.child = renderedTail;
}
renderState.last = renderedTail;
}
}
if (renderState.tail !== null) {
// We still have tail rows to render.
// Pop a row.
const next = renderState.tail;
renderState.rendering = next;
renderState.tail = next.sibling;
renderState.renderingStartTime = now();
next.sibling = null;
// Restore the context.
// TODO: We can probably just avoid popping it instead and only
// setting it the first time we go from not suspended to suspended.
let suspenseContext = suspenseStackCursor.current;
if (didSuspendAlready) {
suspenseContext = setShallowSuspenseListContext(
suspenseContext,
ForceSuspenseFallback,
);
} else {
suspenseContext =
setDefaultShallowSuspenseListContext(suspenseContext);
}
pushSuspenseListContext(workInProgress, suspenseContext);
// Do a pass over the next row.
// Don't bubble properties in this case.
return next;
}
bubbleProperties(workInProgress);
return null;
}
case ScopeComponent: {
if (enableScopeAPI) {
if (current === null) {
const scopeInstance: ReactScopeInstance = createScopeInstance();
workInProgress.stateNode = scopeInstance;
prepareScopeUpdate(scopeInstance, workInProgress);
if (workInProgress.ref !== null) {
markRef(workInProgress);
markUpdate(workInProgress);
}
} else {
if (workInProgress.ref !== null) {
markUpdate(workInProgress);
}
if (current.ref !== workInProgress.ref) {
markRef(workInProgress);
}
}
bubbleProperties(workInProgress);
return null;
}
break;
}
case OffscreenComponent:
case LegacyHiddenComponent: {
popSuspenseHandler(workInProgress);
popHiddenContext(workInProgress);
const nextState: OffscreenState | null = workInProgress.memoizedState;
const nextIsHidden = nextState !== null;
// Schedule a Visibility effect if the visibility has changed
if (enableLegacyHidden && workInProgress.tag === LegacyHiddenComponent) {
// LegacyHidden doesn't do any hiding — it only pre-renders.
} else {
if (current !== null) {
const prevState: OffscreenState | null = current.memoizedState;
const prevIsHidden = prevState !== null;
if (prevIsHidden !== nextIsHidden) {
workInProgress.flags |= Visibility;
}
} else {
// On initial mount, we only need a Visibility effect if the tree
// is hidden.
if (nextIsHidden) {
workInProgress.flags |= Visibility;
}
}
}
if (!nextIsHidden || (workInProgress.mode & ConcurrentMode) === NoMode) {
bubbleProperties(workInProgress);
} else {
// Don't bubble properties for hidden children unless we're rendering
// at offscreen priority.
if (
includesSomeLane(renderLanes, (OffscreenLane: Lane)) &&
// Also don't bubble if the tree suspended
(workInProgress.flags & DidCapture) === NoLanes
) {
bubbleProperties(workInProgress);
// Check if there was an insertion or update in the hidden subtree.
// If so, we need to hide those nodes in the commit phase, so
// schedule a visibility effect.
if (
(!enableLegacyHidden ||
workInProgress.tag !== LegacyHiddenComponent) &&
workInProgress.subtreeFlags & (Placement | Update)
) {
workInProgress.flags |= Visibility;
}
}
}
if (workInProgress.updateQueue !== null) {
// Schedule an effect to attach Suspense retry listeners
// TODO: Move to passive phase
workInProgress.flags |= Update;
}
if (enableCache) {
let previousCache: Cache | null = null;
if (
current !== null &&
current.memoizedState !== null &&
current.memoizedState.cachePool !== null
) {
previousCache = current.memoizedState.cachePool.pool;
}
let cache: Cache | null = null;
if (
workInProgress.memoizedState !== null &&
workInProgress.memoizedState.cachePool !== null
) {
cache = workInProgress.memoizedState.cachePool.pool;
}
if (cache !== previousCache) {
// Run passive effects to retain/release the cache.
workInProgress.flags |= Passive;
}
}
popTransition(workInProgress, current);
return null;
}
case CacheComponent: {
if (enableCache) {
let previousCache: Cache | null = null;
if (current !== null) {
previousCache = current.memoizedState.cache;
}
const cache: Cache = workInProgress.memoizedState.cache;
if (cache !== previousCache) {
// Run passive effects to retain/release the cache.
workInProgress.flags |= Passive;
}
popCacheProvider(workInProgress, cache);
bubbleProperties(workInProgress);
}
return null;
}
case TracingMarkerComponent: {
if (enableTransitionTracing) {
const instance: TracingMarkerInstance | null = workInProgress.stateNode;
if (instance !== null) {
popMarkerInstance(workInProgress);
}
bubbleProperties(workInProgress);
}
return null;
}
}
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 {completeWork};