mirror of
https://github.com/facebook/react.git
synced 2025-11-01 09:12:30 +00:00
44d39c4d76
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.
1755 lines
56 KiB
JavaScript
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,
|
|
};
|