/**
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
* @flow
*/
import type {
Instance,
TextInstance,
SuspenseInstance,
Container,
ChildSet,
UpdatePayload,
HoistableRoot,
} from './ReactFiberConfig';
import type {Fiber, FiberRoot} from './ReactInternalTypes';
import type {Lanes} from './ReactFiberLane';
import {SyncLane} from './ReactFiberLane';
import type {SuspenseState, RetryQueue} from './ReactFiberSuspenseComponent';
import type {UpdateQueue} from './ReactFiberClassUpdateQueue';
import type {FunctionComponentUpdateQueue} from './ReactFiberHooks';
import type {Wakeable} from 'shared/ReactTypes';
import {isOffscreenManual} from './ReactFiberActivityComponent';
import type {
OffscreenState,
OffscreenInstance,
OffscreenQueue,
OffscreenProps,
} from './ReactFiberActivityComponent';
import type {HookFlags} from './ReactHookEffectTags';
import type {Cache} from './ReactFiberCacheComponent';
import type {RootState} from './ReactFiberRoot';
import type {
Transition,
TracingMarkerInstance,
TransitionAbort,
} from './ReactFiberTracingMarkerComponent';
import {
enableCreateEventHandleAPI,
enableProfilerTimer,
enableProfilerCommitHooks,
enableProfilerNestedUpdatePhase,
enableSchedulingProfiler,
enableSuspenseCallback,
enableScopeAPI,
enableUpdaterTracking,
enableCache,
enableTransitionTracing,
enableUseEffectEventHook,
enableFloat,
enableLegacyHidden,
enableHostSingletons,
alwaysThrottleRetries,
} from 'shared/ReactFeatureFlags';
import {
FunctionComponent,
ForwardRef,
ClassComponent,
HostRoot,
HostComponent,
HostHoistable,
HostSingleton,
HostText,
HostPortal,
Profiler,
SuspenseComponent,
DehydratedFragment,
IncompleteClassComponent,
MemoComponent,
SimpleMemoComponent,
SuspenseListComponent,
ScopeComponent,
OffscreenComponent,
LegacyHiddenComponent,
CacheComponent,
TracingMarkerComponent,
} from './ReactWorkTags';
import {
NoFlags,
ContentReset,
Placement,
ChildDeletion,
Snapshot,
Update,
Callback,
Ref,
Hydrating,
Passive,
BeforeMutationMask,
MutationMask,
LayoutMask,
PassiveMask,
Visibility,
ShouldSuspendCommit,
MaySuspendCommit,
} from './ReactFiberFlags';
import getComponentNameFromFiber from 'react-reconciler/src/getComponentNameFromFiber';
import {
resetCurrentFiber as resetCurrentDebugFiberInDEV,
setCurrentFiber as setCurrentDebugFiberInDEV,
getCurrentFiber as getCurrentDebugFiberInDEV,
} from './ReactCurrentFiber';
import {resolveDefaultProps} from './ReactFiberLazyComponent';
import {
isCurrentUpdateNested,
getCommitTime,
recordLayoutEffectDuration,
startLayoutEffectTimer,
recordPassiveEffectDuration,
startPassiveEffectTimer,
} from './ReactProfilerTimer';
import {ConcurrentMode, NoMode, ProfileMode} from './ReactTypeOfMode';
import {
deferHiddenCallbacks,
commitHiddenCallbacks,
commitCallbacks,
} from './ReactFiberClassUpdateQueue';
import {
getPublicInstance,
supportsMutation,
supportsPersistence,
supportsHydration,
supportsResources,
supportsSingletons,
commitMount,
commitUpdate,
resetTextContent,
commitTextUpdate,
appendChild,
appendChildToContainer,
insertBefore,
insertInContainerBefore,
removeChild,
removeChildFromContainer,
clearSuspenseBoundary,
clearSuspenseBoundaryFromContainer,
replaceContainerChildren,
createContainerChildSet,
hideInstance,
hideTextInstance,
unhideInstance,
unhideTextInstance,
commitHydratedContainer,
commitHydratedSuspenseInstance,
clearContainer,
prepareScopeUpdate,
prepareForCommit,
beforeActiveInstanceBlur,
detachDeletedInstance,
clearSingleton,
acquireSingletonInstance,
releaseSingletonInstance,
getHoistableRoot,
acquireResource,
releaseResource,
hydrateHoistable,
mountHoistable,
unmountHoistable,
prepareToCommitHoistables,
suspendInstance,
suspendResource,
} from './ReactFiberConfig';
import {
captureCommitPhaseError,
resolveRetryWakeable,
markCommitTimeOfFallback,
enqueuePendingPassiveProfilerEffect,
restorePendingUpdaters,
addTransitionStartCallbackToPendingTransition,
addTransitionProgressCallbackToPendingTransition,
addTransitionCompleteCallbackToPendingTransition,
addMarkerProgressCallbackToPendingTransition,
addMarkerIncompleteCallbackToPendingTransition,
addMarkerCompleteCallbackToPendingTransition,
setIsRunningInsertionEffect,
getExecutionContext,
CommitContext,
NoContext,
} from './ReactFiberWorkLoop';
import {
NoFlags as NoHookEffect,
HasEffect as HookHasEffect,
Layout as HookLayout,
Insertion as HookInsertion,
Passive as HookPassive,
} from './ReactHookEffectTags';
import {didWarnAboutReassigningProps} from './ReactFiberBeginWork';
import {doesFiberContain} from './ReactFiberTreeReflection';
import {invokeGuardedCallback, clearCaughtError} from 'shared/ReactErrorUtils';
import {
isDevToolsPresent,
markComponentPassiveEffectMountStarted,
markComponentPassiveEffectMountStopped,
markComponentPassiveEffectUnmountStarted,
markComponentPassiveEffectUnmountStopped,
markComponentLayoutEffectMountStarted,
markComponentLayoutEffectMountStopped,
markComponentLayoutEffectUnmountStarted,
markComponentLayoutEffectUnmountStopped,
onCommitUnmount,
} from './ReactFiberDevToolsHook';
import {releaseCache, retainCache} from './ReactFiberCacheComponent';
import {clearTransitionsForLanes} from './ReactFiberLane';
import {
OffscreenVisible,
OffscreenDetached,
OffscreenPassiveEffectsConnected,
} from './ReactFiberActivityComponent';
import {
TransitionRoot,
TransitionTracingMarker,
} from './ReactFiberTracingMarkerComponent';
import {scheduleUpdateOnFiber} from './ReactFiberWorkLoop';
import {enqueueConcurrentRenderForLane} from './ReactFiberConcurrentUpdates';
let didWarnAboutUndefinedSnapshotBeforeUpdate: Set<mixed> | null = null;
if (__DEV__) {
didWarnAboutUndefinedSnapshotBeforeUpdate = new Set();
}
// Used during the commit phase to track the state of the Offscreen component stack.
// Allows us to avoid traversing the return path to find the nearest Offscreen ancestor.
let offscreenSubtreeIsHidden: boolean = false;
let offscreenSubtreeWasHidden: boolean = false;
const PossiblyWeakSet = typeof WeakSet === 'function' ? WeakSet : Set;
let nextEffect: Fiber | null = null;
// Used for Profiling builds to track updaters.
let inProgressLanes: Lanes | null = null;
let inProgressRoot: FiberRoot | null = null;
function shouldProfile(current: Fiber): boolean {
return (
enableProfilerTimer &&
enableProfilerCommitHooks &&
(current.mode & ProfileMode) !== NoMode &&
(getExecutionContext() & CommitContext) !== NoContext
);
}
export function reportUncaughtErrorInDEV(error: mixed) {
// Wrapping each small part of the commit phase into a guarded
// callback is a bit too slow (https://github.com/facebook/react/pull/21666).
// But we rely on it to surface errors to DEV tools like overlays
// (https://github.com/facebook/react/issues/21712).
// As a compromise, rethrow only caught errors in a guard.
if (__DEV__) {
invokeGuardedCallback(null, () => {
throw error;
});
clearCaughtError();
}
}
function callComponentWillUnmountWithTimer(current: Fiber, instance: any) {
instance.props = current.memoizedProps;
instance.state = current.memoizedState;
if (shouldProfile(current)) {
try {
startLayoutEffectTimer();
instance.componentWillUnmount();
} finally {
recordLayoutEffectDuration(current);
}
} else {
instance.componentWillUnmount();
}
}
// Capture errors so they don't interrupt unmounting.
function safelyCallComponentWillUnmount(
current: Fiber,
nearestMountedAncestor: Fiber | null,
instance: any,
) {
try {
callComponentWillUnmountWithTimer(current, instance);
} catch (error) {
captureCommitPhaseError(current, nearestMountedAncestor, error);
}
}
// Capture errors so they don't interrupt mounting.
function safelyAttachRef(current: Fiber, nearestMountedAncestor: Fiber | null) {
try {
commitAttachRef(current);
} catch (error) {
captureCommitPhaseError(current, nearestMountedAncestor, error);
}
}
function safelyDetachRef(current: Fiber, nearestMountedAncestor: Fiber | null) {
const ref = current.ref;
const refCleanup = current.refCleanup;
if (ref !== null) {
if (typeof refCleanup === 'function') {
try {
if (shouldProfile(current)) {
try {
startLayoutEffectTimer();
refCleanup();
} finally {
recordLayoutEffectDuration(current);
}
} else {
refCleanup();
}
} catch (error) {
captureCommitPhaseError(current, nearestMountedAncestor, error);
} finally {
// `refCleanup` has been called. Nullify all references to it to prevent double invocation.
current.refCleanup = null;
const finishedWork = current.alternate;
if (finishedWork != null) {
finishedWork.refCleanup = null;
}
}
} else if (typeof ref === 'function') {
let retVal;
try {
if (shouldProfile(current)) {
try {
startLayoutEffectTimer();
retVal = ref(null);
} finally {
recordLayoutEffectDuration(current);
}
} else {
retVal = ref(null);
}
} catch (error) {
captureCommitPhaseError(current, nearestMountedAncestor, error);
}
if (__DEV__) {
if (typeof retVal === 'function') {
console.error(
'Unexpected return value from a callback ref in %s. ' +
'A callback ref should not return a function.',
getComponentNameFromFiber(current),
);
}
}
} else {
// $FlowFixMe[incompatible-use] unable to narrow type to RefObject
ref.current = null;
}
}
}
function safelyCallDestroy(
current: Fiber,
nearestMountedAncestor: Fiber | null,
destroy: () => void,
) {
try {
destroy();
} catch (error) {
captureCommitPhaseError(current, nearestMountedAncestor, error);
}
}
let focusedInstanceHandle: null | Fiber = null;
let shouldFireAfterActiveInstanceBlur: boolean = false;
export function commitBeforeMutationEffects(
root: FiberRoot,
firstChild: Fiber,
): boolean {
focusedInstanceHandle = prepareForCommit(root.containerInfo);
nextEffect = firstChild;
commitBeforeMutationEffects_begin();
// We no longer need to track the active instance fiber
const shouldFire = shouldFireAfterActiveInstanceBlur;
shouldFireAfterActiveInstanceBlur = false;
focusedInstanceHandle = null;
return shouldFire;
}
function commitBeforeMutationEffects_begin() {
while (nextEffect !== null) {
const fiber = nextEffect;
// This phase is only used for beforeActiveInstanceBlur.
// Let's skip the whole loop if it's off.
if (enableCreateEventHandleAPI) {
// TODO: Should wrap this in flags check, too, as optimization
const deletions = fiber.deletions;
if (deletions !== null) {
for (let i = 0; i < deletions.length; i++) {
const deletion = deletions[i];
commitBeforeMutationEffectsDeletion(deletion);
}
}
}
const child = fiber.child;
if (
(fiber.subtreeFlags & BeforeMutationMask) !== NoFlags &&
child !== null
) {
child.return = fiber;
nextEffect = child;
} else {
commitBeforeMutationEffects_complete();
}
}
}
function commitBeforeMutationEffects_complete() {
while (nextEffect !== null) {
const fiber = nextEffect;
setCurrentDebugFiberInDEV(fiber);
try {
commitBeforeMutationEffectsOnFiber(fiber);
} catch (error) {
captureCommitPhaseError(fiber, fiber.return, error);
}
resetCurrentDebugFiberInDEV();
const sibling = fiber.sibling;
if (sibling !== null) {
sibling.return = fiber.return;
nextEffect = sibling;
return;
}
nextEffect = fiber.return;
}
}
function commitBeforeMutationEffectsOnFiber(finishedWork: Fiber) {
const current = finishedWork.alternate;
const flags = finishedWork.flags;
if (enableCreateEventHandleAPI) {
if (!shouldFireAfterActiveInstanceBlur && focusedInstanceHandle !== null) {
// Check to see if the focused element was inside of a hidden (Suspense) subtree.
// TODO: Move this out of the hot path using a dedicated effect tag.
if (
finishedWork.tag === SuspenseComponent &&
isSuspenseBoundaryBeingHidden(current, finishedWork) &&
// $FlowFixMe[incompatible-call] found when upgrading Flow
doesFiberContain(finishedWork, focusedInstanceHandle)
) {
shouldFireAfterActiveInstanceBlur = true;
beforeActiveInstanceBlur(finishedWork);
}
}
}
if ((flags & Snapshot) !== NoFlags) {
setCurrentDebugFiberInDEV(finishedWork);
}
switch (finishedWork.tag) {
case FunctionComponent: {
if (enableUseEffectEventHook) {
if ((flags & Update) !== NoFlags) {
commitUseEffectEventMount(finishedWork);
}
}
break;
}
case ForwardRef:
case SimpleMemoComponent: {
break;
}
case ClassComponent: {
if ((flags & Snapshot) !== NoFlags) {
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.',
getComponentNameFromFiber(finishedWork) || '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.',
getComponentNameFromFiber(finishedWork) || '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.',
getComponentNameFromFiber(finishedWork),
);
}
}
instance.__reactInternalSnapshotBeforeUpdate = snapshot;
}
}
break;
}
case HostRoot: {
if ((flags & Snapshot) !== NoFlags) {
if (supportsMutation) {
const root = finishedWork.stateNode;
clearContainer(root.containerInfo);
}
}
break;
}
case HostComponent:
case HostHoistable:
case HostSingleton:
case HostText:
case HostPortal:
case IncompleteClassComponent:
// Nothing to do for these component types
break;
default: {
if ((flags & Snapshot) !== NoFlags) {
throw new Error(
'This unit of work tag should not have side-effects. This error is ' +
'likely caused by a bug in React. Please file an issue.',
);
}
}
}
if ((flags & Snapshot) !== NoFlags) {
resetCurrentDebugFiberInDEV();
}
}
function commitBeforeMutationEffectsDeletion(deletion: Fiber) {
if (enableCreateEventHandleAPI) {
// TODO (effects) It would be nice to avoid calling doesFiberContain()
// Maybe we can repurpose one of the subtreeFlags positions for this instead?
// Use it to store which part of the tree the focused instance is in?
// This assumes we can safely determine that instance during the "render" phase.
if (doesFiberContain(deletion, ((focusedInstanceHandle: any): Fiber))) {
shouldFireAfterActiveInstanceBlur = true;
beforeActiveInstanceBlur(deletion);
}
}
}
function commitHookEffectListUnmount(
flags: HookFlags,
finishedWork: Fiber,
nearestMountedAncestor: Fiber | null,
) {
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 & flags) === flags) {
// Unmount
const inst = effect.inst;
const destroy = inst.destroy;
if (destroy !== undefined) {
inst.destroy = undefined;
if (enableSchedulingProfiler) {
if ((flags & HookPassive) !== NoHookEffect) {
markComponentPassiveEffectUnmountStarted(finishedWork);
} else if ((flags & HookLayout) !== NoHookEffect) {
markComponentLayoutEffectUnmountStarted(finishedWork);
}
}
if (__DEV__) {
if ((flags & HookInsertion) !== NoHookEffect) {
setIsRunningInsertionEffect(true);
}
}
safelyCallDestroy(finishedWork, nearestMountedAncestor, destroy);
if (__DEV__) {
if ((flags & HookInsertion) !== NoHookEffect) {
setIsRunningInsertionEffect(false);
}
}
if (enableSchedulingProfiler) {
if ((flags & HookPassive) !== NoHookEffect) {
markComponentPassiveEffectUnmountStopped();
} else if ((flags & HookLayout) !== NoHookEffect) {
markComponentLayoutEffectUnmountStopped();
}
}
}
}
effect = effect.next;
} while (effect !== firstEffect);
}
}
function commitHookEffectListMount(flags: HookFlags, 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 & flags) === flags) {
if (enableSchedulingProfiler) {
if ((flags & HookPassive) !== NoHookEffect) {
markComponentPassiveEffectMountStarted(finishedWork);
} else if ((flags & HookLayout) !== NoHookEffect) {
markComponentLayoutEffectMountStarted(finishedWork);
}
}
// Mount
const create = effect.create;
if (__DEV__) {
if ((flags & HookInsertion) !== NoHookEffect) {
setIsRunningInsertionEffect(true);
}
}
const inst = effect.inst;
const destroy = create();
inst.destroy = destroy;
if (__DEV__) {
if ((flags & HookInsertion) !== NoHookEffect) {
setIsRunningInsertionEffect(false);
}
}
if (enableSchedulingProfiler) {
if ((flags & HookPassive) !== NoHookEffect) {
markComponentPassiveEffectMountStopped();
} else if ((flags & HookLayout) !== NoHookEffect) {
markComponentLayoutEffectMountStopped();
}
}
if (__DEV__) {
if (destroy !== undefined && typeof destroy !== 'function') {
let hookName;
if ((effect.tag & HookLayout) !== NoFlags) {
hookName = 'useLayoutEffect';
} else if ((effect.tag & HookInsertion) !== NoFlags) {
hookName = 'useInsertionEffect';
} else {
hookName = 'useEffect';
}
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 ' +
hookName +
'(async () => ...) or returned a Promise. ' +
'Instead, write the async function inside your effect ' +
'and call it immediately:\n\n' +
hookName +
'(() => {\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(
'%s must not return anything besides a function, ' +
'which is used for clean-up.%s',
hookName,
addendum,
);
}
}
}
effect = effect.next;
} while (effect !== firstEffect);
}
}
function commitUseEffectEventMount(finishedWork: Fiber) {
const updateQueue: FunctionComponentUpdateQueue | null =
(finishedWork.updateQueue: any);
const eventPayloads = updateQueue !== null ? updateQueue.events : null;
if (eventPayloads !== null) {
for (let ii = 0; ii < eventPayloads.length; ii++) {
const {ref, nextImpl} = eventPayloads[ii];
ref.impl = nextImpl;
}
}
}
export function commitPassiveEffectDurations(
finishedRoot: FiberRoot,
finishedWork: Fiber,
): void {
if (
enableProfilerTimer &&
enableProfilerCommitHooks &&
getExecutionContext() & CommitContext
) {
// 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();
let phase = finishedWork.alternate === null ? 'mount' : 'update';
if (enableProfilerNestedUpdatePhase) {
if (isCurrentUpdateNested()) {
phase = 'nested-update';
}
}
if (typeof onPostCommit === 'function') {
onPostCommit(id, phase, passiveEffectDuration, commitTime);
}
// Bubble times to the next nearest ancestor Profiler.
// After we process that Profiler, we'll bubble further up.
let parentFiber = finishedWork.return;
outer: while (parentFiber !== null) {
switch (parentFiber.tag) {
case HostRoot:
const root = parentFiber.stateNode;
root.passiveEffectDuration += passiveEffectDuration;
break outer;
case Profiler:
const parentStateNode = parentFiber.stateNode;
parentStateNode.passiveEffectDuration += passiveEffectDuration;
break outer;
}
parentFiber = parentFiber.return;
}
break;
}
default:
break;
}
}
}
}
function commitHookLayoutEffects(finishedWork: Fiber, hookFlags: HookFlags) {
// 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 (shouldProfile(finishedWork)) {
try {
startLayoutEffectTimer();
commitHookEffectListMount(hookFlags, finishedWork);
} catch (error) {
captureCommitPhaseError(finishedWork, finishedWork.return, error);
}
recordLayoutEffectDuration(finishedWork);
} else {
try {
commitHookEffectListMount(hookFlags, finishedWork);
} catch (error) {
captureCommitPhaseError(finishedWork, finishedWork.return, error);
}
}
}
function commitClassLayoutLifecycles(
finishedWork: Fiber,
current: Fiber | null,
) {
const instance = finishedWork.stateNode;
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.',
getComponentNameFromFiber(finishedWork) || '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.',
getComponentNameFromFiber(finishedWork) || 'instance',
);
}
}
}
if (shouldProfile(finishedWork)) {
try {
startLayoutEffectTimer();
instance.componentDidMount();
} catch (error) {
captureCommitPhaseError(finishedWork, finishedWork.return, error);
}
recordLayoutEffectDuration(finishedWork);
} else {
try {
instance.componentDidMount();
} catch (error) {
captureCommitPhaseError(finishedWork, finishedWork.return, error);
}
}
} 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.',
getComponentNameFromFiber(finishedWork) || '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.',
getComponentNameFromFiber(finishedWork) || 'instance',
);
}
}
}
if (shouldProfile(finishedWork)) {
try {
startLayoutEffectTimer();
instance.componentDidUpdate(
prevProps,
prevState,
instance.__reactInternalSnapshotBeforeUpdate,
);
} catch (error) {
captureCommitPhaseError(finishedWork, finishedWork.return, error);
}
recordLayoutEffectDuration(finishedWork);
} else {
try {
instance.componentDidUpdate(
prevProps,
prevState,
instance.__reactInternalSnapshotBeforeUpdate,
);
} catch (error) {
captureCommitPhaseError(finishedWork, finishedWork.return, error);
}
}
}
}
function commitClassCallbacks(finishedWork: Fiber) {
// 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<mixed> | null =
(finishedWork.updateQueue: any);
if (updateQueue !== null) {
const instance = finishedWork.stateNode;
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.',
getComponentNameFromFiber(finishedWork) || '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.',
getComponentNameFromFiber(finishedWork) || '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.
try {
commitCallbacks(updateQueue, instance);
} catch (error) {
captureCommitPhaseError(finishedWork, finishedWork.return, error);
}
}
}
function commitHostComponentMount(finishedWork: Fiber) {
const type = finishedWork.type;
const props = finishedWork.memoizedProps;
const instance: Instance = finishedWork.stateNode;
try {
commitMount(instance, type, props, finishedWork);
} catch (error) {
captureCommitPhaseError(finishedWork, finishedWork.return, error);
}
}
function commitProfilerUpdate(finishedWork: Fiber, current: Fiber | null) {
if (enableProfilerTimer && getExecutionContext() & CommitContext) {
try {
const {onCommit, onRender} = finishedWork.memoizedProps;
const {effectDuration} = finishedWork.stateNode;
const commitTime = getCommitTime();
let phase = current === null ? 'mount' : 'update';
if (enableProfilerNestedUpdatePhase) {
if (isCurrentUpdateNested()) {
phase = 'nested-update';
}
}
if (typeof onRender === 'function') {
onRender(
finishedWork.memoizedProps.id,
phase,
finishedWork.actualDuration,
finishedWork.treeBaseDuration,
finishedWork.actualStartTime,
commitTime,
);
}
if (enableProfilerCommitHooks) {
if (typeof onCommit === 'function') {
onCommit(
finishedWork.memoizedProps.id,
phase,
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;
outer: while (parentFiber !== null) {
switch (parentFiber.tag) {
case HostRoot:
const root = parentFiber.stateNode;
root.effectDuration += effectDuration;
break outer;
case Profiler:
const parentStateNode = parentFiber.stateNode;
parentStateNode.effectDuration += effectDuration;
break outer;
}
parentFiber = parentFiber.return;
}
}
} catch (error) {
captureCommitPhaseError(finishedWork, finishedWork.return, error);
}
}
}
function commitLayoutEffectOnFiber(
finishedRoot: FiberRoot,
current: Fiber | null,
finishedWork: Fiber,
committedLanes: Lanes,
): void {
// When updating this function, also update reappearLayoutEffects, which does
// most of the same things when an offscreen tree goes from hidden -> visible.
const flags = finishedWork.flags;
switch (finishedWork.tag) {
case FunctionComponent:
case ForwardRef:
case SimpleMemoComponent: {
recursivelyTraverseLayoutEffects(
finishedRoot,
finishedWork,
committedLanes,
);
if (flags & Update) {
commitHookLayoutEffects(finishedWork, HookLayout | HookHasEffect);
}
break;
}
case ClassComponent: {
recursivelyTraverseLayoutEffects(
finishedRoot,
finishedWork,
committedLanes,
);
if (flags & Update) {
commitClassLayoutLifecycles(finishedWork, current);
}
if (flags & Callback) {
commitClassCallbacks(finishedWork);
}
if (flags & Ref) {
safelyAttachRef(finishedWork, finishedWork.return);
}
break;
}
case HostRoot: {
recursivelyTraverseLayoutEffects(
finishedRoot,
finishedWork,
committedLanes,
);
if (flags & Callback) {
// 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<mixed> | null =
(finishedWork.updateQueue: any);
if (updateQueue !== null) {
let instance = null;
if (finishedWork.child !== null) {
switch (finishedWork.child.tag) {
case HostSingleton:
case HostComponent:
instance = getPublicInstance(finishedWork.child.stateNode);
break;
case ClassComponent:
instance = finishedWork.child.stateNode;
break;
}
}
try {
commitCallbacks(updateQueue, instance);
} catch (error) {
captureCommitPhaseError(finishedWork, finishedWork.return, error);
}
}
}
break;
}
case HostHoistable: {
if (enableFloat && supportsResources) {
recursivelyTraverseLayoutEffects(
finishedRoot,
finishedWork,
committedLanes,
);
if (flags & Ref) {
safelyAttachRef(finishedWork, finishedWork.return);
}
break;
}
// Fall through
}
case HostSingleton:
case HostComponent: {
recursivelyTraverseLayoutEffects(
finishedRoot,
finishedWork,
committedLanes,
);
// 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 && flags & Update) {
commitHostComponentMount(finishedWork);
}
if (flags & Ref) {
safelyAttachRef(finishedWork, finishedWork.return);
}
break;
}
case Profiler: {
recursivelyTraverseLayoutEffects(
finishedRoot,
finishedWork,
committedLanes,
);
// TODO: Should this fire inside an offscreen tree? Or should it wait to
// fire when the tree becomes visible again.
if (flags & Update) {
commitProfilerUpdate(finishedWork, current);
}
break;
}
case SuspenseComponent: {
recursivelyTraverseLayoutEffects(
finishedRoot,
finishedWork,
committedLanes,
);
if (flags & Update) {
commitSuspenseHydrationCallbacks(finishedRoot, finishedWork);
}
break;
}
case OffscreenComponent: {
const isModernRoot = (finishedWork.mode & ConcurrentMode) !== NoMode;
if (isModernRoot) {
const isHidden = finishedWork.memoizedState !== null;
const newOffscreenSubtreeIsHidden =
isHidden || offscreenSubtreeIsHidden;
if (newOffscreenSubtreeIsHidden) {
// The Offscreen tree is hidden. Skip over its layout effects.
} else {
// The Offscreen tree is visible.
const wasHidden = current !== null && current.memoizedState !== null;
const newOffscreenSubtreeWasHidden =
wasHidden || offscreenSubtreeWasHidden;
const prevOffscreenSubtreeIsHidden = offscreenSubtreeIsHidden;
const prevOffscreenSubtreeWasHidden = offscreenSubtreeWasHidden;
offscreenSubtreeIsHidden = newOffscreenSubtreeIsHidden;
offscreenSubtreeWasHidden = newOffscreenSubtreeWasHidden;
if (offscreenSubtreeWasHidden && !prevOffscreenSubtreeWasHidden) {
// This is the root of a reappearing boundary. As we continue
// traversing the layout effects, we must also re-mount layout
// effects that were unmounted when the Offscreen subtree was
// hidden. So this is a superset of the normal commitLayoutEffects.
const includeWorkInProgressEffects =
(finishedWork.subtreeFlags & LayoutMask) !== NoFlags;
recursivelyTraverseReappearLayoutEffects(
finishedRoot,
finishedWork,
includeWorkInProgressEffects,
);
} else {
recursivelyTraverseLayoutEffects(
finishedRoot,
finishedWork,
committedLanes,
);
}
offscreenSubtreeIsHidden = prevOffscreenSubtreeIsHidden;
offscreenSubtreeWasHidden = prevOffscreenSubtreeWasHidden;
}
} else {
recursivelyTraverseLayoutEffects(
finishedRoot,
finishedWork,
committedLanes,
);
}
if (flags & Ref) {
const props: OffscreenProps = finishedWork.memoizedProps;
if (props.mode === 'manual') {
safelyAttachRef(finishedWork, finishedWork.return);
} else {
safelyDetachRef(finishedWork, finishedWork.return);
}
}
break;
}
default: {
recursivelyTraverseLayoutEffects(
finishedRoot,
finishedWork,
committedLanes,
);
break;
}
}
}
function abortRootTransitions(
root: FiberRoot,
abort: TransitionAbort,
deletedTransitions: Set<Transition>,
deletedOffscreenInstance: OffscreenInstance | null,
isInDeletedTree: boolean,
) {
if (enableTransitionTracing) {
const rootTransitions = root.incompleteTransitions;
deletedTransitions.forEach(transition => {
if (rootTransitions.has(transition)) {
const transitionInstance: TracingMarkerInstance = (rootTransitions.get(
transition,
): any);
if (transitionInstance.aborts === null) {
transitionInstance.aborts = [];
}
transitionInstance.aborts.push(abort);
if (deletedOffscreenInstance !== null) {
if (
transitionInstance.pendingBoundaries !== null &&
transitionInstance.pendingBoundaries.has(deletedOffscreenInstance)
) {
// $FlowFixMe[incompatible-use] found when upgrading Flow
transitionInstance.pendingBoundaries.delete(
deletedOffscreenInstance,
);
}
}
}
});
}
}
function abortTracingMarkerTransitions(
abortedFiber: Fiber,
abort: TransitionAbort,
deletedTransitions: Set<Transition>,
deletedOffscreenInstance: OffscreenInstance | null,
isInDeletedTree: boolean,
) {
if (enableTransitionTracing) {
const markerInstance: TracingMarkerInstance = abortedFiber.stateNode;
const markerTransitions = markerInstance.transitions;
const pendingBoundaries = markerInstance.pendingBoundaries;
if (markerTransitions !== null) {
// TODO: Refactor this code. Is there a way to move this code to
// the deletions phase instead of calculating it here while making sure
// complete is called appropriately?
deletedTransitions.forEach(transition => {
// If one of the transitions on the tracing marker is a transition
// that was in an aborted subtree, we will abort that tracing marker
if (
abortedFiber !== null &&
markerTransitions.has(transition) &&
(markerInstance.aborts === null ||
!markerInstance.aborts.includes(abort))
) {
if (markerInstance.transitions !== null) {
if (markerInstance.aborts === null) {
markerInstance.aborts = [abort];
addMarkerIncompleteCallbackToPendingTransition(
abortedFiber.memoizedProps.name,
markerInstance.transitions,
markerInstance.aborts,
);
} else {
markerInstance.aborts.push(abort);
}
// We only want to call onTransitionProgress when the marker hasn't been
// deleted
if (
deletedOffscreenInstance !== null &&
!isInDeletedTree &&
pendingBoundaries !== null &&
pendingBoundaries.has(deletedOffscreenInstance)
) {
pendingBoundaries.delete(deletedOffscreenInstance);
addMarkerProgressCallbackToPendingTransition(
abortedFiber.memoizedProps.name,
deletedTransitions,
pendingBoundaries,
);
}
}
}
});
}
}
}
function abortParentMarkerTransitionsForDeletedFiber(
abortedFiber: Fiber,
abort: TransitionAbort,
deletedTransitions: Set<Transition>,
deletedOffscreenInstance: OffscreenInstance | null,
isInDeletedTree: boolean,
) {
if (enableTransitionTracing) {
// Find all pending markers that are waiting on child suspense boundaries in the
// aborted subtree and cancels them
let fiber: null | Fiber = abortedFiber;
while (fiber !== null) {
switch (fiber.tag) {
case TracingMarkerComponent:
abortTracingMarkerTransitions(
fiber,
abort,
deletedTransitions,
deletedOffscreenInstance,
isInDeletedTree,
);
break;
case HostRoot:
const root = fiber.stateNode;
abortRootTransitions(
root,
abort,
deletedTransitions,
deletedOffscreenInstance,
isInDeletedTree,
);
break;
default:
break;
}
fiber = fiber.return;
}
}
}
function commitTransitionProgress(offscreenFiber: Fiber) {
if (enableTransitionTracing) {
// This function adds suspense boundaries to the root
// or tracing marker's pendingBoundaries map.
// When a suspense boundary goes from a resolved to a fallback
// state we add the boundary to the map, and when it goes from
// a fallback to a resolved state, we remove the boundary from
// the map.
// We use stateNode on the Offscreen component as a stable object
// that doesnt change from render to render. This way we can
// distinguish between different Offscreen instances (vs. the same
// Offscreen instance with different fibers)
const offscreenInstance: OffscreenInstance = offscreenFiber.stateNode;
let prevState: SuspenseState | null = null;
const previousFiber = offscreenFiber.alternate;
if (previousFiber !== null && previousFiber.memoizedState !== null) {
prevState = previousFiber.memoizedState;
}
const nextState: SuspenseState | null = offscreenFiber.memoizedState;
const wasHidden = prevState !== null;
const isHidden = nextState !== null;
const pendingMarkers = offscreenInstance._pendingMarkers;
// If there is a name on the suspense boundary, store that in
// the pending boundaries.
let name = null;
const parent = offscreenFiber.return;
if (
parent !== null &&
parent.tag === SuspenseComponent &&
parent.memoizedProps.unstable_name
) {
name = parent.memoizedProps.unstable_name;
}
if (!wasHidden && isHidden) {
// The suspense boundaries was just hidden. Add the boundary
// to the pending boundary set if it's there
if (pendingMarkers !== null) {
pendingMarkers.forEach(markerInstance => {
const pendingBoundaries = markerInstance.pendingBoundaries;
const transitions = markerInstance.transitions;
const markerName = markerInstance.name;
if (
pendingBoundaries !== null &&
!pendingBoundaries.has(offscreenInstance)
) {
pendingBoundaries.set(offscreenInstance, {
name,
});
if (transitions !== null) {
if (
markerInstance.tag === TransitionTracingMarker &&
markerName !== null
) {
addMarkerProgressCallbackToPendingTransition(
markerName,
transitions,
pendingBoundaries,
);
} else if (markerInstance.tag === TransitionRoot) {
transitions.forEach(transition => {
addTransitionProgressCallbackToPendingTransition(
transition,
pendingBoundaries,
);
});
}
}
}
});
}
} else if (wasHidden && !isHidden) {
// The suspense boundary went from hidden to visible. Remove
// the boundary from the pending suspense boundaries set
// if it's there
if (pendingMarkers !== null) {
pendingMarkers.forEach(markerInstance => {
const pendingBoundaries = markerInstance.pendingBoundaries;
const transitions = markerInstance.transitions;
const markerName = markerInstance.name;
if (
pendingBoundaries !== null &&
pendingBoundaries.has(offscreenInstance)
) {
pendingBoundaries.delete(offscreenInstance);
if (transitions !== null) {
if (
markerInstance.tag === TransitionTracingMarker &&
markerName !== null
) {
addMarkerProgressCallbackToPendingTransition(
markerName,
transitions,
pendingBoundaries,
);
// If there are no more unresolved suspense boundaries, the interaction
// is considered finished
if (pendingBoundaries.size === 0) {
if (markerInstance.aborts === null) {
addMarkerCompleteCallbackToPendingTransition(
markerName,
transitions,
);
}
markerInstance.transitions = null;
markerInstance.pendingBoundaries = null;
markerInstance.aborts = null;
}
} else if (markerInstance.tag === TransitionRoot) {
transitions.forEach(transition => {
addTransitionProgressCallbackToPendingTransition(
transition,
pendingBoundaries,
);
});
}
}
}
});
}
}
}
}
function hideOrUnhideAllChildren(finishedWork: Fiber, isHidden: boolean) {
// Only hide or unhide the top-most host nodes.
let hostSubtreeRoot = null;
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 ||
(enableFloat && supportsResources
? node.tag === HostHoistable
: false) ||
(enableHostSingletons && supportsSingletons
? node.tag === HostSingleton
: false)
) {
if (hostSubtreeRoot === null) {
hostSubtreeRoot = node;
try {
const instance = node.stateNode;
if (isHidden) {
hideInstance(instance);
} else {
unhideInstance(node.stateNode, node.memoizedProps);
}
} catch (error) {
captureCommitPhaseError(finishedWork, finishedWork.return, error);
}
}
} else if (node.tag === HostText) {
if (hostSubtreeRoot === null) {
try {
const instance = node.stateNode;
if (isHidden) {
hideTextInstance(instance);
} else {
unhideTextInstance(instance, node.memoizedProps);
}
} catch (error) {
captureCommitPhaseError(finishedWork, finishedWork.return, error);
}
}
} 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;
}
if (hostSubtreeRoot === node) {
hostSubtreeRoot = null;
}
node = node.return;
}
if (hostSubtreeRoot === node) {
hostSubtreeRoot = null;
}
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 HostHoistable:
case HostSingleton:
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') {
if (shouldProfile(finishedWork)) {
try {
startLayoutEffectTimer();
finishedWork.refCleanup = ref(instanceToUse);
} finally {
recordLayoutEffectDuration(finishedWork);
}
} else {
finishedWork.refCleanup = 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().',
getComponentNameFromFiber(finishedWork),
);
}
}
// $FlowFixMe[incompatible-use] unable to narrow type to the non-function case
ref.current = instanceToUse;
}
}
}
function detachFiberMutation(fiber: Fiber) {
// Cut off the return pointer to disconnect it from the tree.
// This enables us to detect and warn against state updates on an unmounted component.
// It also prevents events from bubbling from within disconnected components.
//
// Ideally, we should also 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 that we can't clear child or sibling pointers yet.
// They're needed for passive effects and for findDOMNode.
// We defer those fields, and all other cleanup, to the passive phase (see detachFiberAfterEffects).
//
// Don't reset the alternate yet, either. We need that so we can detach the
// alternate's fields in the passive phase. Clearing the return pointer is
// sufficient for findDOMNode semantics.
const alternate = fiber.alternate;
if (alternate !== null) {
alternate.return = null;
}
fiber.return = null;
}
function detachFiberAfterEffects(fiber: Fiber) {
const alternate = fiber.alternate;
if (alternate !== null) {
fiber.alternate = null;
detachFiberAfterEffects(alternate);
}
// Clear cyclical Fiber fields. This level alone is designed to roughly
// approximate the planned Fiber refactor. In that world, `setState` will be
// bound to a special "instance" object instead of a Fiber. The Instance
// object will not have any of these fields. It will only be connected to
// the fiber tree via a single link at the root. So if this level alone is
// sufficient to fix memory issues, that bodes well for our plans.
fiber.child = null;
fiber.deletions = null;
fiber.sibling = null;
// The `stateNode` is cyclical because on host nodes it points to the host
// tree, which has its own pointers to children, parents, and siblings.
// The other host nodes also point back to fibers, so we should detach that
// one, too.
if (fiber.tag === HostComponent) {
const hostInstance: Instance = fiber.stateNode;
if (hostInstance !== null) {
detachDeletedInstance(hostInstance);
}
}
fiber.stateNode = null;
if (__DEV__) {
fiber._debugSource = null;
fiber._debugOwner = null;
}
// Theoretically, nothing in here should be necessary, because we already
// disconnected the fiber from the tree. So even if something leaks this
// particular fiber, it won't leak anything else.
fiber.return = null;
fiber.dependencies = null;
fiber.memoizedProps = null;
fiber.memoizedState = null;
fiber.pendingProps = null;
fiber.stateNode = null;
// TODO: Move to `commitPassiveUnmountInsideDeletedTreeOnFiber` instead.
fiber.updateQueue = null;
}
function emptyPortalContainer(current: Fiber) {
if (!supportsPersistence) {
return;
}
const portal: {
containerInfo: Container,
pendingChildren: ChildSet,
...
} = current.stateNode;
const {containerInfo} = portal;
const emptyChildSet = createContainerChildSet();
replaceContainerChildren(containerInfo, emptyChildSet);
}
function getHostParentFiber(fiber: Fiber): Fiber {
let parent = fiber.return;
while (parent !== null) {
if (isHostParent(parent)) {
return parent;
}
parent = parent.return;
}
throw new Error(
'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 ||
(enableFloat && supportsResources ? fiber.tag === HostHoistable : false) ||
(enableHostSingletons && supportsSingletons
? fiber.tag === HostSingleton
: false) ||
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;
}
// $FlowFixMe[incompatible-type] found when upgrading Flow
node = node.return;
}
node.sibling.return = node.return;
node = node.sibling;
while (
node.tag !== HostComponent &&
node.tag !== HostText &&
(!(enableHostSingletons && supportsSingletons)
? true
: node.tag !== HostSingleton) &&
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;
}
if (enableHostSingletons && supportsSingletons) {
if (finishedWork.tag === HostSingleton) {
// Singletons are already in the Host and don't need to be placed
// Since they operate somewhat like Portals though their children will
// have Placement and will get placed inside them
return;
}
}
// Recursively insert all host nodes into the parent.
const parentFiber = getHostParentFiber(finishedWork);
switch (parentFiber.tag) {
case HostSingleton: {
if (enableHostSingletons && supportsSingletons) {
const parent: Instance = parentFiber.stateNode;
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.
insertOrAppendPlacementNode(finishedWork, before, parent);
break;
}
// Fall through
}
case HostComponent: {
const parent: Instance = parentFiber.stateNode;
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.
insertOrAppendPlacementNode(finishedWork, before, parent);
break;
}
case HostRoot:
case HostPortal: {
const parent: Container = parentFiber.stateNode.containerInfo;
const before = getHostSibling(finishedWork);
insertOrAppendPlacementNodeIntoContainer(finishedWork, before, parent);
break;
}
default:
throw new Error(
'Invalid host parent fiber. This error is likely caused by a bug ' +
'in React. Please file an issue.',
);
}
}
function insertOrAppendPlacementNodeIntoContainer(
node: Fiber,
before: ?Instance,
parent: Container,
): void {
const {tag} = node;
const isHost = tag === HostComponent || tag === HostText;
if (isHost) {
const stateNode = node.stateNode;
if (before) {
insertInContainerBefore(parent, stateNode, before);
} else {
appendChildToContainer(parent, stateNode);
}
} else if (
tag === HostPortal ||
(enableHostSingletons && supportsSingletons ? tag === HostSingleton : false)
) {
// 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.
// If the insertion is a HostSingleton then it will be placed independently
} 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) {
const stateNode = node.stateNode;
if (before) {
insertBefore(parent, stateNode, before);
} else {
appendChild(parent, stateNode);
}
} else if (
tag === HostPortal ||
(enableHostSingletons && supportsSingletons ? tag === HostSingleton : false)
) {
// 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.
// If the insertion is a HostSingleton then it will be placed independently
} 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;
}
}
}
}
// These are tracked on the stack as we recursively traverse a
// deleted subtree.
// TODO: Update these during the whole mutation phase, not just during
// a deletion.
let hostParent: Instance | Container | null = null;
let hostParentIsContainer: boolean = false;
function commitDeletionEffects(
root: FiberRoot,
returnFiber: Fiber,
deletedFiber: Fiber,
) {
if (supportsMutation) {
// We only have the top Fiber that was deleted but we need to recurse down its
// children to find all the terminal nodes.
// Recursively delete all host nodes from the parent, detach refs, clean
// up mounted layout effects, and call componentWillUnmount.
// We only need to remove the topmost host child in each branch. But then we
// still need to keep traversing to unmount effects, refs, and cWU. TODO: We
// could split this into two separate traversals functions, where the second
// one doesn't include any removeChild logic. This is maybe the same
// function as "disappearLayoutEffects" (or whatever that turns into after
// the layout phase is refactored to use recursion).
// Before starting, find the nearest host parent on the stack so we know
// which instance/container to remove the children from.
// TODO: Instead of searching up the fiber return path on every deletion, we
// can track the nearest host component on the JS stack as we traverse the
// tree during the commit phase. This would make insertions faster, too.
let parent: null | Fiber = returnFiber;
findParent: while (parent !== null) {
switch (parent.tag) {
case HostSingleton:
case HostComponent: {
hostParent = parent.stateNode;
hostParentIsContainer = false;
break findParent;
}
case HostRoot: {
hostParent = parent.stateNode.containerInfo;
hostParentIsContainer = true;
break findParent;
}
case HostPortal: {
hostParent = parent.stateNode.containerInfo;
hostParentIsContainer = true;
break findParent;
}
}
parent = parent.return;
}
if (hostParent === null) {
throw new Error(
'Expected to find a host parent. This error is likely caused by ' +
'a bug in React. Please file an issue.',
);
}
commitDeletionEffectsOnFiber(root, returnFiber, deletedFiber);
hostParent = null;
hostParentIsContainer = false;
} else {
// Detach refs and call componentWillUnmount() on the whole subtree.
commitDeletionEffectsOnFiber(root, returnFiber, deletedFiber);
}
detachFiberMutation(deletedFiber);
}
function recursivelyTraverseDeletionEffects(
finishedRoot: FiberRoot,
nearestMountedAncestor: Fiber,
parent: Fiber,
) {
// TODO: Use a static flag to skip trees that don't have unmount effects
let child = parent.child;
while (child !== null) {
commitDeletionEffectsOnFiber(finishedRoot, nearestMountedAncestor, child);
child = child.sibling;
}
}
function commitDeletionEffectsOnFiber(
finishedRoot: FiberRoot,
nearestMountedAncestor: Fiber,
deletedFiber: Fiber,
) {
onCommitUnmount(deletedFiber);
// The cases in this outer switch modify the stack before they traverse
// into their subtree. There are simpler cases in the inner switch
// that don't modify the stack.
switch (deletedFiber.tag) {
case HostHoistable: {
if (enableFloat && supportsResources) {
if (!offscreenSubtreeWasHidden) {
safelyDetachRef(deletedFiber, nearestMountedAncestor);
}
recursivelyTraverseDeletionEffects(
finishedRoot,
nearestMountedAncestor,
deletedFiber,
);
if (deletedFiber.memoizedState) {
releaseResource(deletedFiber.memoizedState);
} else if (deletedFiber.stateNode) {
unmountHoistable(deletedFiber.stateNode);
}
return;
}
// Fall through
}
case HostSingleton: {
if (enableHostSingletons && supportsSingletons) {
if (!offscreenSubtreeWasHidden) {
safelyDetachRef(deletedFiber, nearestMountedAncestor);
}
const prevHostParent = hostParent;
const prevHostParentIsContainer = hostParentIsContainer;
hostParent = deletedFiber.stateNode;
recursivelyTraverseDeletionEffects(
finishedRoot,
nearestMountedAncestor,
deletedFiber,
);
// Normally this is called in passive unmount effect phase however with
// HostSingleton we warn if you acquire one that is already associated to
// a different fiber. To increase our chances of avoiding this, specifically
// if you keyed a HostSingleton so there will be a delete followed by a Placement
// we treat detach eagerly here
releaseSingletonInstance(deletedFiber.stateNode);
hostParent = prevHostParent;
hostParentIsContainer = prevHostParentIsContainer;
return;
}
// Fall through
}
case HostComponent: {
if (!offscreenSubtreeWasHidden) {
safelyDetachRef(deletedFiber, nearestMountedAncestor);
}
// Intentional fallthrough to next branch
}
case HostText: {
// We only need to remove the nearest host child. Set the host parent
// to `null` on the stack to indicate that nested children don't
// need to be removed.
if (supportsMutation) {
const prevHostParent = hostParent;
const prevHostParentIsContainer = hostParentIsContainer;
hostParent = null;
recursivelyTraverseDeletionEffects(
finishedRoot,
nearestMountedAncestor,
deletedFiber,
);
hostParent = prevHostParent;
hostParentIsContainer = prevHostParentIsContainer;
if (hostParent !== null) {
// Now that all the child effects have unmounted, we can remove the
// node from the tree.
if (hostParentIsContainer) {
removeChildFromContainer(
((hostParent: any): Container),
(deletedFiber.stateNode: Instance | TextInstance),
);
} else {
removeChild(
((hostParent: any): Instance),
(deletedFiber.stateNode: Instance | TextInstance),
);
}
}
} else {
recursivelyTraverseDeletionEffects(
finishedRoot,
nearestMountedAncestor,
deletedFiber,
);
}
return;
}
case DehydratedFragment: {
if (enableSuspenseCallback) {
const hydrationCallbacks = finishedRoot.hydrationCallbacks;
if (hydrationCallbacks !== null) {
const onDeleted = hydrationCallbacks.onDeleted;
if (onDeleted) {
onDeleted((deletedFiber.stateNode: SuspenseInstance));
}
}
}
// Dehydrated fragments don't have any children
// Delete the dehydrated suspense boundary and all of its content.
if (supportsMutation) {
if (hostParent !== null) {
if (hostParentIsContainer) {
clearSuspenseBoundaryFromContainer(
((hostParent: any): Container),
(deletedFiber.stateNode: SuspenseInstance),
);
} else {
clearSuspenseBoundary(
((hostParent: any): Instance),
(deletedFiber.stateNode: SuspenseInstance),
);
}
}
}
return;
}
case HostPortal: {
if (supportsMutation) {
// When we go into a portal, it becomes the parent to remove from.
const prevHostParent = hostParent;
const prevHostParentIsContainer = hostParentIsContainer;
hostParent = deletedFiber.stateNode.containerInfo;
hostParentIsContainer = true;
recursivelyTraverseDeletionEffects(
finishedRoot,
nearestMountedAncestor,
deletedFiber,
);
hostParent = prevHostParent;
hostParentIsContainer = prevHostParentIsContainer;
} else {
emptyPortalContainer(deletedFiber);
recursivelyTraverseDeletionEffects(
finishedRoot,
nearestMountedAncestor,
deletedFiber,
);
}
return;
}
case FunctionComponent:
case ForwardRef:
case MemoComponent:
case SimpleMemoComponent: {
if (!offscreenSubtreeWasHidden) {
const updateQueue: FunctionComponentUpdateQueue | null =
(deletedFiber.updateQueue: any);
if (updateQueue !== null) {
const lastEffect = updateQueue.lastEffect;
if (lastEffect !== null) {
const firstEffect = lastEffect.next;
let effect = firstEffect;
do {
const tag = effect.tag;
const inst = effect.inst;
const destroy = inst.destroy;
if (destroy !== undefined) {
if ((tag & HookInsertion) !== NoHookEffect) {
inst.destroy = undefined;
safelyCallDestroy(
deletedFiber,
nearestMountedAncestor,
destroy,
);
} else if ((tag & HookLayout) !== NoHookEffect) {
if (enableSchedulingProfiler) {
markComponentLayoutEffectUnmountStarted(deletedFiber);
}
if (shouldProfile(deletedFiber)) {
startLayoutEffectTimer();
inst.destroy = undefined;
safelyCallDestroy(
deletedFiber,
nearestMountedAncestor,
destroy,
);
recordLayoutEffectDuration(deletedFiber);
} else {
inst.destroy = undefined;
safelyCallDestroy(
deletedFiber,
nearestMountedAncestor,
destroy,
);
}
if (enableSchedulingProfiler) {
markComponentLayoutEffectUnmountStopped();
}
}
}
effect = effect.next;
} while (effect !== firstEffect);
}
}
}
recursivelyTraverseDeletionEffects(
finishedRoot,
nearestMountedAncestor,
deletedFiber,
);
return;
}
case ClassComponent: {
if (!offscreenSubtreeWasHidden) {
safelyDetachRef(deletedFiber, nearestMountedAncestor);
const instance = deletedFiber.stateNode;
if (typeof instance.componentWillUnmount === 'function') {
safelyCallComponentWillUnmount(
deletedFiber,
nearestMountedAncestor,
instance,
);
}
}
recursivelyTraverseDeletionEffects(
finishedRoot,
nearestMountedAncestor,
deletedFiber,
);
return;
}
case ScopeComponent: {
if (enableScopeAPI) {
safelyDetachRef(deletedFiber, nearestMountedAncestor);
}
recursivelyTraverseDeletionEffects(
finishedRoot,
nearestMountedAncestor,
deletedFiber,
);
return;
}
case OffscreenComponent: {
safelyDetachRef(deletedFiber, nearestMountedAncestor);
if (deletedFiber.mode & ConcurrentMode) {
// If this offscreen component is hidden, we already unmounted it. Before
// deleting the children, track that it's already unmounted so that we
// don't attempt to unmount the effects again.
// TODO: If the tree is hidden, in most cases we should be able to skip
// over the nested children entirely. An exception is we haven't yet found
// the topmost host node to delete, which we already track on the stack.
// But the other case is portals, which need to be detached no matter how
// deeply they are nested. We should use a subtree flag to track whether a
// subtree includes a nested portal.
const prevOffscreenSubtreeWasHidden = offscreenSubtreeWasHidden;
offscreenSubtreeWasHidden =
prevOffscreenSubtreeWasHidden || deletedFiber.memoizedState !== null;
recursivelyTraverseDeletionEffects(
finishedRoot,
nearestMountedAncestor,
deletedFiber,
);
offscreenSubtreeWasHidden = prevOffscreenSubtreeWasHidden;
} else {
recursivelyTraverseDeletionEffects(
finishedRoot,
nearestMountedAncestor,
deletedFiber,
);
}
break;
}
default: {
recursivelyTraverseDeletionEffects(
finishedRoot,
nearestMountedAncestor,
deletedFiber,
);
return;
}
}
}
function commitSuspenseCallback(finishedWork: Fiber) {
// TODO: Move this to passive phase
const newState: SuspenseState | null = finishedWork.memoizedState;
if (enableSuspenseCallback && newState !== null) {
const suspenseCallback = finishedWork.memoizedProps.suspenseCallback;
if (typeof suspenseCallback === 'function') {
const retryQueue: RetryQueue | null = (finishedWork.updateQueue: any);
if (retryQueue !== null) {
suspenseCallback(new Set(retryQueue));
}
} 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) {
try {
commitHydratedSuspenseInstance(suspenseInstance);
if (enableSuspenseCallback) {
const hydrationCallbacks = finishedRoot.hydrationCallbacks;
if (hydrationCallbacks !== null) {
const onHydrated = hydrationCallbacks.onHydrated;
if (onHydrated) {
onHydrated(suspenseInstance);
}
}
}
} catch (error) {
captureCommitPhaseError(finishedWork, finishedWork.return, error);
}
}
}
}
}
}
function getRetryCache(finishedWork: Fiber) {
// TODO: Unify the interface for the retry cache so we don't have to switch
// on the tag like this.
switch (finishedWork.tag) {
case SuspenseComponent:
case SuspenseListComponent: {
let retryCache = finishedWork.stateNode;
if (retryCache === null) {
retryCache = finishedWork.stateNode = new PossiblyWeakSet();
}
return retryCache;
}
case OffscreenComponent: {
const instance: OffscreenInstance = finishedWork.stateNode;
let retryCache: null | Set<Wakeable> | WeakSet<Wakeable> =
instance._retryCache;
if (retryCache === null) {
retryCache = instance._retryCache = new PossiblyWeakSet();
}
return retryCache;
}
default: {
throw new Error(
`Unexpected Suspense handler tag (${finishedWork.tag}). This is a ` +
'bug in React.',
);
}
}
}
export function detachOffscreenInstance(instance: OffscreenInstance): void {
const fiber = instance._current;
if (fiber === null) {
throw new Error(
'Calling Offscreen.detach before instance handle has been set.',
);
}
if ((instance._pendingVisibility & OffscreenDetached) !== NoFlags) {
// The instance is already detached, this is a noop.
return;
}
// TODO: There is an opportunity to optimise this by not entering commit phase
// and unmounting effects directly.
const root = enqueueConcurrentRenderForLane(fiber, SyncLane);
if (root !== null) {
instance._pendingVisibility |= OffscreenDetached;
scheduleUpdateOnFiber(root, fiber, SyncLane);
}
}
export function attachOffscreenInstance(instance: OffscreenInstance): void {
const fiber = instance._current;
if (fiber === null) {
throw new Error(
'Calling Offscreen.detach before instance handle has been set.',
);
}
if ((instance._pendingVisibility & OffscreenDetached) === NoFlags) {
// The instance is already attached, this is a noop.
return;
}
const root = enqueueConcurrentRenderForLane(fiber, SyncLane);
if (root !== null) {
instance._pendingVisibility &= ~OffscreenDetached;
scheduleUpdateOnFiber(root, fiber, SyncLane);
}
}
function attachSuspenseRetryListeners(
finishedWork: Fiber,
wakeables: RetryQueue,
) {
// 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 retryCache = getRetryCache(finishedWork);
wakeables.forEach(wakeable => {
// Memoize using the boundary fiber to prevent redundant listeners.
const retry = resolveRetryWakeable.bind(null, finishedWork, wakeable);
if (!retryCache.has(wakeable)) {
retryCache.add(wakeable);
if (enableUpdaterTracking) {
if (isDevToolsPresent) {
if (inProgressLanes !== null && inProgressRoot !== null) {
// If we have pending work still, associate the original updaters with it.
restorePendingUpdaters(inProgressRoot, inProgressLanes);
} else {
throw Error(
'Expected finished root and lanes to be set. This is a bug in React.',
);
}
}
}
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;
}
export function commitMutationEffects(
root: FiberRoot,
finishedWork: Fiber,
committedLanes: Lanes,
) {
inProgressLanes = committedLanes;
inProgressRoot = root;
setCurrentDebugFiberInDEV(finishedWork);
commitMutationEffectsOnFiber(finishedWork, root, committedLanes);
setCurrentDebugFiberInDEV(finishedWork);
inProgressLanes = null;
inProgressRoot = null;
}
function recursivelyTraverseMutationEffects(
root: FiberRoot,
parentFiber: Fiber,
lanes: Lanes,
) {
// Deletions effects can be scheduled on any fiber type. They need to happen
// before the children effects hae fired.
const deletions = parentFiber.deletions;
if (deletions !== null) {
for (let i = 0; i < deletions.length; i++) {
const childToDelete = deletions[i];
try {
commitDeletionEffects(root, parentFiber, childToDelete);
} catch (error) {
captureCommitPhaseError(childToDelete, parentFiber, error);
}
}
}
const prevDebugFiber = getCurrentDebugFiberInDEV();
if (parentFiber.subtreeFlags & MutationMask) {
let child = parentFiber.child;
while (child !== null) {
setCurrentDebugFiberInDEV(child);
commitMutationEffectsOnFiber(child, root, lanes);
child = child.sibling;
}
}
setCurrentDebugFiberInDEV(prevDebugFiber);
}
let currentHoistableRoot: HoistableRoot | null = null;
function commitMutationEffectsOnFiber(
finishedWork: Fiber,
root: FiberRoot,
lanes: Lanes,
) {
const current = finishedWork.alternate;
const flags = finishedWork.flags;
// The effect flag should be checked *after* we refine the type of fiber,
// because the fiber tag is more specific. An exception is any flag related
// to reconciliation, because those can be set on all fiber types.
switch (finishedWork.tag) {
case FunctionComponent:
case ForwardRef:
case MemoComponent:
case SimpleMemoComponent: {
recursivelyTraverseMutationEffects(root, finishedWork, lanes);
commitReconciliationEffects(finishedWork);
if (flags & Update) {
try {
commitHookEffectListUnmount(
HookInsertion | HookHasEffect,
finishedWork,
finishedWork.return,
);
commitHookEffectListMount(
HookInsertion | HookHasEffect,
finishedWork,
);
} catch (error) {
captureCommitPhaseError(finishedWork, finishedWork.return, error);
}
// 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 (shouldProfile(finishedWork)) {
try {
startLayoutEffectTimer();
commitHookEffectListUnmount(
HookLayout | HookHasEffect,
finishedWork,
finishedWork.return,
);
} catch (error) {
captureCommitPhaseError(finishedWork, finishedWork.return, error);
}
recordLayoutEffectDuration(finishedWork);
} else {
try {
commitHookEffectListUnmount(
HookLayout | HookHasEffect,
finishedWork,
finishedWork.return,
);
} catch (error) {
captureCommitPhaseError(finishedWork, finishedWork.return, error);
}
}
}
return;
}
case ClassComponent: {
recursivelyTraverseMutationEffects(root, finishedWork, lanes);
commitReconciliationEffects(finishedWork);
if (flags & Ref) {
if (current !== null) {
safelyDetachRef(current, current.return);
}
}
if (flags & Callback && offscreenSubtreeIsHidden) {
const updateQueue: UpdateQueue<mixed> | null =
(finishedWork.updateQueue: any);
if (updateQueue !== null) {
deferHiddenCallbacks(updateQueue);
}
}
return;
}
case HostHoistable: {
if (enableFloat && supportsResources) {
// We cast because we always set the root at the React root and so it cannot be
// null while we are processing mutation effects
const hoistableRoot: HoistableRoot = (currentHoistableRoot: any);
recursivelyTraverseMutationEffects(root, finishedWork, lanes);
commitReconciliationEffects(finishedWork);
if (flags & Ref) {
if (current !== null) {
safelyDetachRef(current, current.return);
}
}
if (flags & Update) {
const currentResource =
current !== null ? current.memoizedState : null;
const newResource = finishedWork.memoizedState;
if (current === null) {
// We are mounting a new HostHoistable Fiber. We fork the mount
// behavior based on whether this instance is a Hoistable Instance
// or a Hoistable Resource
if (newResource === null) {
if (finishedWork.stateNode === null) {
finishedWork.stateNode = hydrateHoistable(
hoistableRoot,
finishedWork.type,
finishedWork.memoizedProps,
finishedWork,
);
} else {
mountHoistable(
hoistableRoot,
finishedWork.type,
finishedWork.stateNode,
);
}
} else {
finishedWork.stateNode = acquireResource(
hoistableRoot,
newResource,
finishedWork.memoizedProps,
);
}
} else if (currentResource !== newResource) {
// We are moving to or from Hoistable Resource, or between different Hoistable Resources
if (currentResource === null) {
if (current.stateNode !== null) {
unmountHoistable(current.stateNode);
}
} else {
releaseResource(currentResource);
}
if (newResource === null) {
mountHoistable(
hoistableRoot,
finishedWork.type,
finishedWork.stateNode,
);
} else {
acquireResource(
hoistableRoot,
newResource,
finishedWork.memoizedProps,
);
}
} else if (newResource === null && finishedWork.stateNode !== null) {
// We may have an update on a Hoistable element
const updatePayload: null | UpdatePayload =
(finishedWork.updateQueue: any);
finishedWork.updateQueue = null;
try {
commitUpdate(
finishedWork.stateNode,
updatePayload,
finishedWork.type,
current.memoizedProps,
finishedWork.memoizedProps,
finishedWork,
);
} catch (error) {
captureCommitPhaseError(finishedWork, finishedWork.return, error);
}
}
}
return;
}
// Fall through
}
case HostSingleton: {
if (enableHostSingletons && supportsSingletons) {
if (flags & Update) {
const previousWork = finishedWork.alternate;
if (previousWork === null) {
const singleton = finishedWork.stateNode;
const props = finishedWork.memoizedProps;
// This was a new mount, we need to clear and set initial properties
clearSingleton(singleton);
acquireSingletonInstance(
finishedWork.type,
props,
singleton,
finishedWork,
);
}
}
}
// Fall through
}
case HostComponent: {
recursivelyTraverseMutationEffects(root, finishedWork, lanes);
commitReconciliationEffects(finishedWork);
if (flags & Ref) {
if (current !== null) {
safelyDetachRef(current, current.return);
}
}
if (supportsMutation) {
// TODO: ContentReset gets cleared by the children during the commit
// phase. This is a refactor hazard because it means we must read
// flags the flags after `commitReconciliationEffects` has already run;
// the order matters. We should refactor so that ContentReset does not
// rely on mutating the flag during commit. Like by setting a flag
// during the render phase instead.
if (finishedWork.flags & ContentReset) {
const instance: Instance = finishedWork.stateNode;
try {
resetTextContent(instance);
} catch (error) {
captureCommitPhaseError(finishedWork, finishedWork.return, error);
}
}
if (flags & Update) {
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;
try {
commitUpdate(
instance,
updatePayload,
type,
oldProps,
newProps,
finishedWork,
);
} catch (error) {
captureCommitPhaseError(finishedWork, finishedWork.return, error);
}
}
}
}
return;
}
case HostText: {
recursivelyTraverseMutationEffects(root, finishedWork, lanes);
commitReconciliationEffects(finishedWork);
if (flags & Update) {
if (supportsMutation) {
if (finishedWork.stateNode === null) {
throw new Error(
'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;
try {
commitTextUpdate(textInstance, oldText, newText);
} catch (error) {
captureCommitPhaseError(finishedWork, finishedWork.return, error);
}
}
}
return;
}
case HostRoot: {
if (enableFloat && supportsResources) {
prepareToCommitHoistables();
const previousHoistableRoot = currentHoistableRoot;
currentHoistableRoot = getHoistableRoot(root.containerInfo);
recursivelyTraverseMutationEffects(root, finishedWork, lanes);
currentHoistableRoot = previousHoistableRoot;
commitReconciliationEffects(finishedWork);
} else {
recursivelyTraverseMutationEffects(root, finishedWork, lanes);
commitReconciliationEffects(finishedWork);
}
if (flags & Update) {
if (supportsMutation && supportsHydration) {
if (current !== null) {
const prevRootState: RootState = current.memoizedState;
if (prevRootState.isDehydrated) {
try {
commitHydratedContainer(root.containerInfo);
} catch (error) {
captureCommitPhaseError(
finishedWork,
finishedWork.return,
error,
);
}
}
}
}
if (supportsPersistence) {
const containerInfo = root.containerInfo;
const pendingChildren = root.pendingChildren;
try {
replaceContainerChildren(containerInfo, pendingChildren);
} catch (error) {
captureCommitPhaseError(finishedWork, finishedWork.return, error);
}
}
}
return;
}
case HostPortal: {
if (enableFloat && supportsResources) {
const previousHoistableRoot = currentHoistableRoot;
currentHoistableRoot = getHoistableRoot(
finishedWork.stateNode.containerInfo,
);
recursivelyTraverseMutationEffects(root, finishedWork, lanes);
commitReconciliationEffects(finishedWork);
currentHoistableRoot = previousHoistableRoot;
} else {
recursivelyTraverseMutationEffects(root, finishedWork, lanes);
commitReconciliationEffects(finishedWork);
}
if (flags & Update) {
if (supportsPersistence) {
const portal = finishedWork.stateNode;
const containerInfo = portal.containerInfo;
const pendingChildren = portal.pendingChildren;
try {
replaceContainerChildren(containerInfo, pendingChildren);
} catch (error) {
captureCommitPhaseError(finishedWork, finishedWork.return, error);
}
}
}
return;
}
case SuspenseComponent: {
recursivelyTraverseMutationEffects(root, finishedWork, lanes);
commitReconciliationEffects(finishedWork);
// TODO: We should mark a flag on the Suspense fiber itself, rather than
// relying on the Offscreen fiber having a flag also being marked. The
// reason is that this offscreen fiber might not be part of the work-in-
// progress tree! It could have been reused from a previous render. This
// doesn't lead to incorrect behavior because we don't rely on the flag
// check alone; we also compare the states explicitly below. But for
// modeling purposes, we _should_ be able to rely on the flag check alone.
// So this is a bit fragile.
//
// Also, all this logic could/should move to the passive phase so it
// doesn't block paint.
const offscreenFiber: Fiber = (finishedWork.child: any);
if (offscreenFiber.flags & Visibility) {
// Throttle the appearance and disappearance of Suspense fallbacks.
const isShowingFallback =
(finishedWork.memoizedState: SuspenseState | null) !== null;
const wasShowingFallback =
current !== null &&
(current.memoizedState: SuspenseState | null) !== null;
if (alwaysThrottleRetries) {
if (isShowingFallback !== wasShowingFallback) {
// A fallback is either appearing or disappearing.
markCommitTimeOfFallback();
}
} else {
if (isShowingFallback && !wasShowingFallback) {
// Old behavior. Only mark when a fallback appears, not when
// it disappears.
markCommitTimeOfFallback();
}
}
}
if (flags & Update) {
try {
commitSuspenseCallback(finishedWork);
} catch (error) {
captureCommitPhaseError(finishedWork, finishedWork.return, error);
}
const retryQueue: RetryQueue | null = (finishedWork.updateQueue: any);
if (retryQueue !== null) {
finishedWork.updateQueue = null;
attachSuspenseRetryListeners(finishedWork, retryQueue);
}
}
return;
}
case OffscreenComponent: {
if (flags & Ref) {
if (current !== null) {
safelyDetachRef(current, current.return);
}
}
const newState: OffscreenState | null = finishedWork.memoizedState;
const isHidden = newState !== null;
const wasHidden = current !== null && current.memoizedState !== null;
if (finishedWork.mode & ConcurrentMode) {
// Before committing the children, track on the stack whether this
// offscreen subtree was already hidden, so that we don't unmount the
// effects again.
const prevOffscreenSubtreeIsHidden = offscreenSubtreeIsHidden;
const prevOffscreenSubtreeWasHidden = offscreenSubtreeWasHidden;
offscreenSubtreeIsHidden = prevOffscreenSubtreeIsHidden || isHidden;
offscreenSubtreeWasHidden = prevOffscreenSubtreeWasHidden || wasHidden;
recursivelyTraverseMutationEffects(root, finishedWork, lanes);
offscreenSubtreeWasHidden = prevOffscreenSubtreeWasHidden;
offscreenSubtreeIsHidden = prevOffscreenSubtreeIsHidden;
} else {
recursivelyTraverseMutationEffects(root, finishedWork, lanes);
}
commitReconciliationEffects(finishedWork);
const offscreenInstance: OffscreenInstance = finishedWork.stateNode;
// TODO: Add explicit effect flag to set _current.
offscreenInstance._current = finishedWork;
// Offscreen stores pending changes to visibility in `_pendingVisibility`. This is
// to support batching of `attach` and `detach` calls.
offscreenInstance._visibility &= ~OffscreenDetached;
offscreenInstance._visibility |=
offscreenInstance._pendingVisibility & OffscreenDetached;
if (flags & Visibility) {
// Track the current state on the Offscreen instance so we can
// read it during an event
if (isHidden) {
offscreenInstance._visibility &= ~OffscreenVisible;
} else {
offscreenInstance._visibility |= OffscreenVisible;
}
if (isHidden) {
const isUpdate = current !== null;
const wasHiddenByAncestorOffscreen =
offscreenSubtreeIsHidden || offscreenSubtreeWasHidden;
// Only trigger disapper layout effects if:
// - This is an update, not first mount.
// - This Offscreen was not hidden before.
// - Ancestor Offscreen was not hidden in previous commit.
if (isUpdate && !wasHidden && !wasHiddenByAncestorOffscreen) {
if ((finishedWork.mode & ConcurrentMode) !== NoMode) {
// Disappear the layout effects of all the children
recursivelyTraverseDisappearLayoutEffects(finishedWork);
}
}
} else {
if (wasHidden) {
// TODO: Move re-appear call here for symmetry?
}
}
// Offscreen with manual mode manages visibility manually.
if (supportsMutation && !isOffscreenManual(finishedWork)) {
// TODO: This needs to run whenever there's an insertion or update
// inside a hidden Offscreen tree.
hideOrUnhideAllChildren(finishedWork, isHidden);
}
}
// TODO: Move to passive phase
if (flags & Update) {
const offscreenQueue: OffscreenQueue | null =
(finishedWork.updateQueue: any);
if (offscreenQueue !== null) {
const retryQueue = offscreenQueue.retryQueue;
if (retryQueue !== null) {
offscreenQueue.retryQueue = null;
attachSuspenseRetryListeners(finishedWork, retryQueue);
}
}
}
return;
}
case SuspenseListComponent: {
recursivelyTraverseMutationEffects(root, finishedWork, lanes);
commitReconciliationEffects(finishedWork);
if (flags & Update) {
const retryQueue: Set<Wakeable> | null =
(finishedWork.updateQueue: any);
if (retryQueue !== null) {
finishedWork.updateQueue = null;
attachSuspenseRetryListeners(finishedWork, retryQueue);
}
}
return;
}
case ScopeComponent: {
if (enableScopeAPI) {
recursivelyTraverseMutationEffects(root, finishedWork, lanes);
commitReconciliationEffects(finishedWork);
// TODO: This is a temporary solution that allowed us to transition away
// from React Flare on www.
if (flags & Ref) {
if (current !== null) {
safelyDetachRef(finishedWork, finishedWork.return);
}
safelyAttachRef(finishedWork, finishedWork.return);
}
if (flags & Update) {
const scopeInstance = finishedWork.stateNode;
prepareScopeUpdate(scopeInstance, finishedWork);
}
}
return;
}
default: {
recursivelyTraverseMutationEffects(root, finishedWork, lanes);
commitReconciliationEffects(finishedWork);
return;
}
}
}
function commitReconciliationEffects(finishedWork: Fiber) {
// Placement effects (insertions, reorders) can be scheduled on any fiber
// type. They needs to happen after the children effects have fired, but
// before the effects on this fiber have fired.
const flags = finishedWork.flags;
if (flags & Placement) {
try {
commitPlacement(finishedWork);
} catch (error) {
captureCommitPhaseError(finishedWork, finishedWork.return, error);
}
// Clear the "placement" from effect tag so that we know that this is
// inserted, before any life-cycles like componentDidMount gets called.
// TODO: findDOMNode doesn't rely on this any more but isMounted does
// and isMounted is deprecated anyway so we should be able to kill this.
finishedWork.flags &= ~Placement;
}
if (flags & Hydrating) {
finishedWork.flags &= ~Hydrating;
}
}
export function commitLayoutEffects(
finishedWork: Fiber,
root: FiberRoot,
committedLanes: Lanes,
): void {
inProgressLanes = committedLanes;
inProgressRoot = root;
const current = finishedWork.alternate;
commitLayoutEffectOnFiber(root, current, finishedWork, committedLanes);
inProgressLanes = null;
inProgressRoot = null;
}
function recursivelyTraverseLayoutEffects(
root: FiberRoot,
parentFiber: Fiber,
lanes: Lanes,
) {
const prevDebugFiber = getCurrentDebugFiberInDEV();
if (parentFiber.subtreeFlags & LayoutMask) {
let child = parentFiber.child;
while (child !== null) {
setCurrentDebugFiberInDEV(child);
const current = child.alternate;
commitLayoutEffectOnFiber(root, current, child, lanes);
child = child.sibling;
}
}
setCurrentDebugFiberInDEV(prevDebugFiber);
}
export function disappearLayoutEffects(finishedWork: Fiber) {
switch (finishedWork.tag) {
case FunctionComponent:
case ForwardRef:
case MemoComponent:
case SimpleMemoComponent: {
// TODO (Offscreen) Check: flags & LayoutStatic
if (shouldProfile(finishedWork)) {
try {
startLayoutEffectTimer();
commitHookEffectListUnmount(
HookLayout,
finishedWork,
finishedWork.return,
);
} finally {
recordLayoutEffectDuration(finishedWork);
}
} else {
commitHookEffectListUnmount(
HookLayout,
finishedWork,
finishedWork.return,
);
}
recursivelyTraverseDisappearLayoutEffects(finishedWork);
break;
}
case ClassComponent: {
// TODO (Offscreen) Check: flags & RefStatic
safelyDetachRef(finishedWork, finishedWork.return);
const instance = finishedWork.stateNode;
if (typeof instance.componentWillUnmount === 'function') {
safelyCallComponentWillUnmount(
finishedWork,
finishedWork.return,
instance,
);
}
recursivelyTraverseDisappearLayoutEffects(finishedWork);
break;
}
case HostHoistable:
case HostSingleton:
case HostComponent: {
// TODO (Offscreen) Check: flags & RefStatic
safelyDetachRef(finishedWork, finishedWork.return);
recursivelyTraverseDisappearLayoutEffects(finishedWork);
break;
}
case OffscreenComponent: {
// TODO (Offscreen) Check: flags & RefStatic
safelyDetachRef(finishedWork, finishedWork.return);
const isHidden = finishedWork.memoizedState !== null;
if (isHidden) {
// Nested Offscreen tree is already hidden. Don't disappear
// its effects.
} else {
recursivelyTraverseDisappearLayoutEffects(finishedWork);
}
break;
}
default: {
recursivelyTraverseDisappearLayoutEffects(finishedWork);
break;
}
}
}
function recursivelyTraverseDisappearLayoutEffects(parentFiber: Fiber) {
// TODO (Offscreen) Check: flags & (RefStatic | LayoutStatic)
let child = parentFiber.child;
while (child !== null) {
disappearLayoutEffects(child);
child = child.sibling;
}
}
export function reappearLayoutEffects(
finishedRoot: FiberRoot,
current: Fiber | null,
finishedWork: Fiber,
// This function visits both newly finished work and nodes that were re-used
// from a previously committed tree. We cannot check non-static flags if the
// node was reused.
includeWorkInProgressEffects: boolean,
) {
// Turn on layout effects in a tree that previously disappeared.
const flags = finishedWork.flags;
switch (finishedWork.tag) {
case FunctionComponent:
case ForwardRef:
case SimpleMemoComponent: {
recursivelyTraverseReappearLayoutEffects(
finishedRoot,
finishedWork,
includeWorkInProgressEffects,
);
// TODO: Check flags & LayoutStatic
commitHookLayoutEffects(finishedWork, HookLayout);
break;
}
case ClassComponent: {
recursivelyTraverseReappearLayoutEffects(
finishedRoot,
finishedWork,
includeWorkInProgressEffects,
);
// TODO: Check for LayoutStatic flag
const instance = finishedWork.stateNode;
if (typeof instance.componentDidMount === 'function') {
try {
instance.componentDidMount();
} catch (error) {
captureCommitPhaseError(finishedWork, finishedWork.return, error);
}
}
// Commit any callbacks that would have fired while the component
// was hidden.
const updateQueue: UpdateQueue<mixed> | null =
(finishedWork.updateQueue: any);
if (updateQueue !== null) {
commitHiddenCallbacks(updateQueue, instance);
}
// If this is newly finished work, check for setState callbacks
if (includeWorkInProgressEffects && flags & Callback) {
commitClassCallbacks(finishedWork);
}
// TODO: Check flags & RefStatic
safelyAttachRef(finishedWork, finishedWork.return);
break;
}
// Unlike commitLayoutEffectsOnFiber, we don't need to handle HostRoot
// because this function only visits nodes that are inside an
// Offscreen fiber.
// case HostRoot: {
// ...
// }
case HostHoistable:
case HostSingleton:
case HostComponent: {
recursivelyTraverseReappearLayoutEffects(
finishedRoot,
finishedWork,
includeWorkInProgressEffects,
);
// 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 (includeWorkInProgressEffects && current === null && flags & Update) {
commitHostComponentMount(finishedWork);
}
// TODO: Check flags & Ref
safelyAttachRef(finishedWork, finishedWork.return);
break;
}
case Profiler: {
recursivelyTraverseReappearLayoutEffects(
finishedRoot,
finishedWork,
includeWorkInProgressEffects,
);
// TODO: Figure out how Profiler updates should work with Offscreen
if (includeWorkInProgressEffects && flags & Update) {
commitProfilerUpdate(finishedWork, current);
}
break;
}
case SuspenseComponent: {
recursivelyTraverseReappearLayoutEffects(
finishedRoot,
finishedWork,
includeWorkInProgressEffects,
);
// TODO: Figure out how Suspense hydration callbacks should work
// with Offscreen.
if (includeWorkInProgressEffects && flags & Update) {
commitSuspenseHydrationCallbacks(finishedRoot, finishedWork);
}
break;
}
case OffscreenComponent: {
const offscreenState: OffscreenState = finishedWork.memoizedState;
const isHidden = offscreenState !== null;
if (isHidden) {
// Nested Offscreen tree is still hidden. Don't re-appear its effects.
} else {
recursivelyTraverseReappearLayoutEffects(
finishedRoot,
finishedWork,
includeWorkInProgressEffects,
);
}
// TODO: Check flags & Ref
safelyAttachRef(finishedWork, finishedWork.return);
break;
}
default: {
recursivelyTraverseReappearLayoutEffects(
finishedRoot,
finishedWork,
includeWorkInProgressEffects,
);
break;
}
}
}
function recursivelyTraverseReappearLayoutEffects(
finishedRoot: FiberRoot,
parentFiber: Fiber,
includeWorkInProgressEffects: boolean,
) {
// This function visits both newly finished work and nodes that were re-used
// from a previously committed tree. We cannot check non-static flags if the
// node was reused.
const childShouldIncludeWorkInProgressEffects =
includeWorkInProgressEffects &&
(parentFiber.subtreeFlags & LayoutMask) !== NoFlags;
// TODO (Offscreen) Check: flags & (RefStatic | LayoutStatic)
const prevDebugFiber = getCurrentDebugFiberInDEV();
let child = parentFiber.child;
while (child !== null) {
const current = child.alternate;
reappearLayoutEffects(
finishedRoot,
current,
child,
childShouldIncludeWorkInProgressEffects,
);
child = child.sibling;
}
setCurrentDebugFiberInDEV(prevDebugFiber);
}
function commitHookPassiveMountEffects(
finishedWork: Fiber,
hookFlags: HookFlags,
) {
if (shouldProfile(finishedWork)) {
startPassiveEffectTimer();
try {
commitHookEffectListMount(hookFlags, finishedWork);
} catch (error) {
captureCommitPhaseError(finishedWork, finishedWork.return, error);
}
recordPassiveEffectDuration(finishedWork);
} else {
try {
commitHookEffectListMount(hookFlags, finishedWork);
} catch (error) {
captureCommitPhaseError(finishedWork, finishedWork.return, error);
}
}
}
function commitOffscreenPassiveMountEffects(
current: Fiber | null,
finishedWork: Fiber,
instance: OffscreenInstance,
) {
if (enableCache) {
let previousCache: Cache | null = null;
if (
current !== null &&
current.memoizedState !== null &&
current.memoizedState.cachePool !== null
) {
previousCache = current.memoizedState.cachePool.pool;
}
let nextCache: Cache | null = null;
if (
finishedWork.memoizedState !== null &&
finishedWork.memoizedState.cachePool !== null
) {
nextCache = finishedWork.memoizedState.cachePool.pool;
}
// Retain/release the cache used for pending (suspended) nodes.
// Note that this is only reached in the non-suspended/visible case:
// when the content is suspended/hidden, the retain/release occurs
// via the parent Suspense component (see case above).
if (nextCache !== previousCache) {
if (nextCache != null) {
retainCache(nextCache);
}
if (previousCache != null) {
releaseCache(previousCache);
}
}
}
if (enableTransitionTracing) {
// TODO: Pre-rendering should not be counted as part of a transition. We
// may add separate logs for pre-rendering, but it's not part of the
// primary metrics.
const offscreenState: OffscreenState = finishedWork.memoizedState;
const queue: OffscreenQueue | null = (finishedWork.updateQueue: any);
const isHidden = offscreenState !== null;
if (queue !== null) {
if (isHidden) {
const transitions = queue.transitions;
if (transitions !== null) {
transitions.forEach(transition => {
// Add all the transitions saved in the update queue during
// the render phase (ie the transitions associated with this boundary)
// into the transitions set.
if (instance._transitions === null) {
instance._transitions = new Set();
}
instance._transitions.add(transition);
});
}
const markerInstances = queue.markerInstances;
if (markerInstances !== null) {
markerInstances.forEach(markerInstance => {
const markerTransitions = markerInstance.transitions;
// There should only be a few tracing marker transitions because
// they should be only associated with the transition that
// caused them
if (markerTransitions !== null) {
markerTransitions.forEach(transition => {
if (instance._transitions === null) {
instance._transitions = new Set();
} else if (instance._transitions.has(transition)) {
if (markerInstance.pendingBoundaries === null) {
markerInstance.pendingBoundaries = new Map();
}
if (instance._pendingMarkers === null) {
instance._pendingMarkers = new Set();
}
instance._pendingMarkers.add(markerInstance);
}
});
}
});
}
}
finishedWork.updateQueue = null;
}
commitTransitionProgress(finishedWork);
// TODO: Refactor this into an if/else branch
if (!isHidden) {
instance._transitions = null;
instance._pendingMarkers = null;
}
}
}
function commitCachePassiveMountEffect(
current: Fiber | null,
finishedWork: Fiber,
) {
if (enableCache) {
let previousCache: Cache | null = null;
if (finishedWork.alternate !== null) {
previousCache = finishedWork.alternate.memoizedState.cache;
}
const nextCache = finishedWork.memoizedState.cache;
// Retain/release the cache. In theory the cache component
// could be "borrowing" a cache instance owned by some parent,
// in which case we could avoid retaining/releasing. But it
// is non-trivial to determine when that is the case, so we
// always retain/release.
if (nextCache !== previousCache) {
retainCache(nextCache);
if (previousCache != null) {
releaseCache(previousCache);
}
}
}
}
function commitTracingMarkerPassiveMountEffect(finishedWork: Fiber) {
// Get the transitions that were initiatized during the render
// and add a start transition callback for each of them
// We will only call this on initial mount of the tracing marker
// only if there are no suspense children
const instance = finishedWork.stateNode;
if (instance.transitions !== null && instance.pendingBoundaries === null) {
addMarkerCompleteCallbackToPendingTransition(
finishedWork.memoizedProps.name,
instance.transitions,
);
instance.transitions = null;
instance.pendingBoundaries = null;
instance.aborts = null;
instance.name = null;
}
}
export function commitPassiveMountEffects(
root: FiberRoot,
finishedWork: Fiber,
committedLanes: Lanes,
committedTransitions: Array<Transition> | null,
): void {
setCurrentDebugFiberInDEV(finishedWork);
commitPassiveMountOnFiber(
root,
finishedWork,
committedLanes,
committedTransitions,
);
resetCurrentDebugFiberInDEV();
}
function recursivelyTraversePassiveMountEffects(
root: FiberRoot,
parentFiber: Fiber,
committedLanes: Lanes,
committedTransitions: Array<Transition> | null,
) {
const prevDebugFiber = getCurrentDebugFiberInDEV();
if (parentFiber.subtreeFlags & PassiveMask) {
let child = parentFiber.child;
while (child !== null) {
setCurrentDebugFiberInDEV(child);
commitPassiveMountOnFiber(
root,
child,
committedLanes,
committedTransitions,
);
child = child.sibling;
}
}
setCurrentDebugFiberInDEV(prevDebugFiber);
}
function commitPassiveMountOnFiber(
finishedRoot: FiberRoot,
finishedWork: Fiber,
committedLanes: Lanes,
committedTransitions: Array<Transition> | null,
): void {
// When updating this function, also update reconnectPassiveEffects, which does
// most of the same things when an offscreen tree goes from hidden -> visible,
// or when toggling effects inside a hidden tree.
const flags = finishedWork.flags;
switch (finishedWork.tag) {
case FunctionComponent:
case ForwardRef:
case SimpleMemoComponent: {
recursivelyTraversePassiveMountEffects(
finishedRoot,
finishedWork,
committedLanes,
committedTransitions,
);
if (flags & Passive) {
commitHookPassiveMountEffects(
finishedWork,
HookPassive | HookHasEffect,
);
}
break;
}
case HostRoot: {
recursivelyTraversePassiveMountEffects(
finishedRoot,
finishedWork,
committedLanes,
committedTransitions,
);
if (flags & Passive) {
if (enableCache) {
let previousCache: Cache | null = null;
if (finishedWork.alternate !== null) {
previousCache = finishedWork.alternate.memoizedState.cache;
}
const nextCache = finishedWork.memoizedState.cache;
// Retain/release the root cache.
// Note that on initial mount, previousCache and nextCache will be the same
// and this retain won't occur. To counter this, we instead retain the HostRoot's
// initial cache when creating the root itself (see createFiberRoot() in
// ReactFiberRoot.js). Subsequent updates that change the cache are reflected
// here, such that previous/next caches are retained correctly.
if (nextCache !== previousCache) {
retainCache(nextCache);
if (previousCache != null) {
releaseCache(previousCache);
}
}
}
if (enableTransitionTracing) {
// Get the transitions that were initiatized during the render
// and add a start transition callback for each of them
const root: FiberRoot = finishedWork.stateNode;
const incompleteTransitions = root.incompleteTransitions;
// Initial render
if (committedTransitions !== null) {
committedTransitions.forEach(transition => {
addTransitionStartCallbackToPendingTransition(transition);
});
clearTransitionsForLanes(finishedRoot, committedLanes);
}
incompleteTransitions.forEach((markerInstance, transition) => {
const pendingBoundaries = markerInstance.pendingBoundaries;
if (pendingBoundaries === null || pendingBoundaries.size === 0) {
if (markerInstance.aborts === null) {
addTransitionCompleteCallbackToPendingTransition(transition);
}
incompleteTransitions.delete(transition);
}
});
clearTransitionsForLanes(finishedRoot, committedLanes);
}
}
break;
}
case LegacyHiddenComponent: {
if (enableLegacyHidden) {
recursivelyTraversePassiveMountEffects(
finishedRoot,
finishedWork,
committedLanes,
committedTransitions,
);
if (flags & Passive) {
const current = finishedWork.alternate;
const instance: OffscreenInstance = finishedWork.stateNode;
commitOffscreenPassiveMountEffects(current, finishedWork, instance);
}
}
break;
}
case OffscreenComponent: {
// TODO: Pass `current` as argument to this function
const instance: OffscreenInstance = finishedWork.stateNode;
const nextState: OffscreenState | null = finishedWork.memoizedState;
const isHidden = nextState !== null;
if (isHidden) {
if (instance._visibility & OffscreenPassiveEffectsConnected) {
// The effects are currently connected. Update them.
recursivelyTraversePassiveMountEffects(
finishedRoot,
finishedWork,
committedLanes,
committedTransitions,
);
} else {
if (finishedWork.mode & ConcurrentMode) {
// The effects are currently disconnected. Since the tree is hidden,
// don't connect them. This also applies to the initial render.
if (enableCache || enableTransitionTracing) {
// "Atomic" effects are ones that need to fire on every commit,
// even during pre-rendering. An example is updating the reference
// count on cache instances.
recursivelyTraverseAtomicPassiveEffects(
finishedRoot,
finishedWork,
committedLanes,
committedTransitions,
);
}
} else {
// Legacy Mode: Fire the effects even if the tree is hidden.
instance._visibility |= OffscreenPassiveEffectsConnected;
recursivelyTraversePassiveMountEffects(
finishedRoot,
finishedWork,
committedLanes,
committedTransitions,
);
}
}
} else {
// Tree is visible
if (instance._visibility & OffscreenPassiveEffectsConnected) {
// The effects are currently connected. Update them.
recursivelyTraversePassiveMountEffects(
finishedRoot,
finishedWork,
committedLanes,
committedTransitions,
);
} else {
// The effects are currently disconnected. Reconnect them, while also
// firing effects inside newly mounted trees. This also applies to
// the initial render.
instance._visibility |= OffscreenPassiveEffectsConnected;
const includeWorkInProgressEffects =
(finishedWork.subtreeFlags & PassiveMask) !== NoFlags;
recursivelyTraverseReconnectPassiveEffects(
finishedRoot,
finishedWork,
committedLanes,
committedTransitions,
includeWorkInProgressEffects,
);
}
}
if (flags & Passive) {
const current = finishedWork.alternate;
commitOffscreenPassiveMountEffects(current, finishedWork, instance);
}
break;
}
case CacheComponent: {
recursivelyTraversePassiveMountEffects(
finishedRoot,
finishedWork,
committedLanes,
committedTransitions,
);
if (flags & Passive) {
// TODO: Pass `current` as argument to this function
const current = finishedWork.alternate;
commitCachePassiveMountEffect(current, finishedWork);
}
break;
}
case TracingMarkerComponent: {
if (enableTransitionTracing) {
recursivelyTraversePassiveMountEffects(
finishedRoot,
finishedWork,
committedLanes,
committedTransitions,
);
if (flags & Passive) {
commitTracingMarkerPassiveMountEffect(finishedWork);
}
break;
}
// Intentional fallthrough to next branch
}
default: {
recursivelyTraversePassiveMountEffects(
finishedRoot,
finishedWork,
committedLanes,
committedTransitions,
);
break;
}
}
}
function recursivelyTraverseReconnectPassiveEffects(
finishedRoot: FiberRoot,
parentFiber: Fiber,
committedLanes: Lanes,
committedTransitions: Array<Transition> | null,
includeWorkInProgressEffects: boolean,
) {
// This function visits both newly finished work and nodes that were re-used
// from a previously committed tree. We cannot check non-static flags if the
// node was reused.
const childShouldIncludeWorkInProgressEffects =
includeWorkInProgressEffects &&
(parentFiber.subtreeFlags & PassiveMask) !== NoFlags;
// TODO (Offscreen) Check: flags & (RefStatic | LayoutStatic)
const prevDebugFiber = getCurrentDebugFiberInDEV();
let child = parentFiber.child;
while (child !== null) {
reconnectPassiveEffects(
finishedRoot,
child,
committedLanes,
committedTransitions,
childShouldIncludeWorkInProgressEffects,
);
child = child.sibling;
}
setCurrentDebugFiberInDEV(prevDebugFiber);
}
export function reconnectPassiveEffects(
finishedRoot: FiberRoot,
finishedWork: Fiber,
committedLanes: Lanes,
committedTransitions: Array<Transition> | null,
// This function visits both newly finished work and nodes that were re-used
// from a previously committed tree. We cannot check non-static flags if the
// node was reused.
includeWorkInProgressEffects: boolean,
) {
const flags = finishedWork.flags;
switch (finishedWork.tag) {
case FunctionComponent:
case ForwardRef:
case SimpleMemoComponent: {
recursivelyTraverseReconnectPassiveEffects(
finishedRoot,
finishedWork,
committedLanes,
committedTransitions,
includeWorkInProgressEffects,
);
// TODO: Check for PassiveStatic flag
commitHookPassiveMountEffects(finishedWork, HookPassive);
break;
}
// Unlike commitPassiveMountOnFiber, we don't need to handle HostRoot
// because this function only visits nodes that are inside an
// Offscreen fiber.
// case HostRoot: {
// ...
// }
case LegacyHiddenComponent: {
if (enableLegacyHidden) {
recursivelyTraverseReconnectPassiveEffects(
finishedRoot,
finishedWork,
committedLanes,
committedTransitions,
includeWorkInProgressEffects,
);
if (includeWorkInProgressEffects && flags & Passive) {
// TODO: Pass `current` as argument to this function
const current: Fiber | null = finishedWork.alternate;
const instance: OffscreenInstance = finishedWork.stateNode;
commitOffscreenPassiveMountEffects(current, finishedWork, instance);
}
}
break;
}
case OffscreenComponent: {
const instance: OffscreenInstance = finishedWork.stateNode;
const nextState: OffscreenState | null = finishedWork.memoizedState;
const isHidden = nextState !== null;
if (isHidden) {
if (instance._visibility & OffscreenPassiveEffectsConnected) {
// The effects are currently connected. Update them.
recursivelyTraverseReconnectPassiveEffects(
finishedRoot,
finishedWork,
committedLanes,
committedTransitions,
includeWorkInProgressEffects,
);
} else {
if (finishedWork.mode & ConcurrentMode) {
// The effects are currently disconnected. Since the tree is hidden,
// don't connect them. This also applies to the initial render.
if (enableCache || enableTransitionTracing) {
// "Atomic" effects are ones that need to fire on every commit,
// even during pre-rendering. An example is updating the reference
// count on cache instances.
recursivelyTraverseAtomicPassiveEffects(
finishedRoot,
finishedWork,
committedLanes,
committedTransitions,
);
}
} else {
// Legacy Mode: Fire the effects even if the tree is hidden.
instance._visibility |= OffscreenPassiveEffectsConnected;
recursivelyTraverseReconnectPassiveEffects(
finishedRoot,
finishedWork,
committedLanes,
committedTransitions,
includeWorkInProgressEffects,
);
}
}
} else {
// Tree is visible
// Since we're already inside a reconnecting tree, it doesn't matter
// whether the effects are currently connected. In either case, we'll
// continue traversing the tree and firing all the effects.
//
// We do need to set the "connected" flag on the instance, though.
instance._visibility |= OffscreenPassiveEffectsConnected;
recursivelyTraverseReconnectPassiveEffects(
finishedRoot,
finishedWork,
committedLanes,
committedTransitions,
includeWorkInProgressEffects,
);
}
if (includeWorkInProgressEffects && flags & Passive) {
// TODO: Pass `current` as argument to this function
const current: Fiber | null = finishedWork.alternate;
commitOffscreenPassiveMountEffects(current, finishedWork, instance);
}
break;
}
case CacheComponent: {
recursivelyTraverseReconnectPassiveEffects(
finishedRoot,
finishedWork,
committedLanes,
committedTransitions,
includeWorkInProgressEffects,
);
if (includeWorkInProgressEffects && flags & Passive) {
// TODO: Pass `current` as argument to this function
const current = finishedWork.alternate;
commitCachePassiveMountEffect(current, finishedWork);
}
break;
}
case TracingMarkerComponent: {
if (enableTransitionTracing) {
recursivelyTraverseReconnectPassiveEffects(
finishedRoot,
finishedWork,
committedLanes,
committedTransitions,
includeWorkInProgressEffects,
);
if (includeWorkInProgressEffects && flags & Passive) {
commitTracingMarkerPassiveMountEffect(finishedWork);
}
break;
}
// Intentional fallthrough to next branch
}
default: {
recursivelyTraverseReconnectPassiveEffects(
finishedRoot,
finishedWork,
committedLanes,
committedTransitions,
includeWorkInProgressEffects,
);
break;
}
}
}
function recursivelyTraverseAtomicPassiveEffects(
finishedRoot: FiberRoot,
parentFiber: Fiber,
committedLanes: Lanes,
committedTransitions: Array<Transition> | null,
) {
// "Atomic" effects are ones that need to fire on every commit, even during
// pre-rendering. We call this function when traversing a hidden tree whose
// regular effects are currently disconnected.
const prevDebugFiber = getCurrentDebugFiberInDEV();
// TODO: Add special flag for atomic effects
if (parentFiber.subtreeFlags & PassiveMask) {
let child = parentFiber.child;
while (child !== null) {
setCurrentDebugFiberInDEV(child);
commitAtomicPassiveEffects(
finishedRoot,
child,
committedLanes,
committedTransitions,
);
child = child.sibling;
}
}
setCurrentDebugFiberInDEV(prevDebugFiber);
}
function commitAtomicPassiveEffects(
finishedRoot: FiberRoot,
finishedWork: Fiber,
committedLanes: Lanes,
committedTransitions: Array<Transition> | null,
) {
// "Atomic" effects are ones that need to fire on every commit, even during
// pre-rendering. We call this function when traversing a hidden tree whose
// regular effects are currently disconnected.
const flags = finishedWork.flags;
switch (finishedWork.tag) {
case OffscreenComponent: {
recursivelyTraverseAtomicPassiveEffects(
finishedRoot,
finishedWork,
committedLanes,
committedTransitions,
);
if (flags & Passive) {
// TODO: Pass `current` as argument to this function
const current = finishedWork.alternate;
const instance: OffscreenInstance = finishedWork.stateNode;
commitOffscreenPassiveMountEffects(current, finishedWork, instance);
}
break;
}
case CacheComponent: {
recursivelyTraverseAtomicPassiveEffects(
finishedRoot,
finishedWork,
committedLanes,
committedTransitions,
);
if (flags & Passive) {
// TODO: Pass `current` as argument to this function
const current = finishedWork.alternate;
commitCachePassiveMountEffect(current, finishedWork);
}
break;
}
default: {
recursivelyTraverseAtomicPassiveEffects(
finishedRoot,
finishedWork,
committedLanes,
committedTransitions,
);
break;
}
}
}
export function commitPassiveUnmountEffects(finishedWork: Fiber): void {
setCurrentDebugFiberInDEV(finishedWork);
commitPassiveUnmountOnFiber(finishedWork);
resetCurrentDebugFiberInDEV();
}
// If we're inside a brand new tree, or a tree that was already visible, then we
// should only suspend host components that have a ShouldSuspendCommit flag.
// Components without it haven't changed since the last commit, so we can skip
// over those.
//
// When we enter a tree that is being revealed (going from hidden -> visible),
// we need to suspend _any_ component that _may_ suspend. Even if they're
// already in the "current" tree. Because their visibility has changed, the
// browser may not have prerendered them yet. So we check the MaySuspendCommit
// flag instead.
let suspenseyCommitFlag = ShouldSuspendCommit;
export function accumulateSuspenseyCommit(finishedWork: Fiber): void {
accumulateSuspenseyCommitOnFiber(finishedWork);
}
function recursivelyAccumulateSuspenseyCommit(parentFiber: Fiber): void {
if (parentFiber.subtreeFlags & suspenseyCommitFlag) {
let child = parentFiber.child;
while (child !== null) {
accumulateSuspenseyCommitOnFiber(child);
child = child.sibling;
}
}
}
function accumulateSuspenseyCommitOnFiber(fiber: Fiber) {
switch (fiber.tag) {
case HostHoistable: {
recursivelyAccumulateSuspenseyCommit(fiber);
if (fiber.flags & suspenseyCommitFlag) {
if (fiber.memoizedState !== null) {
suspendResource(
// This should always be set by visiting HostRoot first
(currentHoistableRoot: any),
fiber.memoizedState,
fiber.memoizedProps,
);
} else {
const type = fiber.type;
const props = fiber.memoizedProps;
suspendInstance(type, props);
}
}
break;
}
case HostComponent: {
recursivelyAccumulateSuspenseyCommit(fiber);
if (fiber.flags & suspenseyCommitFlag) {
const type = fiber.type;
const props = fiber.memoizedProps;
suspendInstance(type, props);
}
break;
}
case HostRoot:
case HostPortal: {
if (enableFloat && supportsResources) {
const previousHoistableRoot = currentHoistableRoot;
const container: Container = fiber.stateNode.containerInfo;
currentHoistableRoot = getHoistableRoot(container);
recursivelyAccumulateSuspenseyCommit(fiber);
currentHoistableRoot = previousHoistableRoot;
} else {
recursivelyAccumulateSuspenseyCommit(fiber);
}
break;
}
case OffscreenComponent: {
const isHidden = (fiber.memoizedState: OffscreenState | null) !== null;
if (isHidden) {
// Don't suspend in hidden trees
} else {
const current = fiber.alternate;
const wasHidden =
current !== null &&
(current.memoizedState: OffscreenState | null) !== null;
if (wasHidden) {
// This tree is being revealed. Visit all newly visible suspensey
// instances, even if they're in the current tree.
const prevFlags = suspenseyCommitFlag;
suspenseyCommitFlag = MaySuspendCommit;
recursivelyAccumulateSuspenseyCommit(fiber);
suspenseyCommitFlag = prevFlags;
} else {
recursivelyAccumulateSuspenseyCommit(fiber);
}
}
break;
}
default: {
recursivelyAccumulateSuspenseyCommit(fiber);
}
}
}
function detachAlternateSiblings(parentFiber: Fiber) {
// A fiber was deleted from this parent fiber, but it's still part of the
// previous (alternate) parent fiber's list of children. Because children
// are a linked list, an earlier sibling that's still alive will be
// connected to the deleted fiber via its `alternate`:
//
// live fiber --alternate--> previous live fiber --sibling--> deleted
// fiber
//
// We can't disconnect `alternate` on nodes that haven't been deleted yet,
// but we can disconnect the `sibling` and `child` pointers.
const previousFiber = parentFiber.alternate;
if (previousFiber !== null) {
let detachedChild = previousFiber.child;
if (detachedChild !== null) {
previousFiber.child = null;
do {
// $FlowFixMe[incompatible-use] found when upgrading Flow
const detachedSibling = detachedChild.sibling;
// $FlowFixMe[incompatible-use] found when upgrading Flow
detachedChild.sibling = null;
detachedChild = detachedSibling;
} while (detachedChild !== null);
}
}
}
function commitHookPassiveUnmountEffects(
finishedWork: Fiber,
nearestMountedAncestor: null | Fiber,
hookFlags: HookFlags,
) {
if (shouldProfile(finishedWork)) {
startPassiveEffectTimer();
commitHookEffectListUnmount(
hookFlags,
finishedWork,
nearestMountedAncestor,
);
recordPassiveEffectDuration(finishedWork);
} else {
commitHookEffectListUnmount(
hookFlags,
finishedWork,
nearestMountedAncestor,
);
}
}
function recursivelyTraversePassiveUnmountEffects(parentFiber: Fiber): void {
// Deletions effects can be scheduled on any fiber type. They need to happen
// before the children effects have fired.
const deletions = parentFiber.deletions;
if ((parentFiber.flags & ChildDeletion) !== NoFlags) {
if (deletions !== null) {
for (let i = 0; i < deletions.length; i++) {
const childToDelete = deletions[i];
// TODO: Convert this to use recursion
nextEffect = childToDelete;
commitPassiveUnmountEffectsInsideOfDeletedTree_begin(
childToDelete,
parentFiber,
);
}
}
detachAlternateSiblings(parentFiber);
}
const prevDebugFiber = getCurrentDebugFiberInDEV();
// TODO: Split PassiveMask into separate masks for mount and unmount?
if (parentFiber.subtreeFlags & PassiveMask) {
let child = parentFiber.child;
while (child !== null) {
setCurrentDebugFiberInDEV(child);
commitPassiveUnmountOnFiber(child);
child = child.sibling;
}
}
setCurrentDebugFiberInDEV(prevDebugFiber);
}
function commitPassiveUnmountOnFiber(finishedWork: Fiber): void {
switch (finishedWork.tag) {
case FunctionComponent:
case ForwardRef:
case SimpleMemoComponent: {
recursivelyTraversePassiveUnmountEffects(finishedWork);
if (finishedWork.flags & Passive) {
commitHookPassiveUnmountEffects(
finishedWork,
finishedWork.return,
HookPassive | HookHasEffect,
);
}
break;
}
case OffscreenComponent: {
const instance: OffscreenInstance = finishedWork.stateNode;
const nextState: OffscreenState | null = finishedWork.memoizedState;
const isHidden = nextState !== null;
if (
isHidden &&
instance._visibility & OffscreenPassiveEffectsConnected &&
// For backwards compatibility, don't unmount when a tree suspends. In
// the future we may change this to unmount after a delay.
(finishedWork.return === null ||
finishedWork.return.tag !== SuspenseComponent)
) {
// The effects are currently connected. Disconnect them.
// TODO: Add option or heuristic to delay before disconnecting the
// effects. Then if the tree reappears before the delay has elapsed, we
// can skip toggling the effects entirely.
instance._visibility &= ~OffscreenPassiveEffectsConnected;
recursivelyTraverseDisconnectPassiveEffects(finishedWork);
} else {
recursivelyTraversePassiveUnmountEffects(finishedWork);
}
break;
}
default: {
recursivelyTraversePassiveUnmountEffects(finishedWork);
break;
}
}
}
function recursivelyTraverseDisconnectPassiveEffects(parentFiber: Fiber): void {
// Deletions effects can be scheduled on any fiber type. They need to happen
// before the children effects have fired.
const deletions = parentFiber.deletions;
if ((parentFiber.flags & ChildDeletion) !== NoFlags) {
if (deletions !== null) {
for (let i = 0; i < deletions.length; i++) {
const childToDelete = deletions[i];
// TODO: Convert this to use recursion
nextEffect = childToDelete;
commitPassiveUnmountEffectsInsideOfDeletedTree_begin(
childToDelete,
parentFiber,
);
}
}
detachAlternateSiblings(parentFiber);
}
const prevDebugFiber = getCurrentDebugFiberInDEV();
// TODO: Check PassiveStatic flag
let child = parentFiber.child;
while (child !== null) {
setCurrentDebugFiberInDEV(child);
disconnectPassiveEffect(child);
child = child.sibling;
}
setCurrentDebugFiberInDEV(prevDebugFiber);
}
export function disconnectPassiveEffect(finishedWork: Fiber): void {
switch (finishedWork.tag) {
case FunctionComponent:
case ForwardRef:
case SimpleMemoComponent: {
// TODO: Check PassiveStatic flag
commitHookPassiveUnmountEffects(
finishedWork,
finishedWork.return,
HookPassive,
);
// When disconnecting passive effects, we fire the effects in the same
// order as during a deletiong: parent before child
recursivelyTraverseDisconnectPassiveEffects(finishedWork);
break;
}
case OffscreenComponent: {
const instance: OffscreenInstance = finishedWork.stateNode;
if (instance._visibility & OffscreenPassiveEffectsConnected) {
instance._visibility &= ~OffscreenPassiveEffectsConnected;
recursivelyTraverseDisconnectPassiveEffects(finishedWork);
} else {
// The effects are already disconnected.
}
break;
}
default: {
recursivelyTraverseDisconnectPassiveEffects(finishedWork);
break;
}
}
}
function commitPassiveUnmountEffectsInsideOfDeletedTree_begin(
deletedSubtreeRoot: Fiber,
nearestMountedAncestor: Fiber | null,
) {
while (nextEffect !== null) {
const fiber = nextEffect;
// Deletion effects fire in parent -> child order
// TODO: Check if fiber has a PassiveStatic flag
setCurrentDebugFiberInDEV(fiber);
commitPassiveUnmountInsideDeletedTreeOnFiber(fiber, nearestMountedAncestor);
resetCurrentDebugFiberInDEV();
const child = fiber.child;
// TODO: Only traverse subtree if it has a PassiveStatic flag.
if (child !== null) {
child.return = fiber;
nextEffect = child;
} else {
commitPassiveUnmountEffectsInsideOfDeletedTree_complete(
deletedSubtreeRoot,
);
}
}
}
function commitPassiveUnmountEffectsInsideOfDeletedTree_complete(
deletedSubtreeRoot: Fiber,
) {
while (nextEffect !== null) {
const fiber = nextEffect;
const sibling = fiber.sibling;
const returnFiber = fiber.return;
// Recursively traverse the entire deleted tree and clean up fiber fields.
// This is more aggressive than ideal, and the long term goal is to only
// have to detach the deleted tree at the root.
detachFiberAfterEffects(fiber);
if (fiber === deletedSubtreeRoot) {
nextEffect = null;
return;
}
if (sibling !== null) {
sibling.return = returnFiber;
nextEffect = sibling;
return;
}
nextEffect = returnFiber;
}
}
function commitPassiveUnmountInsideDeletedTreeOnFiber(
current: Fiber,
nearestMountedAncestor: Fiber | null,
): void {
switch (current.tag) {
case FunctionComponent:
case ForwardRef:
case SimpleMemoComponent: {
commitHookPassiveUnmountEffects(
current,
nearestMountedAncestor,
HookPassive,
);
break;
}
// TODO: run passive unmount effects when unmounting a root.
// Because passive unmount effects are not currently run,
// the cache instance owned by the root will never be freed.
// When effects are run, the cache should be freed here:
// case HostRoot: {
// if (enableCache) {
// const cache = current.memoizedState.cache;
// releaseCache(cache);
// }
// break;
// }
case LegacyHiddenComponent:
case OffscreenComponent: {
if (enableCache) {
if (
current.memoizedState !== null &&
current.memoizedState.cachePool !== null
) {
const cache: Cache = current.memoizedState.cachePool.pool;
// Retain/release the cache used for pending (suspended) nodes.
// Note that this is only reached in the non-suspended/visible case:
// when the content is suspended/hidden, the retain/release occurs
// via the parent Suspense component (see case above).
if (cache != null) {
retainCache(cache);
}
}
}
break;
}
case SuspenseComponent: {
if (enableTransitionTracing) {
// We need to mark this fiber's parents as deleted
const offscreenFiber: Fiber = (current.child: any);
const instance: OffscreenInstance = offscreenFiber.stateNode;
const transitions = instance._transitions;
if (transitions !== null) {
const abortReason = {
reason: 'suspense',
name: current.memoizedProps.unstable_name || null,
};
if (
current.memoizedState === null ||
current.memoizedState.dehydrated === null
) {
abortParentMarkerTransitionsForDeletedFiber(
offscreenFiber,
abortReason,
transitions,
instance,
true,
);
if (nearestMountedAncestor !== null) {
abortParentMarkerTransitionsForDeletedFiber(
nearestMountedAncestor,
abortReason,
transitions,
instance,
false,
);
}
}
}
}
break;
}
case CacheComponent: {
if (enableCache) {
const cache = current.memoizedState.cache;
releaseCache(cache);
}
break;
}
case TracingMarkerComponent: {
if (enableTransitionTracing) {
// We need to mark this fiber's parents as deleted
const instance: TracingMarkerInstance = current.stateNode;
const transitions = instance.transitions;
if (transitions !== null) {
const abortReason = {
reason: 'marker',
name: current.memoizedProps.name,
};
abortParentMarkerTransitionsForDeletedFiber(
current,
abortReason,
transitions,
null,
true,
);
if (nearestMountedAncestor !== null) {
abortParentMarkerTransitionsForDeletedFiber(
nearestMountedAncestor,
abortReason,
transitions,
null,
false,
);
}
}
}
break;
}
}
}
function invokeLayoutEffectMountInDEV(fiber: Fiber): void {
if (__DEV__) {
// We don't need to re-check StrictEffectsMode here.
// This function is only called if that check has already passed.
switch (fiber.tag) {
case FunctionComponent:
case ForwardRef:
case SimpleMemoComponent: {
try {
commitHookEffectListMount(HookLayout | HookHasEffect, fiber);
} catch (error) {
captureCommitPhaseError(fiber, fiber.return, error);
}
break;
}
case ClassComponent: {
const instance = fiber.stateNode;
if (typeof instance.componentDidMount === 'function') {
try {
instance.componentDidMount();
} catch (error) {
captureCommitPhaseError(fiber, fiber.return, error);
}
}
break;
}
}
}
}
function invokePassiveEffectMountInDEV(fiber: Fiber): void {
if (__DEV__) {
// We don't need to re-check StrictEffectsMode here.
// This function is only called if that check has already passed.
switch (fiber.tag) {
case FunctionComponent:
case ForwardRef:
case SimpleMemoComponent: {
try {
commitHookEffectListMount(HookPassive | HookHasEffect, fiber);
} catch (error) {
captureCommitPhaseError(fiber, fiber.return, error);
}
break;
}
}
}
}
function invokeLayoutEffectUnmountInDEV(fiber: Fiber): void {
if (__DEV__) {
// We don't need to re-check StrictEffectsMode here.
// This function is only called if that check has already passed.
switch (fiber.tag) {
case FunctionComponent:
case ForwardRef:
case SimpleMemoComponent: {
try {
commitHookEffectListUnmount(
HookLayout | HookHasEffect,
fiber,
fiber.return,
);
} catch (error) {
captureCommitPhaseError(fiber, fiber.return, error);
}
break;
}
case ClassComponent: {
const instance = fiber.stateNode;
if (typeof instance.componentWillUnmount === 'function') {
safelyCallComponentWillUnmount(fiber, fiber.return, instance);
}
break;
}
}
}
}
function invokePassiveEffectUnmountInDEV(fiber: Fiber): void {
if (__DEV__) {
// We don't need to re-check StrictEffectsMode here.
// This function is only called if that check has already passed.
switch (fiber.tag) {
case FunctionComponent:
case ForwardRef:
case SimpleMemoComponent: {
try {
commitHookEffectListUnmount(
HookPassive | HookHasEffect,
fiber,
fiber.return,
);
} catch (error) {
captureCommitPhaseError(fiber, fiber.return, error);
}
}
}
}
}
export {
commitPlacement,
commitAttachRef,
invokeLayoutEffectMountInDEV,
invokeLayoutEffectUnmountInDEV,
invokePassiveEffectMountInDEV,
invokePassiveEffectUnmountInDEV,
};