Files
react/packages/react-reconciler/src/ReactFiberCommitWork.old.js
T
Brian Vaughn 44d39c4d76 Removed skip-error-boundaries modifications from old fork (#19961)
Technically this change is unnecessary, since the feature is controlled by a flag, but since we decided not to ship this in v17– I'm going to remove it for now entirely.
2020-10-05 15:49:44 -04:00

1755 lines
56 KiB
JavaScript

/**
* Copyright (c) Facebook, Inc. and its affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
* @flow
*/
import type {
Instance,
TextInstance,
SuspenseInstance,
Container,
ChildSet,
UpdatePayload,
} from './ReactFiberHostConfig';
import type {Fiber} from './ReactInternalTypes';
import type {FiberRoot} from './ReactInternalTypes';
import type {Lanes} from './ReactFiberLane';
import type {SuspenseState} from './ReactFiberSuspenseComponent.old';
import type {UpdateQueue} from './ReactUpdateQueue.old';
import type {FunctionComponentUpdateQueue} from './ReactFiberHooks.old';
import type {Wakeable} from 'shared/ReactTypes';
import type {ReactPriorityLevel} from './ReactInternalTypes';
import type {OffscreenState} from './ReactFiberOffscreenComponent';
import {unstable_wrap as Schedule_tracing_wrap} from 'scheduler/tracing';
import {
enableSchedulerTracing,
enableProfilerTimer,
enableProfilerCommitHooks,
enableSuspenseServerRenderer,
enableFundamentalAPI,
enableSuspenseCallback,
enableScopeAPI,
} from 'shared/ReactFeatureFlags';
import {
FunctionComponent,
ForwardRef,
ClassComponent,
HostRoot,
HostComponent,
HostText,
HostPortal,
Profiler,
SuspenseComponent,
DehydratedFragment,
IncompleteClassComponent,
MemoComponent,
SimpleMemoComponent,
SuspenseListComponent,
FundamentalComponent,
ScopeComponent,
Block,
OffscreenComponent,
LegacyHiddenComponent,
} from './ReactWorkTags';
import {
invokeGuardedCallback,
hasCaughtError,
clearCaughtError,
} from 'shared/ReactErrorUtils';
import {
NoFlags,
ContentReset,
Placement,
Snapshot,
Update,
} from './ReactFiberFlags';
import getComponentName from 'shared/getComponentName';
import invariant from 'shared/invariant';
import {onCommitUnmount} from './ReactFiberDevToolsHook.old';
import {resolveDefaultProps} from './ReactFiberLazyComponent.old';
import {
getCommitTime,
recordLayoutEffectDuration,
startLayoutEffectTimer,
} from './ReactProfilerTimer.old';
import {ProfileMode} from './ReactTypeOfMode';
import {commitUpdateQueue} from './ReactUpdateQueue.old';
import {
getPublicInstance,
supportsMutation,
supportsPersistence,
supportsHydration,
commitMount,
commitUpdate,
resetTextContent,
commitTextUpdate,
appendChild,
appendChildToContainer,
insertBefore,
insertInContainerBefore,
removeChild,
removeChildFromContainer,
clearSuspenseBoundary,
clearSuspenseBoundaryFromContainer,
replaceContainerChildren,
createContainerChildSet,
hideInstance,
hideTextInstance,
unhideInstance,
unhideTextInstance,
unmountFundamentalComponent,
updateFundamentalComponent,
commitHydratedContainer,
commitHydratedSuspenseInstance,
clearContainer,
prepareScopeUpdate,
} from './ReactFiberHostConfig';
import {
captureCommitPhaseError,
resolveRetryWakeable,
markCommitTimeOfFallback,
enqueuePendingPassiveHookEffectMount,
enqueuePendingPassiveHookEffectUnmount,
enqueuePendingPassiveProfilerEffect,
} from './ReactFiberWorkLoop.old';
import {
NoFlags as NoHookEffect,
HasEffect as HookHasEffect,
Layout as HookLayout,
Passive as HookPassive,
} from './ReactHookEffectTags';
import {didWarnAboutReassigningProps} from './ReactFiberBeginWork.old';
let didWarnAboutUndefinedSnapshotBeforeUpdate: Set<mixed> | null = null;
if (__DEV__) {
didWarnAboutUndefinedSnapshotBeforeUpdate = new Set();
}
const PossiblyWeakSet = typeof WeakSet === 'function' ? WeakSet : Set;
const callComponentWillUnmountWithTimer = function(current, instance) {
instance.props = current.memoizedProps;
instance.state = current.memoizedState;
if (
enableProfilerTimer &&
enableProfilerCommitHooks &&
current.mode & ProfileMode
) {
try {
startLayoutEffectTimer();
instance.componentWillUnmount();
} finally {
recordLayoutEffectDuration(current);
}
} else {
instance.componentWillUnmount();
}
};
// Capture errors so they don't interrupt unmounting.
function safelyCallComponentWillUnmount(current: Fiber, instance: any) {
if (__DEV__) {
invokeGuardedCallback(
null,
callComponentWillUnmountWithTimer,
null,
current,
instance,
);
if (hasCaughtError()) {
const unmountError = clearCaughtError();
captureCommitPhaseError(current, unmountError);
}
} else {
try {
callComponentWillUnmountWithTimer(current, instance);
} catch (unmountError) {
captureCommitPhaseError(current, unmountError);
}
}
}
function safelyDetachRef(current: Fiber) {
const ref = current.ref;
if (ref !== null) {
if (typeof ref === 'function') {
if (__DEV__) {
invokeGuardedCallback(null, ref, null, null);
if (hasCaughtError()) {
const refError = clearCaughtError();
captureCommitPhaseError(current, refError);
}
} else {
try {
ref(null);
} catch (refError) {
captureCommitPhaseError(current, refError);
}
}
} else {
ref.current = null;
}
}
}
function safelyCallDestroy(current: Fiber, destroy: () => void) {
if (__DEV__) {
invokeGuardedCallback(null, destroy, null);
if (hasCaughtError()) {
const error = clearCaughtError();
captureCommitPhaseError(current, error);
}
} else {
try {
destroy();
} catch (error) {
captureCommitPhaseError(current, error);
}
}
}
function commitBeforeMutationLifeCycles(
current: Fiber | null,
finishedWork: Fiber,
): void {
switch (finishedWork.tag) {
case FunctionComponent:
case ForwardRef:
case SimpleMemoComponent:
case Block: {
return;
}
case ClassComponent: {
if (finishedWork.flags & Snapshot) {
if (current !== null) {
const prevProps = current.memoizedProps;
const prevState = current.memoizedState;
const instance = finishedWork.stateNode;
// We could update instance props and state here,
// but instead we rely on them being set during last render.
// TODO: revisit this when we implement resuming.
if (__DEV__) {
if (
finishedWork.type === finishedWork.elementType &&
!didWarnAboutReassigningProps
) {
if (instance.props !== finishedWork.memoizedProps) {
console.error(
'Expected %s props to match memoized props before ' +
'getSnapshotBeforeUpdate. ' +
'This might either be because of a bug in React, or because ' +
'a component reassigns its own `this.props`. ' +
'Please file an issue.',
getComponentName(finishedWork.type) || 'instance',
);
}
if (instance.state !== finishedWork.memoizedState) {
console.error(
'Expected %s state to match memoized state before ' +
'getSnapshotBeforeUpdate. ' +
'This might either be because of a bug in React, or because ' +
'a component reassigns its own `this.state`. ' +
'Please file an issue.',
getComponentName(finishedWork.type) || 'instance',
);
}
}
}
const snapshot = instance.getSnapshotBeforeUpdate(
finishedWork.elementType === finishedWork.type
? prevProps
: resolveDefaultProps(finishedWork.type, prevProps),
prevState,
);
if (__DEV__) {
const didWarnSet = ((didWarnAboutUndefinedSnapshotBeforeUpdate: any): Set<mixed>);
if (snapshot === undefined && !didWarnSet.has(finishedWork.type)) {
didWarnSet.add(finishedWork.type);
console.error(
'%s.getSnapshotBeforeUpdate(): A snapshot value (or null) ' +
'must be returned. You have returned undefined.',
getComponentName(finishedWork.type),
);
}
}
instance.__reactInternalSnapshotBeforeUpdate = snapshot;
}
}
return;
}
case HostRoot: {
if (supportsMutation) {
if (finishedWork.flags & Snapshot) {
const root = finishedWork.stateNode;
clearContainer(root.containerInfo);
}
}
return;
}
case HostComponent:
case HostText:
case HostPortal:
case IncompleteClassComponent:
// Nothing to do for these component types
return;
}
invariant(
false,
'This unit of work tag should not have side-effects. This error is ' +
'likely caused by a bug in React. Please file an issue.',
);
}
function commitHookEffectListUnmount(tag: number, finishedWork: Fiber) {
const updateQueue: FunctionComponentUpdateQueue | null = (finishedWork.updateQueue: any);
const lastEffect = updateQueue !== null ? updateQueue.lastEffect : null;
if (lastEffect !== null) {
const firstEffect = lastEffect.next;
let effect = firstEffect;
do {
if ((effect.tag & tag) === tag) {
// Unmount
const destroy = effect.destroy;
effect.destroy = undefined;
if (destroy !== undefined) {
destroy();
}
}
effect = effect.next;
} while (effect !== firstEffect);
}
}
function commitHookEffectListMount(tag: number, finishedWork: Fiber) {
const updateQueue: FunctionComponentUpdateQueue | null = (finishedWork.updateQueue: any);
const lastEffect = updateQueue !== null ? updateQueue.lastEffect : null;
if (lastEffect !== null) {
const firstEffect = lastEffect.next;
let effect = firstEffect;
do {
if ((effect.tag & tag) === tag) {
// Mount
const create = effect.create;
effect.destroy = create();
if (__DEV__) {
const destroy = effect.destroy;
if (destroy !== undefined && typeof destroy !== 'function') {
let addendum;
if (destroy === null) {
addendum =
' You returned null. If your effect does not require clean ' +
'up, return undefined (or nothing).';
} else if (typeof destroy.then === 'function') {
addendum =
'\n\nIt looks like you wrote useEffect(async () => ...) or returned a Promise. ' +
'Instead, write the async function inside your effect ' +
'and call it immediately:\n\n' +
'useEffect(() => {\n' +
' async function fetchData() {\n' +
' // You can await here\n' +
' const response = await MyAPI.getData(someId);\n' +
' // ...\n' +
' }\n' +
' fetchData();\n' +
`}, [someId]); // Or [] if effect doesn't need props or state\n\n` +
'Learn more about data fetching with Hooks: https://reactjs.org/link/hooks-data-fetching';
} else {
addendum = ' You returned: ' + destroy;
}
console.error(
'An effect function must not return anything besides a function, ' +
'which is used for clean-up.%s',
addendum,
);
}
}
}
effect = effect.next;
} while (effect !== firstEffect);
}
}
function schedulePassiveEffects(finishedWork: Fiber) {
const updateQueue: FunctionComponentUpdateQueue | null = (finishedWork.updateQueue: any);
const lastEffect = updateQueue !== null ? updateQueue.lastEffect : null;
if (lastEffect !== null) {
const firstEffect = lastEffect.next;
let effect = firstEffect;
do {
const {next, tag} = effect;
if (
(tag & HookPassive) !== NoHookEffect &&
(tag & HookHasEffect) !== NoHookEffect
) {
enqueuePendingPassiveHookEffectUnmount(finishedWork, effect);
enqueuePendingPassiveHookEffectMount(finishedWork, effect);
}
effect = next;
} while (effect !== firstEffect);
}
}
export function commitPassiveEffectDurations(
finishedRoot: FiberRoot,
finishedWork: Fiber,
): void {
if (enableProfilerTimer && enableProfilerCommitHooks) {
// Only Profilers with work in their subtree will have an Update effect scheduled.
if ((finishedWork.flags & Update) !== NoFlags) {
switch (finishedWork.tag) {
case Profiler: {
const {passiveEffectDuration} = finishedWork.stateNode;
const {id, onPostCommit} = finishedWork.memoizedProps;
// This value will still reflect the previous commit phase.
// It does not get reset until the start of the next commit phase.
const commitTime = getCommitTime();
if (typeof onPostCommit === 'function') {
if (enableSchedulerTracing) {
onPostCommit(
id,
finishedWork.alternate === null ? 'mount' : 'update',
passiveEffectDuration,
commitTime,
finishedRoot.memoizedInteractions,
);
} else {
onPostCommit(
id,
finishedWork.alternate === null ? 'mount' : 'update',
passiveEffectDuration,
commitTime,
);
}
}
// Bubble times to the next nearest ancestor Profiler.
// After we process that Profiler, we'll bubble further up.
let parentFiber = finishedWork.return;
while (parentFiber !== null) {
if (parentFiber.tag === Profiler) {
const parentStateNode = parentFiber.stateNode;
parentStateNode.passiveEffectDuration += passiveEffectDuration;
break;
}
parentFiber = parentFiber.return;
}
break;
}
default:
break;
}
}
}
}
function commitLifeCycles(
finishedRoot: FiberRoot,
current: Fiber | null,
finishedWork: Fiber,
committedLanes: Lanes,
): void {
switch (finishedWork.tag) {
case FunctionComponent:
case ForwardRef:
case SimpleMemoComponent:
case Block: {
// At this point layout effects have already been destroyed (during mutation phase).
// This is done to prevent sibling component effects from interfering with each other,
// e.g. a destroy function in one component should never override a ref set
// by a create function in another component during the same commit.
if (
enableProfilerTimer &&
enableProfilerCommitHooks &&
finishedWork.mode & ProfileMode
) {
try {
startLayoutEffectTimer();
commitHookEffectListMount(HookLayout | HookHasEffect, finishedWork);
} finally {
recordLayoutEffectDuration(finishedWork);
}
} else {
commitHookEffectListMount(HookLayout | HookHasEffect, finishedWork);
}
schedulePassiveEffects(finishedWork);
return;
}
case ClassComponent: {
const instance = finishedWork.stateNode;
if (finishedWork.flags & Update) {
if (current === null) {
// We could update instance props and state here,
// but instead we rely on them being set during last render.
// TODO: revisit this when we implement resuming.
if (__DEV__) {
if (
finishedWork.type === finishedWork.elementType &&
!didWarnAboutReassigningProps
) {
if (instance.props !== finishedWork.memoizedProps) {
console.error(
'Expected %s props to match memoized props before ' +
'componentDidMount. ' +
'This might either be because of a bug in React, or because ' +
'a component reassigns its own `this.props`. ' +
'Please file an issue.',
getComponentName(finishedWork.type) || 'instance',
);
}
if (instance.state !== finishedWork.memoizedState) {
console.error(
'Expected %s state to match memoized state before ' +
'componentDidMount. ' +
'This might either be because of a bug in React, or because ' +
'a component reassigns its own `this.state`. ' +
'Please file an issue.',
getComponentName(finishedWork.type) || 'instance',
);
}
}
}
if (
enableProfilerTimer &&
enableProfilerCommitHooks &&
finishedWork.mode & ProfileMode
) {
try {
startLayoutEffectTimer();
instance.componentDidMount();
} finally {
recordLayoutEffectDuration(finishedWork);
}
} else {
instance.componentDidMount();
}
} else {
const prevProps =
finishedWork.elementType === finishedWork.type
? current.memoizedProps
: resolveDefaultProps(finishedWork.type, current.memoizedProps);
const prevState = current.memoizedState;
// We could update instance props and state here,
// but instead we rely on them being set during last render.
// TODO: revisit this when we implement resuming.
if (__DEV__) {
if (
finishedWork.type === finishedWork.elementType &&
!didWarnAboutReassigningProps
) {
if (instance.props !== finishedWork.memoizedProps) {
console.error(
'Expected %s props to match memoized props before ' +
'componentDidUpdate. ' +
'This might either be because of a bug in React, or because ' +
'a component reassigns its own `this.props`. ' +
'Please file an issue.',
getComponentName(finishedWork.type) || 'instance',
);
}
if (instance.state !== finishedWork.memoizedState) {
console.error(
'Expected %s state to match memoized state before ' +
'componentDidUpdate. ' +
'This might either be because of a bug in React, or because ' +
'a component reassigns its own `this.state`. ' +
'Please file an issue.',
getComponentName(finishedWork.type) || 'instance',
);
}
}
}
if (
enableProfilerTimer &&
enableProfilerCommitHooks &&
finishedWork.mode & ProfileMode
) {
try {
startLayoutEffectTimer();
instance.componentDidUpdate(
prevProps,
prevState,
instance.__reactInternalSnapshotBeforeUpdate,
);
} finally {
recordLayoutEffectDuration(finishedWork);
}
} else {
instance.componentDidUpdate(
prevProps,
prevState,
instance.__reactInternalSnapshotBeforeUpdate,
);
}
}
}
// TODO: I think this is now always non-null by the time it reaches the
// commit phase. Consider removing the type check.
const updateQueue: UpdateQueue<
*,
> | null = (finishedWork.updateQueue: any);
if (updateQueue !== null) {
if (__DEV__) {
if (
finishedWork.type === finishedWork.elementType &&
!didWarnAboutReassigningProps
) {
if (instance.props !== finishedWork.memoizedProps) {
console.error(
'Expected %s props to match memoized props before ' +
'processing the update queue. ' +
'This might either be because of a bug in React, or because ' +
'a component reassigns its own `this.props`. ' +
'Please file an issue.',
getComponentName(finishedWork.type) || 'instance',
);
}
if (instance.state !== finishedWork.memoizedState) {
console.error(
'Expected %s state to match memoized state before ' +
'processing the update queue. ' +
'This might either be because of a bug in React, or because ' +
'a component reassigns its own `this.state`. ' +
'Please file an issue.',
getComponentName(finishedWork.type) || 'instance',
);
}
}
}
// We could update instance props and state here,
// but instead we rely on them being set during last render.
// TODO: revisit this when we implement resuming.
commitUpdateQueue(finishedWork, updateQueue, instance);
}
return;
}
case HostRoot: {
// TODO: I think this is now always non-null by the time it reaches the
// commit phase. Consider removing the type check.
const updateQueue: UpdateQueue<
*,
> | null = (finishedWork.updateQueue: any);
if (updateQueue !== null) {
let instance = null;
if (finishedWork.child !== null) {
switch (finishedWork.child.tag) {
case HostComponent:
instance = getPublicInstance(finishedWork.child.stateNode);
break;
case ClassComponent:
instance = finishedWork.child.stateNode;
break;
}
}
commitUpdateQueue(finishedWork, updateQueue, instance);
}
return;
}
case HostComponent: {
const instance: Instance = finishedWork.stateNode;
// Renderers may schedule work to be done after host components are mounted
// (eg DOM renderer may schedule auto-focus for inputs and form controls).
// These effects should only be committed when components are first mounted,
// aka when there is no current/alternate.
if (current === null && finishedWork.flags & Update) {
const type = finishedWork.type;
const props = finishedWork.memoizedProps;
commitMount(instance, type, props, finishedWork);
}
return;
}
case HostText: {
// We have no life-cycles associated with text.
return;
}
case HostPortal: {
// We have no life-cycles associated with portals.
return;
}
case Profiler: {
if (enableProfilerTimer) {
const {onCommit, onRender} = finishedWork.memoizedProps;
const {effectDuration} = finishedWork.stateNode;
const commitTime = getCommitTime();
if (typeof onRender === 'function') {
if (enableSchedulerTracing) {
onRender(
finishedWork.memoizedProps.id,
current === null ? 'mount' : 'update',
finishedWork.actualDuration,
finishedWork.treeBaseDuration,
finishedWork.actualStartTime,
commitTime,
finishedRoot.memoizedInteractions,
);
} else {
onRender(
finishedWork.memoizedProps.id,
current === null ? 'mount' : 'update',
finishedWork.actualDuration,
finishedWork.treeBaseDuration,
finishedWork.actualStartTime,
commitTime,
);
}
}
if (enableProfilerCommitHooks) {
if (typeof onCommit === 'function') {
if (enableSchedulerTracing) {
onCommit(
finishedWork.memoizedProps.id,
current === null ? 'mount' : 'update',
effectDuration,
commitTime,
finishedRoot.memoizedInteractions,
);
} else {
onCommit(
finishedWork.memoizedProps.id,
current === null ? 'mount' : 'update',
effectDuration,
commitTime,
);
}
}
// 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.
enqueuePendingPassiveProfilerEffect(finishedWork);
// Propagate layout effect durations to the next nearest Profiler ancestor.
// Do not reset these values until the next render so DevTools has a chance to read them first.
let parentFiber = finishedWork.return;
while (parentFiber !== null) {
if (parentFiber.tag === Profiler) {
const parentStateNode = parentFiber.stateNode;
parentStateNode.effectDuration += effectDuration;
break;
}
parentFiber = parentFiber.return;
}
}
}
return;
}
case SuspenseComponent: {
commitSuspenseHydrationCallbacks(finishedRoot, finishedWork);
return;
}
case SuspenseListComponent:
case IncompleteClassComponent:
case FundamentalComponent:
case ScopeComponent:
case OffscreenComponent:
case LegacyHiddenComponent:
return;
}
invariant(
false,
'This unit of work tag should not have side-effects. This error is ' +
'likely caused by a bug in React. Please file an issue.',
);
}
function hideOrUnhideAllChildren(finishedWork, isHidden) {
if (supportsMutation) {
// We only have the top Fiber that was inserted but we need to recurse down its
// children to find all the terminal nodes.
let node: Fiber = finishedWork;
while (true) {
if (node.tag === HostComponent) {
const instance = node.stateNode;
if (isHidden) {
hideInstance(instance);
} else {
unhideInstance(node.stateNode, node.memoizedProps);
}
} else if (node.tag === HostText) {
const instance = node.stateNode;
if (isHidden) {
hideTextInstance(instance);
} else {
unhideTextInstance(instance, node.memoizedProps);
}
} else if (
(node.tag === OffscreenComponent ||
node.tag === LegacyHiddenComponent) &&
(node.memoizedState: OffscreenState) !== null &&
node !== finishedWork
) {
// Found a nested Offscreen component that is hidden. Don't search
// any deeper. This tree should remain hidden.
} else if (node.child !== null) {
node.child.return = node;
node = node.child;
continue;
}
if (node === finishedWork) {
return;
}
while (node.sibling === null) {
if (node.return === null || node.return === finishedWork) {
return;
}
node = node.return;
}
node.sibling.return = node.return;
node = node.sibling;
}
}
}
function commitAttachRef(finishedWork: Fiber) {
const ref = finishedWork.ref;
if (ref !== null) {
const instance = finishedWork.stateNode;
let instanceToUse;
switch (finishedWork.tag) {
case HostComponent:
instanceToUse = getPublicInstance(instance);
break;
default:
instanceToUse = instance;
}
// Moved outside to ensure DCE works with this flag
if (enableScopeAPI && finishedWork.tag === ScopeComponent) {
instanceToUse = instance;
}
if (typeof ref === 'function') {
ref(instanceToUse);
} else {
if (__DEV__) {
if (!ref.hasOwnProperty('current')) {
console.error(
'Unexpected ref object provided for %s. ' +
'Use either a ref-setter function or React.createRef().',
getComponentName(finishedWork.type),
);
}
}
ref.current = instanceToUse;
}
}
}
function commitDetachRef(current: Fiber) {
const currentRef = current.ref;
if (currentRef !== null) {
if (typeof currentRef === 'function') {
currentRef(null);
} else {
currentRef.current = null;
}
}
}
// User-originating errors (lifecycles and refs) should not interrupt
// deletion, so don't let them throw. Host-originating errors should
// interrupt deletion, so it's okay
function commitUnmount(
finishedRoot: FiberRoot,
current: Fiber,
renderPriorityLevel: ReactPriorityLevel,
): void {
onCommitUnmount(current);
switch (current.tag) {
case FunctionComponent:
case ForwardRef:
case MemoComponent:
case SimpleMemoComponent:
case Block: {
const updateQueue: FunctionComponentUpdateQueue | null = (current.updateQueue: any);
if (updateQueue !== null) {
const lastEffect = updateQueue.lastEffect;
if (lastEffect !== null) {
const firstEffect = lastEffect.next;
let effect = firstEffect;
do {
const {destroy, tag} = effect;
if (destroy !== undefined) {
if ((tag & HookPassive) !== NoHookEffect) {
enqueuePendingPassiveHookEffectUnmount(current, effect);
} else {
if (
enableProfilerTimer &&
enableProfilerCommitHooks &&
current.mode & ProfileMode
) {
startLayoutEffectTimer();
safelyCallDestroy(current, destroy);
recordLayoutEffectDuration(current);
} else {
safelyCallDestroy(current, destroy);
}
}
}
effect = effect.next;
} while (effect !== firstEffect);
}
}
return;
}
case ClassComponent: {
safelyDetachRef(current);
const instance = current.stateNode;
if (typeof instance.componentWillUnmount === 'function') {
safelyCallComponentWillUnmount(current, instance);
}
return;
}
case HostComponent: {
safelyDetachRef(current);
return;
}
case HostPortal: {
// TODO: this is recursive.
// We are also not using this parent because
// the portal will get pushed immediately.
if (supportsMutation) {
unmountHostComponents(finishedRoot, current, renderPriorityLevel);
} else if (supportsPersistence) {
emptyPortalContainer(current);
}
return;
}
case FundamentalComponent: {
if (enableFundamentalAPI) {
const fundamentalInstance = current.stateNode;
if (fundamentalInstance !== null) {
unmountFundamentalComponent(fundamentalInstance);
current.stateNode = null;
}
}
return;
}
case DehydratedFragment: {
if (enableSuspenseCallback) {
const hydrationCallbacks = finishedRoot.hydrationCallbacks;
if (hydrationCallbacks !== null) {
const onDeleted = hydrationCallbacks.onDeleted;
if (onDeleted) {
onDeleted((current.stateNode: SuspenseInstance));
}
}
}
return;
}
case ScopeComponent: {
if (enableScopeAPI) {
safelyDetachRef(current);
}
return;
}
}
}
function commitNestedUnmounts(
finishedRoot: FiberRoot,
root: Fiber,
renderPriorityLevel: ReactPriorityLevel,
): void {
// While we're inside a removed host node we don't want to call
// removeChild on the inner nodes because they're removed by the top
// call anyway. We also want to call componentWillUnmount on all
// composites before this host node is removed from the tree. Therefore
// we do an inner loop while we're still inside the host node.
let node: Fiber = root;
while (true) {
commitUnmount(finishedRoot, node, renderPriorityLevel);
// Visit children because they may contain more composite or host nodes.
// Skip portals because commitUnmount() currently visits them recursively.
if (
node.child !== null &&
// If we use mutation we drill down into portals using commitUnmount above.
// If we don't use mutation we drill down into portals here instead.
(!supportsMutation || node.tag !== HostPortal)
) {
node.child.return = node;
node = node.child;
continue;
}
if (node === root) {
return;
}
while (node.sibling === null) {
if (node.return === null || node.return === root) {
return;
}
node = node.return;
}
node.sibling.return = node.return;
node = node.sibling;
}
}
function detachFiberMutation(fiber: Fiber) {
// Cut off the return pointers to disconnect it from the tree. Ideally, we
// should clear the child pointer of the parent alternate to let this
// get GC:ed but we don't know which for sure which parent is the current
// one so we'll settle for GC:ing the subtree of this child. This child
// itself will be GC:ed when the parent updates the next time.
// Note: we cannot null out sibling here, otherwise it can cause issues
// with findDOMNode and how it requires the sibling field to carry out
// traversal in a later effect. See PR #16820. We now clear the sibling
// field after effects, see: detachFiberAfterEffects.
//
// Don't disconnect stateNode now; it will be detached in detachFiberAfterEffects.
// It may be required if the current component is an error boundary,
// and one of its descendants throws while unmounting a passive effect.
fiber.alternate = null;
fiber.child = null;
fiber.dependencies = null;
fiber.firstEffect = null;
fiber.lastEffect = null;
fiber.memoizedProps = null;
fiber.memoizedState = null;
fiber.pendingProps = null;
fiber.return = null;
fiber.updateQueue = null;
if (__DEV__) {
fiber._debugOwner = null;
}
}
function emptyPortalContainer(current: Fiber) {
if (!supportsPersistence) {
return;
}
const portal: {
containerInfo: Container,
pendingChildren: ChildSet,
...
} = current.stateNode;
const {containerInfo} = portal;
const emptyChildSet = createContainerChildSet(containerInfo);
replaceContainerChildren(containerInfo, emptyChildSet);
}
function commitContainer(finishedWork: Fiber) {
if (!supportsPersistence) {
return;
}
switch (finishedWork.tag) {
case ClassComponent:
case HostComponent:
case HostText:
case FundamentalComponent: {
return;
}
case HostRoot:
case HostPortal: {
const portalOrRoot: {
containerInfo: Container,
pendingChildren: ChildSet,
...
} = finishedWork.stateNode;
const {containerInfo, pendingChildren} = portalOrRoot;
replaceContainerChildren(containerInfo, pendingChildren);
return;
}
}
invariant(
false,
'This unit of work tag should not have side-effects. This error is ' +
'likely caused by a bug in React. Please file an issue.',
);
}
function getHostParentFiber(fiber: Fiber): Fiber {
let parent = fiber.return;
while (parent !== null) {
if (isHostParent(parent)) {
return parent;
}
parent = parent.return;
}
invariant(
false,
'Expected to find a host parent. This error is likely caused by a bug ' +
'in React. Please file an issue.',
);
}
function isHostParent(fiber: Fiber): boolean {
return (
fiber.tag === HostComponent ||
fiber.tag === HostRoot ||
fiber.tag === HostPortal
);
}
function getHostSibling(fiber: Fiber): ?Instance {
// We're going to search forward into the tree until we find a sibling host
// node. Unfortunately, if multiple insertions are done in a row we have to
// search past them. This leads to exponential search for the next sibling.
// TODO: Find a more efficient way to do this.
let node: Fiber = fiber;
siblings: while (true) {
// If we didn't find anything, let's try the next sibling.
while (node.sibling === null) {
if (node.return === null || isHostParent(node.return)) {
// If we pop out of the root or hit the parent the fiber we are the
// last sibling.
return null;
}
node = node.return;
}
node.sibling.return = node.return;
node = node.sibling;
while (
node.tag !== HostComponent &&
node.tag !== HostText &&
node.tag !== DehydratedFragment
) {
// If it is not host node and, we might have a host node inside it.
// Try to search down until we find one.
if (node.flags & Placement) {
// If we don't have a child, try the siblings instead.
continue siblings;
}
// If we don't have a child, try the siblings instead.
// We also skip portals because they are not part of this host tree.
if (node.child === null || node.tag === HostPortal) {
continue siblings;
} else {
node.child.return = node;
node = node.child;
}
}
// Check if this host node is stable or about to be placed.
if (!(node.flags & Placement)) {
// Found it!
return node.stateNode;
}
}
}
function commitPlacement(finishedWork: Fiber): void {
if (!supportsMutation) {
return;
}
// Recursively insert all host nodes into the parent.
const parentFiber = getHostParentFiber(finishedWork);
// Note: these two variables *must* always be updated together.
let parent;
let isContainer;
const parentStateNode = parentFiber.stateNode;
switch (parentFiber.tag) {
case HostComponent:
parent = parentStateNode;
isContainer = false;
break;
case HostRoot:
parent = parentStateNode.containerInfo;
isContainer = true;
break;
case HostPortal:
parent = parentStateNode.containerInfo;
isContainer = true;
break;
case FundamentalComponent:
if (enableFundamentalAPI) {
parent = parentStateNode.instance;
isContainer = false;
}
// eslint-disable-next-line-no-fallthrough
default:
invariant(
false,
'Invalid host parent fiber. This error is likely caused by a bug ' +
'in React. Please file an issue.',
);
}
if (parentFiber.flags & ContentReset) {
// Reset the text content of the parent before doing any insertions
resetTextContent(parent);
// Clear ContentReset from the effect tag
parentFiber.flags &= ~ContentReset;
}
const before = getHostSibling(finishedWork);
// We only have the top Fiber that was inserted but we need to recurse down its
// children to find all the terminal nodes.
if (isContainer) {
insertOrAppendPlacementNodeIntoContainer(finishedWork, before, parent);
} else {
insertOrAppendPlacementNode(finishedWork, before, parent);
}
}
function insertOrAppendPlacementNodeIntoContainer(
node: Fiber,
before: ?Instance,
parent: Container,
): void {
const {tag} = node;
const isHost = tag === HostComponent || tag === HostText;
if (isHost || (enableFundamentalAPI && tag === FundamentalComponent)) {
const stateNode = isHost ? node.stateNode : node.stateNode.instance;
if (before) {
insertInContainerBefore(parent, stateNode, before);
} else {
appendChildToContainer(parent, stateNode);
}
} else if (tag === HostPortal) {
// If the insertion itself is a portal, then we don't want to traverse
// down its children. Instead, we'll get insertions from each child in
// the portal directly.
} else {
const child = node.child;
if (child !== null) {
insertOrAppendPlacementNodeIntoContainer(child, before, parent);
let sibling = child.sibling;
while (sibling !== null) {
insertOrAppendPlacementNodeIntoContainer(sibling, before, parent);
sibling = sibling.sibling;
}
}
}
}
function insertOrAppendPlacementNode(
node: Fiber,
before: ?Instance,
parent: Instance,
): void {
const {tag} = node;
const isHost = tag === HostComponent || tag === HostText;
if (isHost || (enableFundamentalAPI && tag === FundamentalComponent)) {
const stateNode = isHost ? node.stateNode : node.stateNode.instance;
if (before) {
insertBefore(parent, stateNode, before);
} else {
appendChild(parent, stateNode);
}
} else if (tag === HostPortal) {
// If the insertion itself is a portal, then we don't want to traverse
// down its children. Instead, we'll get insertions from each child in
// the portal directly.
} else {
const child = node.child;
if (child !== null) {
insertOrAppendPlacementNode(child, before, parent);
let sibling = child.sibling;
while (sibling !== null) {
insertOrAppendPlacementNode(sibling, before, parent);
sibling = sibling.sibling;
}
}
}
}
function unmountHostComponents(
finishedRoot: FiberRoot,
current: Fiber,
renderPriorityLevel: ReactPriorityLevel,
): void {
// We only have the top Fiber that was deleted but we need to recurse down its
// children to find all the terminal nodes.
let node: Fiber = current;
// Each iteration, currentParent is populated with node's host parent if not
// currentParentIsValid.
let currentParentIsValid = false;
// Note: these two variables *must* always be updated together.
let currentParent;
let currentParentIsContainer;
while (true) {
if (!currentParentIsValid) {
let parent = node.return;
findParent: while (true) {
invariant(
parent !== null,
'Expected to find a host parent. This error is likely caused by ' +
'a bug in React. Please file an issue.',
);
const parentStateNode = parent.stateNode;
switch (parent.tag) {
case HostComponent:
currentParent = parentStateNode;
currentParentIsContainer = false;
break findParent;
case HostRoot:
currentParent = parentStateNode.containerInfo;
currentParentIsContainer = true;
break findParent;
case HostPortal:
currentParent = parentStateNode.containerInfo;
currentParentIsContainer = true;
break findParent;
case FundamentalComponent:
if (enableFundamentalAPI) {
currentParent = parentStateNode.instance;
currentParentIsContainer = false;
}
}
parent = parent.return;
}
currentParentIsValid = true;
}
if (node.tag === HostComponent || node.tag === HostText) {
commitNestedUnmounts(finishedRoot, node, renderPriorityLevel);
// After all the children have unmounted, it is now safe to remove the
// node from the tree.
if (currentParentIsContainer) {
removeChildFromContainer(
((currentParent: any): Container),
(node.stateNode: Instance | TextInstance),
);
} else {
removeChild(
((currentParent: any): Instance),
(node.stateNode: Instance | TextInstance),
);
}
// Don't visit children because we already visited them.
} else if (enableFundamentalAPI && node.tag === FundamentalComponent) {
const fundamentalNode = node.stateNode.instance;
commitNestedUnmounts(finishedRoot, node, renderPriorityLevel);
// After all the children have unmounted, it is now safe to remove the
// node from the tree.
if (currentParentIsContainer) {
removeChildFromContainer(
((currentParent: any): Container),
(fundamentalNode: Instance),
);
} else {
removeChild(
((currentParent: any): Instance),
(fundamentalNode: Instance),
);
}
} else if (
enableSuspenseServerRenderer &&
node.tag === DehydratedFragment
) {
if (enableSuspenseCallback) {
const hydrationCallbacks = finishedRoot.hydrationCallbacks;
if (hydrationCallbacks !== null) {
const onDeleted = hydrationCallbacks.onDeleted;
if (onDeleted) {
onDeleted((node.stateNode: SuspenseInstance));
}
}
}
// Delete the dehydrated suspense boundary and all of its content.
if (currentParentIsContainer) {
clearSuspenseBoundaryFromContainer(
((currentParent: any): Container),
(node.stateNode: SuspenseInstance),
);
} else {
clearSuspenseBoundary(
((currentParent: any): Instance),
(node.stateNode: SuspenseInstance),
);
}
} else if (node.tag === HostPortal) {
if (node.child !== null) {
// When we go into a portal, it becomes the parent to remove from.
// We will reassign it back when we pop the portal on the way up.
currentParent = node.stateNode.containerInfo;
currentParentIsContainer = true;
// Visit children because portals might contain host components.
node.child.return = node;
node = node.child;
continue;
}
} else {
commitUnmount(finishedRoot, node, renderPriorityLevel);
// Visit children because we may find more host components below.
if (node.child !== null) {
node.child.return = node;
node = node.child;
continue;
}
}
if (node === current) {
return;
}
while (node.sibling === null) {
if (node.return === null || node.return === current) {
return;
}
node = node.return;
if (node.tag === HostPortal) {
// When we go out of the portal, we need to restore the parent.
// Since we don't keep a stack of them, we will search for it.
currentParentIsValid = false;
}
}
node.sibling.return = node.return;
node = node.sibling;
}
}
function commitDeletion(
finishedRoot: FiberRoot,
current: Fiber,
renderPriorityLevel: ReactPriorityLevel,
): void {
if (supportsMutation) {
// Recursively delete all host nodes from the parent.
// Detach refs and call componentWillUnmount() on the whole subtree.
unmountHostComponents(finishedRoot, current, renderPriorityLevel);
} else {
// Detach refs and call componentWillUnmount() on the whole subtree.
commitNestedUnmounts(finishedRoot, current, renderPriorityLevel);
}
const alternate = current.alternate;
detachFiberMutation(current);
if (alternate !== null) {
detachFiberMutation(alternate);
}
}
function commitWork(current: Fiber | null, finishedWork: Fiber): void {
if (!supportsMutation) {
switch (finishedWork.tag) {
case FunctionComponent:
case ForwardRef:
case MemoComponent:
case SimpleMemoComponent:
case Block: {
// Layout effects are destroyed during the mutation phase so that all
// destroy functions for all fibers are called before any create functions.
// This prevents sibling component effects from interfering with each other,
// e.g. a destroy function in one component should never override a ref set
// by a create function in another component during the same commit.
if (
enableProfilerTimer &&
enableProfilerCommitHooks &&
finishedWork.mode & ProfileMode
) {
try {
startLayoutEffectTimer();
commitHookEffectListUnmount(
HookLayout | HookHasEffect,
finishedWork,
);
} finally {
recordLayoutEffectDuration(finishedWork);
}
} else {
commitHookEffectListUnmount(HookLayout | HookHasEffect, finishedWork);
}
return;
}
case Profiler: {
return;
}
case SuspenseComponent: {
commitSuspenseComponent(finishedWork);
attachSuspenseRetryListeners(finishedWork);
return;
}
case SuspenseListComponent: {
attachSuspenseRetryListeners(finishedWork);
return;
}
case HostRoot: {
if (supportsHydration) {
const root: FiberRoot = finishedWork.stateNode;
if (root.hydrate) {
// We've just hydrated. No need to hydrate again.
root.hydrate = false;
commitHydratedContainer(root.containerInfo);
}
}
break;
}
case OffscreenComponent:
case LegacyHiddenComponent: {
return;
}
}
commitContainer(finishedWork);
return;
}
switch (finishedWork.tag) {
case FunctionComponent:
case ForwardRef:
case MemoComponent:
case SimpleMemoComponent:
case Block: {
// Layout effects are destroyed during the mutation phase so that all
// destroy functions for all fibers are called before any create functions.
// This prevents sibling component effects from interfering with each other,
// e.g. a destroy function in one component should never override a ref set
// by a create function in another component during the same commit.
if (
enableProfilerTimer &&
enableProfilerCommitHooks &&
finishedWork.mode & ProfileMode
) {
try {
startLayoutEffectTimer();
commitHookEffectListUnmount(HookLayout | HookHasEffect, finishedWork);
} finally {
recordLayoutEffectDuration(finishedWork);
}
} else {
commitHookEffectListUnmount(HookLayout | HookHasEffect, finishedWork);
}
return;
}
case ClassComponent: {
return;
}
case HostComponent: {
const instance: Instance = finishedWork.stateNode;
if (instance != null) {
// Commit the work prepared earlier.
const newProps = finishedWork.memoizedProps;
// For hydration we reuse the update path but we treat the oldProps
// as the newProps. The updatePayload will contain the real change in
// this case.
const oldProps = current !== null ? current.memoizedProps : newProps;
const type = finishedWork.type;
// TODO: Type the updateQueue to be specific to host components.
const updatePayload: null | UpdatePayload = (finishedWork.updateQueue: any);
finishedWork.updateQueue = null;
if (updatePayload !== null) {
commitUpdate(
instance,
updatePayload,
type,
oldProps,
newProps,
finishedWork,
);
}
}
return;
}
case HostText: {
invariant(
finishedWork.stateNode !== null,
'This should have a text node initialized. This error is likely ' +
'caused by a bug in React. Please file an issue.',
);
const textInstance: TextInstance = finishedWork.stateNode;
const newText: string = finishedWork.memoizedProps;
// For hydration we reuse the update path but we treat the oldProps
// as the newProps. The updatePayload will contain the real change in
// this case.
const oldText: string =
current !== null ? current.memoizedProps : newText;
commitTextUpdate(textInstance, oldText, newText);
return;
}
case HostRoot: {
if (supportsHydration) {
const root: FiberRoot = finishedWork.stateNode;
if (root.hydrate) {
// We've just hydrated. No need to hydrate again.
root.hydrate = false;
commitHydratedContainer(root.containerInfo);
}
}
return;
}
case Profiler: {
return;
}
case SuspenseComponent: {
commitSuspenseComponent(finishedWork);
attachSuspenseRetryListeners(finishedWork);
return;
}
case SuspenseListComponent: {
attachSuspenseRetryListeners(finishedWork);
return;
}
case IncompleteClassComponent: {
return;
}
case FundamentalComponent: {
if (enableFundamentalAPI) {
const fundamentalInstance = finishedWork.stateNode;
updateFundamentalComponent(fundamentalInstance);
return;
}
break;
}
case ScopeComponent: {
if (enableScopeAPI) {
const scopeInstance = finishedWork.stateNode;
prepareScopeUpdate(scopeInstance, finishedWork);
return;
}
break;
}
case OffscreenComponent:
case LegacyHiddenComponent: {
const newState: OffscreenState | null = finishedWork.memoizedState;
const isHidden = newState !== null;
hideOrUnhideAllChildren(finishedWork, isHidden);
return;
}
}
invariant(
false,
'This unit of work tag should not have side-effects. This error is ' +
'likely caused by a bug in React. Please file an issue.',
);
}
function commitSuspenseComponent(finishedWork: Fiber) {
const newState: SuspenseState | null = finishedWork.memoizedState;
if (newState !== null) {
markCommitTimeOfFallback();
if (supportsMutation) {
// Hide the Offscreen component that contains the primary children. TODO:
// Ideally, this effect would have been scheduled on the Offscreen fiber
// itself. That's how unhiding works: the Offscreen component schedules an
// effect on itself. However, in this case, the component didn't complete,
// so the fiber was never added to the effect list in the normal path. We
// could have appended it to the effect list in the Suspense component's
// second pass, but doing it this way is less complicated. This would be
// simpler if we got rid of the effect list and traversed the tree, like
// we're planning to do.
const primaryChildParent: Fiber = (finishedWork.child: any);
hideOrUnhideAllChildren(primaryChildParent, true);
}
}
if (enableSuspenseCallback && newState !== null) {
const suspenseCallback = finishedWork.memoizedProps.suspenseCallback;
if (typeof suspenseCallback === 'function') {
const wakeables: Set<Wakeable> | null = (finishedWork.updateQueue: any);
if (wakeables !== null) {
suspenseCallback(new Set(wakeables));
}
} else if (__DEV__) {
if (suspenseCallback !== undefined) {
console.error('Unexpected type for suspenseCallback.');
}
}
}
}
function commitSuspenseHydrationCallbacks(
finishedRoot: FiberRoot,
finishedWork: Fiber,
) {
if (!supportsHydration) {
return;
}
const newState: SuspenseState | null = finishedWork.memoizedState;
if (newState === null) {
const current = finishedWork.alternate;
if (current !== null) {
const prevState: SuspenseState | null = current.memoizedState;
if (prevState !== null) {
const suspenseInstance = prevState.dehydrated;
if (suspenseInstance !== null) {
commitHydratedSuspenseInstance(suspenseInstance);
if (enableSuspenseCallback) {
const hydrationCallbacks = finishedRoot.hydrationCallbacks;
if (hydrationCallbacks !== null) {
const onHydrated = hydrationCallbacks.onHydrated;
if (onHydrated) {
onHydrated(suspenseInstance);
}
}
}
}
}
}
}
}
function attachSuspenseRetryListeners(finishedWork: Fiber) {
// If this boundary just timed out, then it will have a set of wakeables.
// For each wakeable, attach a listener so that when it resolves, React
// attempts to re-render the boundary in the primary (pre-timeout) state.
const wakeables: Set<Wakeable> | null = (finishedWork.updateQueue: any);
if (wakeables !== null) {
finishedWork.updateQueue = null;
let retryCache = finishedWork.stateNode;
if (retryCache === null) {
retryCache = finishedWork.stateNode = new PossiblyWeakSet();
}
wakeables.forEach(wakeable => {
// Memoize using the boundary fiber to prevent redundant listeners.
let retry = resolveRetryWakeable.bind(null, finishedWork, wakeable);
if (!retryCache.has(wakeable)) {
if (enableSchedulerTracing) {
if (wakeable.__reactDoNotTraceInteractions !== true) {
retry = Schedule_tracing_wrap(retry);
}
}
retryCache.add(wakeable);
wakeable.then(retry, retry);
}
});
}
}
// This function detects when a Suspense boundary goes from visible to hidden.
// It returns false if the boundary is already hidden.
// TODO: Use an effect tag.
export function isSuspenseBoundaryBeingHidden(
current: Fiber | null,
finishedWork: Fiber,
): boolean {
if (current !== null) {
const oldState: SuspenseState | null = current.memoizedState;
if (oldState === null || oldState.dehydrated !== null) {
const newState: SuspenseState | null = finishedWork.memoizedState;
return newState !== null && newState.dehydrated === null;
}
}
return false;
}
function commitResetTextContent(current: Fiber) {
if (!supportsMutation) {
return;
}
resetTextContent(current.stateNode);
}
export {
commitBeforeMutationLifeCycles,
commitResetTextContent,
commitPlacement,
commitDeletion,
commitWork,
commitLifeCycles,
commitAttachRef,
commitDetachRef,
};