diff --git a/.eslintrc.js b/.eslintrc.js index 591952fd36177..0668458d985ca 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -113,18 +113,7 @@ module.exports = { 'react-internal/warning-args': ERROR, 'react-internal/no-production-logging': ERROR, 'react-internal/no-cross-fork-imports': ERROR, - 'react-internal/no-cross-fork-types': [ - ERROR, - { - old: [ - 'firstEffect', - 'nextEffect', - // Disabled because it's also used by the Hook type. - // 'lastEffect', - ], - new: ['subtreeFlags'], - }, - ], + 'react-internal/no-cross-fork-types': ERROR, }, overrides: [ diff --git a/packages/react-dom/src/__tests__/ReactDOMServerPartialHydration-test.internal.js b/packages/react-dom/src/__tests__/ReactDOMServerPartialHydration-test.internal.js index 9a897789b47ca..b06440f8a68d3 100644 --- a/packages/react-dom/src/__tests__/ReactDOMServerPartialHydration-test.internal.js +++ b/packages/react-dom/src/__tests__/ReactDOMServerPartialHydration-test.internal.js @@ -367,13 +367,9 @@ describe('ReactDOMServerPartialHydration', () => { // This is a new node. expect(span).not.toBe(span2); - if (gate(flags => flags.new)) { - // The effects list refactor causes this to be null because the Suspense Offscreen's child - // is null. However, since we can't hydrate Suspense in legacy this change in behavior is ok - expect(ref.current).toBe(null); - } else { - expect(ref.current).toBe(span2); - } + // The effects list refactor causes this to be null because the Suspense Offscreen's child + // is null. However, since we can't hydrate Suspense in legacy this change in behavior is ok + expect(ref.current).toBe(null); // Resolving the promise should render the final content. suspend = false; diff --git a/packages/react-reconciler/src/ReactChildFiber.old.js b/packages/react-reconciler/src/ReactChildFiber.old.js index 0aea86f033672..a68c93b812f48 100644 --- a/packages/react-reconciler/src/ReactChildFiber.old.js +++ b/packages/react-reconciler/src/ReactChildFiber.old.js @@ -15,7 +15,7 @@ import type {Fiber} from './ReactInternalTypes'; import type {Lanes} from './ReactFiberLane'; import getComponentName from 'shared/getComponentName'; -import {Placement, Deletion} from './ReactFiberFlags'; +import {Deletion, Placement} from './ReactFiberFlags'; import { getIteratorFn, REACT_ELEMENT_TYPE, @@ -277,20 +277,13 @@ function ChildReconciler(shouldTrackSideEffects) { // Noop. return; } - // Deletions are added in reversed order so we add it to the front. - // At this point, the return fiber's effect list is empty except for - // deletions, so we can just append the deletion to the list. The remaining - // effects aren't added until the complete phase. Once we implement - // resuming, this may not be true. - const last = returnFiber.lastEffect; - if (last !== null) { - last.nextEffect = childToDelete; - returnFiber.lastEffect = childToDelete; + const deletions = returnFiber.deletions; + if (deletions === null) { + returnFiber.deletions = [childToDelete]; + returnFiber.flags |= Deletion; } else { - returnFiber.firstEffect = returnFiber.lastEffect = childToDelete; + deletions.push(childToDelete); } - childToDelete.nextEffect = null; - childToDelete.flags = Deletion; } function deleteRemainingChildren( diff --git a/packages/react-reconciler/src/ReactFiber.old.js b/packages/react-reconciler/src/ReactFiber.old.js index 6906869acee90..bb7a24f257f11 100644 --- a/packages/react-reconciler/src/ReactFiber.old.js +++ b/packages/react-reconciler/src/ReactFiber.old.js @@ -29,7 +29,7 @@ import { enableScopeAPI, enableBlocksAPI, } from 'shared/ReactFeatureFlags'; -import {NoFlags, Placement} from './ReactFiberFlags'; +import {NoFlags, Placement, StaticMask} from './ReactFiberFlags'; import {ConcurrentRoot, BlockingRoot} from './ReactRootTags'; import { IndeterminateComponent, @@ -144,10 +144,8 @@ function FiberNode( // Effects this.flags = NoFlags; - this.nextEffect = null; - - this.firstEffect = null; - this.lastEffect = null; + this.subtreeFlags = NoFlags; + this.deletions = null; this.lanes = NoLanes; this.childLanes = NoLanes; @@ -285,13 +283,8 @@ export function createWorkInProgress(current: Fiber, pendingProps: any): Fiber { workInProgress.type = current.type; // We already have an alternate. - // Reset the effect tag. - workInProgress.flags = NoFlags; - - // The effect list is no longer valid. - workInProgress.nextEffect = null; - workInProgress.firstEffect = null; - workInProgress.lastEffect = null; + workInProgress.subtreeFlags = NoFlags; + workInProgress.deletions = null; if (enableProfilerTimer) { // We intentionally reset, rather than copy, actualDuration & actualStartTime. @@ -303,6 +296,9 @@ export function createWorkInProgress(current: Fiber, pendingProps: any): Fiber { } } + // Reset all effects except static ones. + // Static effects are not specific to a render. + workInProgress.flags = current.flags & StaticMask; workInProgress.childLanes = current.childLanes; workInProgress.lanes = current.lanes; @@ -368,11 +364,6 @@ export function resetWorkInProgress(workInProgress: Fiber, renderLanes: Lanes) { // that child fiber is setting, not the reconciliation. workInProgress.flags &= Placement; - // The effect list is no longer valid. - workInProgress.nextEffect = null; - workInProgress.firstEffect = null; - workInProgress.lastEffect = null; - const current = workInProgress.alternate; if (current === null) { // Reset to createFiber's initial values. @@ -380,6 +371,7 @@ export function resetWorkInProgress(workInProgress: Fiber, renderLanes: Lanes) { workInProgress.lanes = renderLanes; workInProgress.child = null; + workInProgress.subtreeFlags = NoFlags; workInProgress.memoizedProps = null; workInProgress.memoizedState = null; workInProgress.updateQueue = null; @@ -400,6 +392,8 @@ export function resetWorkInProgress(workInProgress: Fiber, renderLanes: Lanes) { workInProgress.lanes = current.lanes; workInProgress.child = current.child; + workInProgress.subtreeFlags = current.subtreeFlags; + workInProgress.deletions = null; workInProgress.memoizedProps = current.memoizedProps; workInProgress.memoizedState = current.memoizedState; workInProgress.updateQueue = current.updateQueue; @@ -822,9 +816,8 @@ export function assignFiberPropertiesInDEV( target.dependencies = source.dependencies; target.mode = source.mode; target.flags = source.flags; - target.nextEffect = source.nextEffect; - target.firstEffect = source.firstEffect; - target.lastEffect = source.lastEffect; + target.subtreeFlags = source.subtreeFlags; + target.deletions = source.deletions; target.lanes = source.lanes; target.childLanes = source.childLanes; target.alternate = source.alternate; diff --git a/packages/react-reconciler/src/ReactFiberBeginWork.old.js b/packages/react-reconciler/src/ReactFiberBeginWork.old.js index ef41dff89a0ea..de069cdd1b57a 100644 --- a/packages/react-reconciler/src/ReactFiberBeginWork.old.js +++ b/packages/react-reconciler/src/ReactFiberBeginWork.old.js @@ -60,10 +60,10 @@ import { Hydrating, ContentReset, DidCapture, - Update, Ref, Deletion, ForceUpdateForLegacySuspense, + StaticMask, } from './ReactFiberFlags'; import ReactSharedInternals from 'shared/ReactSharedInternals'; import { @@ -674,8 +674,6 @@ function updateProfiler( renderLanes: Lanes, ) { if (enableProfilerTimer) { - workInProgress.flags |= Update; - // Reset effect durations for the next eventual effect phase. // These are reset during render to allow the DevTools commit hook a chance to read them, const stateNode = workInProgress.stateNode; @@ -1151,6 +1149,9 @@ function updateHostComponent( workInProgress.flags |= ContentReset; } + // React DevTools reads this flag. + workInProgress.flags |= PerformedWork; + markRef(current, workInProgress); reconcileChildren(current, workInProgress, nextChildren, renderLanes); return workInProgress.child; @@ -2090,9 +2091,14 @@ function updateSuspensePrimaryChildren( primaryChildFragment.sibling = null; if (currentFallbackChildFragment !== null) { // Delete the fallback child fragment - currentFallbackChildFragment.nextEffect = null; - currentFallbackChildFragment.flags = Deletion; - workInProgress.firstEffect = workInProgress.lastEffect = currentFallbackChildFragment; + const deletions = workInProgress.deletions; + if (deletions === null) { + workInProgress.deletions = [currentFallbackChildFragment]; + // TODO (effects) Rename this to better reflect its new usage (e.g. ChildDeletions) + workInProgress.flags |= Deletion; + } else { + deletions.push(currentFallbackChildFragment); + } } workInProgress.child = primaryChildFragment; @@ -2149,24 +2155,19 @@ function updateSuspenseFallbackChildren( // The fallback fiber was added as a deletion effect during the first pass. // However, since we're going to remain on the fallback, we no longer want - // to delete it. So we need to remove it from the list. Deletions are stored - // on the same list as effects. We want to keep the effects from the primary - // tree. So we copy the primary child fragment's effect list, which does not - // include the fallback deletion effect. - const progressedLastEffect = primaryChildFragment.lastEffect; - if (progressedLastEffect !== null) { - workInProgress.firstEffect = primaryChildFragment.firstEffect; - workInProgress.lastEffect = progressedLastEffect; - progressedLastEffect.nextEffect = null; - } else { - // TODO: Reset this somewhere else? Lol legacy mode is so weird. - workInProgress.firstEffect = workInProgress.lastEffect = null; - } + // to delete it. + workInProgress.deletions = null; } else { primaryChildFragment = createWorkInProgressOffscreenFiber( currentPrimaryChildFragment, primaryChildProps, ); + + // Since we're reusing a current tree, we need to reuse the flags, too. + // (We don't do this in legacy mode, because in legacy mode we don't re-use + // the current tree; see previous branch.) + primaryChildFragment.subtreeFlags = + currentPrimaryChildFragment.subtreeFlags & StaticMask; } let fallbackChildFragment; if (currentFallbackChildFragment !== null) { @@ -2651,7 +2652,6 @@ function initSuspenseListRenderState( tail: null | Fiber, lastContentRow: null | Fiber, tailMode: SuspenseListTailMode, - lastEffectBeforeRendering: null | Fiber, ): void { const renderState: null | SuspenseListRenderState = workInProgress.memoizedState; @@ -2663,7 +2663,6 @@ function initSuspenseListRenderState( last: lastContentRow, tail: tail, tailMode: tailMode, - lastEffect: lastEffectBeforeRendering, }: SuspenseListRenderState); } else { // We can reuse the existing object from previous renders. @@ -2673,7 +2672,6 @@ function initSuspenseListRenderState( renderState.last = lastContentRow; renderState.tail = tail; renderState.tailMode = tailMode; - renderState.lastEffect = lastEffectBeforeRendering; } } @@ -2755,7 +2753,6 @@ function updateSuspenseListComponent( tail, lastContentRow, tailMode, - workInProgress.lastEffect, ); break; } @@ -2787,7 +2784,6 @@ function updateSuspenseListComponent( tail, null, // last tailMode, - workInProgress.lastEffect, ); break; } @@ -2798,7 +2794,6 @@ function updateSuspenseListComponent( null, // tail null, // last undefined, - workInProgress.lastEffect, ); break; } @@ -3058,15 +3053,14 @@ function remountFiber( // Delete the old fiber and place the new one. // Since the old fiber is disconnected, we have to schedule it manually. - const last = returnFiber.lastEffect; - if (last !== null) { - last.nextEffect = current; - returnFiber.lastEffect = current; + const deletions = returnFiber.deletions; + if (deletions === null) { + returnFiber.deletions = [current]; + // TODO (effects) Rename this to better reflect its new usage (e.g. ChildDeletions) + returnFiber.flags |= Deletion; } else { - returnFiber.firstEffect = returnFiber.lastEffect = current; + deletions.push(current); } - current.nextEffect = null; - current.flags = Deletion; newWorkInProgress.flags |= Placement; @@ -3151,15 +3145,6 @@ function beginWork( } case Profiler: if (enableProfilerTimer) { - // Profiler should only call onRender when one of its descendants actually rendered. - const hasChildWork = includesSomeLane( - renderLanes, - workInProgress.childLanes, - ); - if (hasChildWork) { - workInProgress.flags |= Update; - } - // Reset effect durations for the next eventual effect phase. // These are reset during render to allow the DevTools commit hook a chance to read them, const stateNode = workInProgress.stateNode; @@ -3266,7 +3251,6 @@ function beginWork( // update in the past but didn't complete it. renderState.rendering = null; renderState.tail = null; - renderState.lastEffect = null; } pushSuspenseContext(workInProgress, suspenseStackCursor.current); diff --git a/packages/react-reconciler/src/ReactFiberClassComponent.old.js b/packages/react-reconciler/src/ReactFiberClassComponent.old.js index c5f3527419490..1ed1f44d6389d 100644 --- a/packages/react-reconciler/src/ReactFiberClassComponent.old.js +++ b/packages/react-reconciler/src/ReactFiberClassComponent.old.js @@ -12,13 +12,14 @@ import type {Lanes} from './ReactFiberLane'; import type {UpdateQueue} from './ReactUpdateQueue.old'; import * as React from 'react'; -import {Update, Snapshot} from './ReactFiberFlags'; +import {Update, Snapshot, MountLayoutDev} from './ReactFiberFlags'; import { debugRenderPhaseSideEffectsForStrictMode, disableLegacyContext, enableDebugTracing, enableSchedulingProfiler, warnAboutDeprecatedLifecycles, + enableDoubleInvokingEffects, } from 'shared/ReactFeatureFlags'; import ReactStrictModeWarnings from './ReactStrictModeWarnings.old'; import {isMounted} from './ReactFiberTreeReflection'; @@ -29,7 +30,13 @@ import invariant from 'shared/invariant'; import {REACT_CONTEXT_TYPE, REACT_PROVIDER_TYPE} from 'shared/ReactSymbols'; import {resolveDefaultProps} from './ReactFiberLazyComponent.old'; -import {DebugTracingMode, StrictMode} from './ReactTypeOfMode'; +import { + BlockingMode, + ConcurrentMode, + DebugTracingMode, + NoMode, + StrictMode, +} from './ReactTypeOfMode'; import { enqueueUpdate, @@ -890,7 +897,16 @@ function mountClassInstance( } if (typeof instance.componentDidMount === 'function') { - workInProgress.flags |= Update; + if ( + __DEV__ && + enableDoubleInvokingEffects && + (workInProgress.mode & (BlockingMode | ConcurrentMode)) !== NoMode + ) { + // Never double-invoke effects for legacy roots. + workInProgress.flags |= MountLayoutDev | Update; + } else { + workInProgress.flags |= Update; + } } } @@ -960,7 +976,15 @@ function resumeMountClassInstance( // If an update was already in progress, we should schedule an Update // effect even though we're bailing out, so that cWU/cDU are called. if (typeof instance.componentDidMount === 'function') { - workInProgress.flags |= Update; + if ( + __DEV__ && + enableDoubleInvokingEffects && + (workInProgress.mode & (BlockingMode | ConcurrentMode)) !== NoMode + ) { + workInProgress.flags |= MountLayoutDev | Update; + } else { + workInProgress.flags |= Update; + } } return false; } @@ -1003,13 +1027,29 @@ function resumeMountClassInstance( } } if (typeof instance.componentDidMount === 'function') { - workInProgress.flags |= Update; + if ( + __DEV__ && + enableDoubleInvokingEffects && + (workInProgress.mode & (BlockingMode | ConcurrentMode)) !== NoMode + ) { + workInProgress.flags |= MountLayoutDev | Update; + } else { + workInProgress.flags |= Update; + } } } else { // If an update was already in progress, we should schedule an Update // effect even though we're bailing out, so that cWU/cDU are called. if (typeof instance.componentDidMount === 'function') { - workInProgress.flags |= Update; + if ( + __DEV__ && + enableDoubleInvokingEffects && + (workInProgress.mode & (BlockingMode | ConcurrentMode)) !== NoMode + ) { + workInProgress.flags |= MountLayoutDev | Update; + } else { + workInProgress.flags |= Update; + } } // If shouldComponentUpdate returned false, we should still update the diff --git a/packages/react-reconciler/src/ReactFiberCommitWork.old.js b/packages/react-reconciler/src/ReactFiberCommitWork.old.js index 84ff06fe7f998..c8a7c96dd900a 100644 --- a/packages/react-reconciler/src/ReactFiberCommitWork.old.js +++ b/packages/react-reconciler/src/ReactFiberCommitWork.old.js @@ -17,13 +17,14 @@ import type { } from './ReactFiberHostConfig'; import type {Fiber} from './ReactInternalTypes'; import type {FiberRoot} from './ReactInternalTypes'; -import type {Lanes} from './ReactFiberLane'; import type {SuspenseState} from './ReactFiberSuspenseComponent.old'; import type {UpdateQueue} from './ReactUpdateQueue.old'; import type {FunctionComponentUpdateQueue} from './ReactFiberHooks.old'; import type {Wakeable} from 'shared/ReactTypes'; import type {ReactPriorityLevel} from './ReactInternalTypes'; import type {OffscreenState} from './ReactFiberOffscreenComponent'; +import type {HookFlags} from './ReactHookEffectTags'; +import type {Flags} from './ReactFiberFlags'; import {unstable_wrap as Schedule_tracing_wrap} from 'scheduler/tracing'; import { @@ -34,6 +35,8 @@ import { enableFundamentalAPI, enableSuspenseCallback, enableScopeAPI, + enableDoubleInvokingEffects, + enableRecursiveCommitTraversal, } from 'shared/ReactFeatureFlags'; import { FunctionComponent, @@ -66,25 +69,52 @@ import { ContentReset, Placement, Snapshot, + Visibility, Update, + Callback, + Ref, + PlacementAndUpdate, + Hydrating, + HydratingAndUpdate, + Passive, + PassiveStatic, + BeforeMutationMask, + MutationMask, + LayoutMask, + PassiveMask, + MountLayoutDev, + MountPassiveDev, } from './ReactFiberFlags'; import getComponentName from 'shared/getComponentName'; import invariant from 'shared/invariant'; - +import { + current as currentDebugFiberInDEV, + resetCurrentFiber as resetCurrentDebugFiberInDEV, + setCurrentFiber as setCurrentDebugFiberInDEV, +} from './ReactCurrentFiber'; import {onCommitUnmount} from './ReactFiberDevToolsHook.old'; import {resolveDefaultProps} from './ReactFiberLazyComponent.old'; import { getCommitTime, recordLayoutEffectDuration, startLayoutEffectTimer, + recordPassiveEffectDuration, + startPassiveEffectTimer, } from './ReactProfilerTimer.old'; -import {ProfileMode} from './ReactTypeOfMode'; +import { + NoMode, + BlockingMode, + ConcurrentMode, + ProfileMode, +} from './ReactTypeOfMode'; import {commitUpdateQueue} from './ReactUpdateQueue.old'; import { getPublicInstance, supportsMutation, supportsPersistence, supportsHydration, + prepareForCommit, + beforeActiveInstanceBlur, commitMount, commitUpdate, resetTextContent, @@ -114,9 +144,6 @@ import { captureCommitPhaseError, resolveRetryWakeable, markCommitTimeOfFallback, - enqueuePendingPassiveHookEffectMount, - enqueuePendingPassiveHookEffectUnmount, - enqueuePendingPassiveProfilerEffect, } from './ReactFiberWorkLoop.old'; import { NoFlags as NoHookEffect, @@ -125,6 +152,12 @@ import { Passive as HookPassive, } from './ReactHookEffectTags'; import {didWarnAboutReassigningProps} from './ReactFiberBeginWork.old'; +import {doesFiberContain} from './ReactFiberTreeReflection'; + +let nextEffect: Fiber | null = null; + +// Used to avoid traversing the return path to find the nearest Profiler ancestor during commit. +let nearestProfilerOnStack: Fiber | null = null; let didWarnAboutUndefinedSnapshotBeforeUpdate: Set | null = null; if (__DEV__) { @@ -153,7 +186,11 @@ const callComponentWillUnmountWithTimer = function(current, instance) { }; // Capture errors so they don't interrupt unmounting. -function safelyCallComponentWillUnmount(current: Fiber, instance: any) { +function safelyCallComponentWillUnmount( + current: Fiber, + instance: any, + nearestMountedAncestor: Fiber | null, +) { if (__DEV__) { invokeGuardedCallback( null, @@ -164,18 +201,19 @@ function safelyCallComponentWillUnmount(current: Fiber, instance: any) { ); if (hasCaughtError()) { const unmountError = clearCaughtError(); - captureCommitPhaseError(current, unmountError); + captureCommitPhaseError(current, nearestMountedAncestor, unmountError); } } else { try { callComponentWillUnmountWithTimer(current, instance); } catch (unmountError) { - captureCommitPhaseError(current, unmountError); + captureCommitPhaseError(current, nearestMountedAncestor, unmountError); } } } -function safelyDetachRef(current: Fiber) { +/** @noinline */ +function safelyDetachRef(current: Fiber, nearestMountedAncestor: Fiber) { const ref = current.ref; if (ref !== null) { if (typeof ref === 'function') { @@ -194,7 +232,7 @@ function safelyDetachRef(current: Fiber) { if (hasCaughtError()) { const refError = clearCaughtError(); - captureCommitPhaseError(current, refError); + captureCommitPhaseError(current, nearestMountedAncestor, refError); } } else { try { @@ -213,7 +251,7 @@ function safelyDetachRef(current: Fiber) { ref(null); } } catch (refError) { - captureCommitPhaseError(current, refError); + captureCommitPhaseError(current, nearestMountedAncestor, refError); } } } else { @@ -222,127 +260,44 @@ function safelyDetachRef(current: Fiber) { } } -function safelyCallDestroy(current: Fiber, destroy: () => void) { +export function safelyCallDestroy( + current: Fiber, + nearestMountedAncestor: Fiber | null, + destroy: () => void, +) { if (__DEV__) { invokeGuardedCallback(null, destroy, null); if (hasCaughtError()) { const error = clearCaughtError(); - captureCommitPhaseError(current, error); + captureCommitPhaseError(current, nearestMountedAncestor, error); } } else { try { destroy(); } catch (error) { - captureCommitPhaseError(current, error); + captureCommitPhaseError(current, nearestMountedAncestor, error); } } } -function commitBeforeMutationLifeCycles( - current: Fiber | null, +/** @noinline */ +function commitHookEffectListUnmount( + flags: HookFlags, finishedWork: Fiber, -): void { - switch (finishedWork.tag) { - case FunctionComponent: - case ForwardRef: - case SimpleMemoComponent: - case Block: { - return; - } - case ClassComponent: { - if (finishedWork.flags & Snapshot) { - if (current !== null) { - const prevProps = current.memoizedProps; - const prevState = current.memoizedState; - const instance = finishedWork.stateNode; - // We could update instance props and state here, - // but instead we rely on them being set during last render. - // TODO: revisit this when we implement resuming. - if (__DEV__) { - if ( - finishedWork.type === finishedWork.elementType && - !didWarnAboutReassigningProps - ) { - if (instance.props !== finishedWork.memoizedProps) { - console.error( - 'Expected %s props to match memoized props before ' + - 'getSnapshotBeforeUpdate. ' + - 'This might either be because of a bug in React, or because ' + - 'a component reassigns its own `this.props`. ' + - 'Please file an issue.', - getComponentName(finishedWork.type) || 'instance', - ); - } - if (instance.state !== finishedWork.memoizedState) { - console.error( - 'Expected %s state to match memoized state before ' + - 'getSnapshotBeforeUpdate. ' + - 'This might either be because of a bug in React, or because ' + - 'a component reassigns its own `this.state`. ' + - 'Please file an issue.', - getComponentName(finishedWork.type) || 'instance', - ); - } - } - } - const snapshot = instance.getSnapshotBeforeUpdate( - finishedWork.elementType === finishedWork.type - ? prevProps - : resolveDefaultProps(finishedWork.type, prevProps), - prevState, - ); - if (__DEV__) { - const didWarnSet = ((didWarnAboutUndefinedSnapshotBeforeUpdate: any): Set); - if (snapshot === undefined && !didWarnSet.has(finishedWork.type)) { - didWarnSet.add(finishedWork.type); - console.error( - '%s.getSnapshotBeforeUpdate(): A snapshot value (or null) ' + - 'must be returned. You have returned undefined.', - getComponentName(finishedWork.type), - ); - } - } - instance.__reactInternalSnapshotBeforeUpdate = snapshot; - } - } - return; - } - case HostRoot: { - if (supportsMutation) { - if (finishedWork.flags & Snapshot) { - const root = finishedWork.stateNode; - clearContainer(root.containerInfo); - } - } - return; - } - case HostComponent: - case HostText: - case HostPortal: - case IncompleteClassComponent: - // Nothing to do for these component types - return; - } - invariant( - false, - 'This unit of work tag should not have side-effects. This error is ' + - 'likely caused by a bug in React. Please file an issue.', - ); -} - -function commitHookEffectListUnmount(tag: number, finishedWork: Fiber) { + 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 & tag) === tag) { + if ((effect.tag & flags) === flags) { // Unmount const destroy = effect.destroy; effect.destroy = undefined; if (destroy !== undefined) { - destroy(); + safelyCallDestroy(finishedWork, nearestMountedAncestor, destroy); } } effect = effect.next; @@ -350,14 +305,15 @@ function commitHookEffectListUnmount(tag: number, finishedWork: Fiber) { } } -function commitHookEffectListMount(tag: number, finishedWork: Fiber) { +/** @noinline */ +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 & tag) === tag) { + if ((effect.tag & flags) === flags) { // Mount const create = effect.create; effect.destroy = create(); @@ -400,442 +356,1692 @@ function commitHookEffectListMount(tag: number, finishedWork: Fiber) { } } -function schedulePassiveEffects(finishedWork: Fiber) { - const updateQueue: FunctionComponentUpdateQueue | null = (finishedWork.updateQueue: any); - const lastEffect = updateQueue !== null ? updateQueue.lastEffect : null; - if (lastEffect !== null) { - const firstEffect = lastEffect.next; - let effect = firstEffect; - do { - const {next, tag} = effect; - if ( - (tag & HookPassive) !== NoHookEffect && - (tag & HookHasEffect) !== NoHookEffect - ) { - enqueuePendingPassiveHookEffectUnmount(finishedWork, effect); - enqueuePendingPassiveHookEffectMount(finishedWork, effect); - } - effect = next; - } while (effect !== firstEffect); - } -} - -export function commitPassiveEffectDurations( +function commitProfilerPassiveEffect( finishedRoot: FiberRoot, finishedWork: Fiber, ): void { if (enableProfilerTimer && enableProfilerCommitHooks) { - // Only Profilers with work in their subtree will have an Update effect scheduled. - if ((finishedWork.flags & Update) !== NoFlags) { - switch (finishedWork.tag) { - case Profiler: { - const {passiveEffectDuration} = finishedWork.stateNode; - const {id, onPostCommit} = finishedWork.memoizedProps; - - // This value will still reflect the previous commit phase. - // It does not get reset until the start of the next commit phase. - const commitTime = getCommitTime(); - - if (typeof onPostCommit === 'function') { - if (enableSchedulerTracing) { - onPostCommit( - id, - finishedWork.alternate === null ? 'mount' : 'update', - passiveEffectDuration, - commitTime, - finishedRoot.memoizedInteractions, - ); - } else { - onPostCommit( - id, - finishedWork.alternate === null ? 'mount' : 'update', - passiveEffectDuration, - commitTime, - ); - } - } + switch (finishedWork.tag) { + case Profiler: { + const {passiveEffectDuration} = finishedWork.stateNode; + const {id, onPostCommit} = finishedWork.memoizedProps; - // Bubble times to the next nearest ancestor Profiler. - // After we process that Profiler, we'll bubble further up. - let parentFiber = finishedWork.return; - while (parentFiber !== null) { - if (parentFiber.tag === Profiler) { - const parentStateNode = parentFiber.stateNode; - parentStateNode.passiveEffectDuration += passiveEffectDuration; - break; - } - parentFiber = parentFiber.return; + // This value will still reflect the previous commit phase. + // It does not get reset until the start of the next commit phase. + const commitTime = getCommitTime(); + + if (typeof onPostCommit === 'function') { + if (enableSchedulerTracing) { + onPostCommit( + id, + finishedWork.alternate === null ? 'mount' : 'update', + passiveEffectDuration, + commitTime, + finishedRoot.memoizedInteractions, + ); + } else { + onPostCommit( + id, + finishedWork.alternate === null ? 'mount' : 'update', + passiveEffectDuration, + commitTime, + ); } - break; } - default: - break; + break; } + default: + break; } } } -function commitLifeCycles( - finishedRoot: FiberRoot, - current: Fiber | null, - finishedWork: Fiber, - committedLanes: Lanes, -): void { - switch (finishedWork.tag) { - case FunctionComponent: - case ForwardRef: - case SimpleMemoComponent: - case Block: { - // At this point layout effects have already been destroyed (during mutation phase). - // This is done to prevent sibling component effects from interfering with each other, - // e.g. a destroy function in one component should never override a ref set - // by a create function in another component during the same commit. - if ( - enableProfilerTimer && - enableProfilerCommitHooks && - finishedWork.mode & ProfileMode - ) { - try { - startLayoutEffectTimer(); - commitHookEffectListMount(HookLayout | HookHasEffect, finishedWork); - } finally { - recordLayoutEffectDuration(finishedWork); - } - } else { - commitHookEffectListMount(HookLayout | HookHasEffect, finishedWork); +let focusedInstanceHandle: null | Fiber = null; +let shouldFireAfterActiveInstanceBlur: boolean = false; + +export function commitBeforeMutationEffects( + root: FiberRoot, + firstChild: Fiber, +) { + focusedInstanceHandle = prepareForCommit(root.containerInfo); + + if (enableRecursiveCommitTraversal) { + recursivelyCommitBeforeMutationEffects(firstChild); + } else { + nextEffect = firstChild; + iterativelyCommitBeforeMutationEffects_begin(); + } + + // We no longer need to track the active instance fiber + const shouldFire = shouldFireAfterActiveInstanceBlur; + shouldFireAfterActiveInstanceBlur = false; + focusedInstanceHandle = null; + + return shouldFire; +} + +function recursivelyCommitBeforeMutationEffects(firstChild: Fiber) { + let fiber = firstChild; + while (fiber !== null) { + // TODO: Should wrap this in flags check, too, as optimization + if (fiber.deletions !== null) { + commitBeforeMutationEffectsDeletions(fiber.deletions); + } + + const child = fiber.child; + if (fiber.subtreeFlags & BeforeMutationMask && child !== null) { + recursivelyCommitBeforeMutationEffects(child); + } + + if (__DEV__) { + setCurrentDebugFiberInDEV(fiber); + invokeGuardedCallback( + null, + commitBeforeMutationEffectsOnFiber, + null, + fiber, + ); + if (hasCaughtError()) { + const error = clearCaughtError(); + captureCommitPhaseError(fiber, fiber.return, error); + } + resetCurrentDebugFiberInDEV(); + } else { + try { + commitBeforeMutationEffectsOnFiber(fiber); + } catch (error) { + captureCommitPhaseError(fiber, fiber.return, error); + } + } + fiber = fiber.sibling; + } +} + +function iterativelyCommitBeforeMutationEffects_begin() { + while (nextEffect !== null) { + const fiber = nextEffect; + + // TODO: Should wrap this in flags check, too, as optimization + const deletions = fiber.deletions; + if (deletions !== null) { + commitBeforeMutationEffectsDeletions(deletions); + } + + const child = fiber.child; + if ( + (fiber.subtreeFlags & BeforeMutationMask) !== NoFlags && + child !== null + ) { + child.return = fiber; + nextEffect = child; + } else { + iterativelyCommitBeforeMutationEffects_complete(); + } + } +} + +function iterativelyCommitBeforeMutationEffects_complete() { + while (nextEffect !== null) { + const fiber = nextEffect; + if (__DEV__) { + setCurrentDebugFiberInDEV(fiber); + invokeGuardedCallback( + null, + commitBeforeMutationEffectsOnFiber, + null, + fiber, + ); + if (hasCaughtError()) { + const error = clearCaughtError(); + captureCommitPhaseError(fiber, fiber.return, error); + } + resetCurrentDebugFiberInDEV(); + } else { + try { + commitBeforeMutationEffectsOnFiber(fiber); + } catch (error) { + captureCommitPhaseError(fiber, fiber.return, error); } + } - schedulePassiveEffects(finishedWork); + const sibling = fiber.sibling; + if (sibling !== null) { + sibling.return = fiber.return; + nextEffect = sibling; return; } - case ClassComponent: { - const instance = finishedWork.stateNode; - if (finishedWork.flags & Update) { - if (current === null) { - // We could update instance props and state here, - // but instead we rely on them being set during last render. - // TODO: revisit this when we implement resuming. - if (__DEV__) { - if ( - finishedWork.type === finishedWork.elementType && - !didWarnAboutReassigningProps - ) { - if (instance.props !== finishedWork.memoizedProps) { - console.error( - 'Expected %s props to match memoized props before ' + - 'componentDidMount. ' + - 'This might either be because of a bug in React, or because ' + - 'a component reassigns its own `this.props`. ' + - 'Please file an issue.', - getComponentName(finishedWork.type) || 'instance', - ); - } - if (instance.state !== finishedWork.memoizedState) { - console.error( - 'Expected %s state to match memoized state before ' + - 'componentDidMount. ' + - 'This might either be because of a bug in React, or because ' + - 'a component reassigns its own `this.state`. ' + - 'Please file an issue.', - getComponentName(finishedWork.type) || 'instance', - ); + + nextEffect = fiber.return; + } +} + +/** @noinline */ +function commitBeforeMutationEffectsOnFiber(finishedWork: Fiber) { + const current = finishedWork.alternate; + const flags = finishedWork.flags; + + if (!shouldFireAfterActiveInstanceBlur && focusedInstanceHandle !== null) { + // Check to see if the focused element was inside of a hidden (Suspense) subtree. + if ( + // TODO: Can optimize this further with separate Hide and Show flags. We + // only care about Hide here. + (flags & Visibility) !== NoFlags && + finishedWork.tag === SuspenseComponent && + isSuspenseBoundaryBeingHidden(current, finishedWork) && + doesFiberContain(finishedWork, focusedInstanceHandle) + ) { + shouldFireAfterActiveInstanceBlur = true; + beforeActiveInstanceBlur(finishedWork); + } + } + + if ((flags & Snapshot) !== NoFlags) { + setCurrentDebugFiberInDEV(finishedWork); + switch (finishedWork.tag) { + case FunctionComponent: + case ForwardRef: + case SimpleMemoComponent: + case Block: { + break; + } + case ClassComponent: { + if (finishedWork.flags & Snapshot) { + if (current !== null) { + const prevProps = current.memoizedProps; + const prevState = current.memoizedState; + const instance = finishedWork.stateNode; + // We could update instance props and state here, + // but instead we rely on them being set during last render. + // TODO: revisit this when we implement resuming. + if (__DEV__) { + if ( + finishedWork.type === finishedWork.elementType && + !didWarnAboutReassigningProps + ) { + if (instance.props !== finishedWork.memoizedProps) { + console.error( + 'Expected %s props to match memoized props before ' + + 'getSnapshotBeforeUpdate. ' + + 'This might either be because of a bug in React, or because ' + + 'a component reassigns its own `this.props`. ' + + 'Please file an issue.', + getComponentName(finishedWork.type) || 'instance', + ); + } + if (instance.state !== finishedWork.memoizedState) { + console.error( + 'Expected %s state to match memoized state before ' + + 'getSnapshotBeforeUpdate. ' + + 'This might either be because of a bug in React, or because ' + + 'a component reassigns its own `this.state`. ' + + 'Please file an issue.', + getComponentName(finishedWork.type) || 'instance', + ); + } } } - } - if ( - enableProfilerTimer && - enableProfilerCommitHooks && - finishedWork.mode & ProfileMode - ) { - try { - startLayoutEffectTimer(); - instance.componentDidMount(); - } finally { - recordLayoutEffectDuration(finishedWork); - } - } else { - instance.componentDidMount(); - } - } else { - const prevProps = - finishedWork.elementType === finishedWork.type - ? current.memoizedProps - : resolveDefaultProps(finishedWork.type, current.memoizedProps); - const prevState = current.memoizedState; - // We could update instance props and state here, - // but instead we rely on them being set during last render. - // TODO: revisit this when we implement resuming. - if (__DEV__) { - if ( - finishedWork.type === finishedWork.elementType && - !didWarnAboutReassigningProps - ) { - if (instance.props !== finishedWork.memoizedProps) { - console.error( - 'Expected %s props to match memoized props before ' + - 'componentDidUpdate. ' + - 'This might either be because of a bug in React, or because ' + - 'a component reassigns its own `this.props`. ' + - 'Please file an issue.', - getComponentName(finishedWork.type) || 'instance', - ); - } - if (instance.state !== finishedWork.memoizedState) { + const snapshot = instance.getSnapshotBeforeUpdate( + finishedWork.elementType === finishedWork.type + ? prevProps + : resolveDefaultProps(finishedWork.type, prevProps), + prevState, + ); + if (__DEV__) { + const didWarnSet = ((didWarnAboutUndefinedSnapshotBeforeUpdate: any): Set); + if ( + snapshot === undefined && + !didWarnSet.has(finishedWork.type) + ) { + didWarnSet.add(finishedWork.type); console.error( - 'Expected %s state to match memoized state before ' + - 'componentDidUpdate. ' + - 'This might either be because of a bug in React, or because ' + - 'a component reassigns its own `this.state`. ' + - 'Please file an issue.', - getComponentName(finishedWork.type) || 'instance', + '%s.getSnapshotBeforeUpdate(): A snapshot value (or null) ' + + 'must be returned. You have returned undefined.', + getComponentName(finishedWork.type), ); } } - } - if ( - enableProfilerTimer && - enableProfilerCommitHooks && - finishedWork.mode & ProfileMode - ) { - try { - startLayoutEffectTimer(); - instance.componentDidUpdate( - prevProps, - prevState, - instance.__reactInternalSnapshotBeforeUpdate, - ); - } finally { - recordLayoutEffectDuration(finishedWork); - } - } else { - instance.componentDidUpdate( - prevProps, - prevState, - instance.__reactInternalSnapshotBeforeUpdate, - ); - } - } - } - - // TODO: I think this is now always non-null by the time it reaches the - // commit phase. Consider removing the type check. - const updateQueue: UpdateQueue< - *, - > | null = (finishedWork.updateQueue: any); - if (updateQueue !== null) { - if (__DEV__) { - if ( - finishedWork.type === finishedWork.elementType && - !didWarnAboutReassigningProps - ) { - if (instance.props !== finishedWork.memoizedProps) { - console.error( - 'Expected %s props to match memoized props before ' + - 'processing the update queue. ' + - 'This might either be because of a bug in React, or because ' + - 'a component reassigns its own `this.props`. ' + - 'Please file an issue.', - getComponentName(finishedWork.type) || 'instance', - ); - } - if (instance.state !== finishedWork.memoizedState) { - console.error( - 'Expected %s state to match memoized state before ' + - 'processing the update queue. ' + - 'This might either be because of a bug in React, or because ' + - 'a component reassigns its own `this.state`. ' + - 'Please file an issue.', - getComponentName(finishedWork.type) || 'instance', - ); - } + instance.__reactInternalSnapshotBeforeUpdate = snapshot; } } - // We could update instance props and state here, - // but instead we rely on them being set during last render. - // TODO: revisit this when we implement resuming. - commitUpdateQueue(finishedWork, updateQueue, instance); + break; } - return; - } - case HostRoot: { - // TODO: I think this is now always non-null by the time it reaches the - // commit phase. Consider removing the type check. - const updateQueue: UpdateQueue< - *, - > | null = (finishedWork.updateQueue: any); - if (updateQueue !== null) { - let instance = null; - if (finishedWork.child !== null) { - switch (finishedWork.child.tag) { - case HostComponent: - instance = getPublicInstance(finishedWork.child.stateNode); - break; - case ClassComponent: - instance = finishedWork.child.stateNode; - break; + case HostRoot: { + if (supportsMutation) { + if (finishedWork.flags & Snapshot) { + const root = finishedWork.stateNode; + clearContainer(root.containerInfo); } } - commitUpdateQueue(finishedWork, updateQueue, instance); + break; } - return; + case HostComponent: + case HostText: + case HostPortal: + case IncompleteClassComponent: + // Nothing to do for these component types + break; + default: + invariant( + false, + 'This unit of work tag should not have side-effects. This error is ' + + 'likely caused by a bug in React. Please file an issue.', + ); } - case HostComponent: { - const instance: Instance = finishedWork.stateNode; + resetCurrentDebugFiberInDEV(); + } +} - // Renderers may schedule work to be done after host components are mounted - // (eg DOM renderer may schedule auto-focus for inputs and form controls). - // These effects should only be committed when components are first mounted, - // aka when there is no current/alternate. - if (current === null && finishedWork.flags & Update) { - const type = finishedWork.type; - const props = finishedWork.memoizedProps; - commitMount(instance, type, props, finishedWork); - } +/** @noinline */ +function commitBeforeMutationEffectsDeletions(deletions: Array) { + for (let i = 0; i < deletions.length; i++) { + const fiber = deletions[i]; - return; - } - case HostText: { - // We have no life-cycles associated with text. - return; - } - case HostPortal: { - // We have no life-cycles associated with portals. - return; - } - case Profiler: { - if (enableProfilerTimer) { - const {onCommit, onRender} = finishedWork.memoizedProps; - const {effectDuration} = finishedWork.stateNode; + // 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. - const commitTime = getCommitTime(); + if (doesFiberContain(fiber, ((focusedInstanceHandle: any): Fiber))) { + shouldFireAfterActiveInstanceBlur = true; + beforeActiveInstanceBlur(fiber); + } + } +} - if (typeof onRender === 'function') { - if (enableSchedulerTracing) { - onRender( - finishedWork.memoizedProps.id, - current === null ? 'mount' : 'update', - finishedWork.actualDuration, - finishedWork.treeBaseDuration, - finishedWork.actualStartTime, - commitTime, - finishedRoot.memoizedInteractions, +export function commitMutationEffects( + firstChild: Fiber, + root: FiberRoot, + renderPriorityLevel: ReactPriorityLevel, +) { + if (enableRecursiveCommitTraversal) { + recursivelyCommitMutationEffects(firstChild, root, renderPriorityLevel); + } else { + nextEffect = firstChild; + iterativelyCommitMutationEffects_begin(root, renderPriorityLevel); + } +} + +function recursivelyCommitMutationEffects( + firstChild: Fiber, + root: FiberRoot, + renderPriorityLevel: ReactPriorityLevel, +) { + let fiber = firstChild; + while (fiber !== null) { + const deletions = fiber.deletions; + if (deletions !== null) { + commitMutationEffectsDeletions( + deletions, + fiber, + root, + renderPriorityLevel, + ); + } + + if (fiber.child !== null) { + const mutationFlags = fiber.subtreeFlags & MutationMask; + if (mutationFlags !== NoFlags) { + recursivelyCommitMutationEffects( + fiber.child, + root, + renderPriorityLevel, + ); + } + } + + if (__DEV__) { + setCurrentDebugFiberInDEV(fiber); + invokeGuardedCallback( + null, + commitMutationEffectsOnFiber, + null, + fiber, + root, + renderPriorityLevel, + ); + if (hasCaughtError()) { + const error = clearCaughtError(); + captureCommitPhaseError(fiber, fiber.return, error); + } + resetCurrentDebugFiberInDEV(); + } else { + try { + commitMutationEffectsOnFiber(fiber, root, renderPriorityLevel); + } catch (error) { + captureCommitPhaseError(fiber, fiber.return, error); + } + } + fiber = fiber.sibling; + } +} + +function iterativelyCommitMutationEffects_begin( + root: FiberRoot, + renderPriorityLevel: ReactPriorityLevel, +) { + while (nextEffect !== null) { + const fiber = nextEffect; + + // TODO: Should wrap this in flags check, too, as optimization + const deletions = fiber.deletions; + if (deletions !== null) { + commitMutationEffectsDeletions( + deletions, + fiber, + root, + renderPriorityLevel, + ); + } + + const child = fiber.child; + if ((fiber.subtreeFlags & MutationMask) !== NoFlags && child !== null) { + child.return = fiber; + nextEffect = child; + } else { + iterativelyCommitMutationEffects_complete(root, renderPriorityLevel); + } + } +} + +function iterativelyCommitMutationEffects_complete( + root: FiberRoot, + renderPriorityLevel: ReactPriorityLevel, +) { + while (nextEffect !== null) { + const fiber = nextEffect; + if (__DEV__) { + setCurrentDebugFiberInDEV(fiber); + invokeGuardedCallback( + null, + commitMutationEffectsOnFiber, + null, + fiber, + root, + renderPriorityLevel, + ); + if (hasCaughtError()) { + const error = clearCaughtError(); + captureCommitPhaseError(fiber, fiber.return, error); + } + resetCurrentDebugFiberInDEV(); + } else { + try { + commitMutationEffectsOnFiber(fiber, root, renderPriorityLevel); + } catch (error) { + captureCommitPhaseError(fiber, fiber.return, error); + } + } + + const sibling = fiber.sibling; + if (sibling !== null) { + sibling.return = fiber.return; + nextEffect = sibling; + return; + } + + nextEffect = fiber.return; + } +} + +/** @noinline */ +function commitMutationEffectsOnFiber( + fiber: Fiber, + root: FiberRoot, + renderPriorityLevel, +) { + const flags = fiber.flags; + if (flags & ContentReset) { + commitResetTextContent(fiber); + } + + if (flags & Ref) { + const current = fiber.alternate; + if (current !== null) { + commitDetachRef(current); + } + if (enableScopeAPI) { + // TODO: This is a temporary solution that allowed us to transition away from React Flare on www. + if (fiber.tag === ScopeComponent) { + commitAttachRef(fiber); + } + } + } + + // The following switch statement is only concerned about placement, + // updates, and deletions. To avoid needing to add a case for every possible + // bitmap value, we remove the secondary effects from the effect tag and + // switch on that value. + const primaryFlags = flags & (Placement | Update | Hydrating); + switch (primaryFlags) { + case Placement: { + commitPlacement(fiber); + // 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. + fiber.flags &= ~Placement; + break; + } + case PlacementAndUpdate: { + // Placement + commitPlacement(fiber); + // Clear the "placement" from effect tag so that we know that this is + // inserted, before any life-cycles like componentDidMount gets called. + fiber.flags &= ~Placement; + + // Update + const current = fiber.alternate; + commitWork(current, fiber); + break; + } + case Hydrating: { + fiber.flags &= ~Hydrating; + break; + } + case HydratingAndUpdate: { + fiber.flags &= ~Hydrating; + + // Update + const current = fiber.alternate; + commitWork(current, fiber); + break; + } + case Update: { + const current = fiber.alternate; + commitWork(current, fiber); + break; + } + } +} + +/** @noinline */ +function commitMutationEffectsDeletions( + deletions: Array, + nearestMountedAncestor: Fiber, + root: FiberRoot, + renderPriorityLevel, +) { + for (let i = 0; i < deletions.length; i++) { + const childToDelete = deletions[i]; + if (__DEV__) { + invokeGuardedCallback( + null, + commitDeletion, + null, + root, + childToDelete, + nearestMountedAncestor, + renderPriorityLevel, + ); + if (hasCaughtError()) { + const error = clearCaughtError(); + captureCommitPhaseError(childToDelete, nearestMountedAncestor, error); + } + } else { + try { + commitDeletion( + root, + childToDelete, + nearestMountedAncestor, + renderPriorityLevel, + ); + } catch (error) { + captureCommitPhaseError(childToDelete, nearestMountedAncestor, error); + } + } + } +} + +export function commitLayoutEffects( + finishedWork: Fiber, + finishedRoot: FiberRoot, +) { + if (enableRecursiveCommitTraversal) { + if (__DEV__) { + setCurrentDebugFiberInDEV(finishedWork); + invokeGuardedCallback( + null, + recursivelyCommitLayoutEffects, + null, + finishedWork, + finishedRoot, + ); + if (hasCaughtError()) { + const error = clearCaughtError(); + captureCommitPhaseError(finishedWork, null, error); + } + resetCurrentDebugFiberInDEV(); + } else { + try { + recursivelyCommitLayoutEffects(finishedWork, finishedRoot); + } catch (error) { + captureCommitPhaseError(finishedWork, null, error); + } + } + } else { + nextEffect = finishedWork; + iterativelyCommitLayoutEffects_begin(finishedWork, finishedRoot); + } +} + +function recursivelyCommitLayoutEffects( + finishedWork: Fiber, + finishedRoot: FiberRoot, +) { + const {flags, tag} = finishedWork; + switch (tag) { + case Profiler: { + let prevProfilerOnStack = null; + if (enableProfilerTimer && enableProfilerCommitHooks) { + prevProfilerOnStack = nearestProfilerOnStack; + nearestProfilerOnStack = finishedWork; + } + + let child = finishedWork.child; + while (child !== null) { + const primarySubtreeFlags = finishedWork.subtreeFlags & LayoutMask; + if (primarySubtreeFlags !== NoFlags) { + if (__DEV__) { + const prevCurrentFiberInDEV = currentDebugFiberInDEV; + setCurrentDebugFiberInDEV(child); + invokeGuardedCallback( + null, + recursivelyCommitLayoutEffects, + null, + child, + finishedRoot, ); + if (hasCaughtError()) { + const error = clearCaughtError(); + captureCommitPhaseError(child, finishedWork, error); + } + if (prevCurrentFiberInDEV !== null) { + setCurrentDebugFiberInDEV(prevCurrentFiberInDEV); + } else { + resetCurrentDebugFiberInDEV(); + } } else { - onRender( - finishedWork.memoizedProps.id, - current === null ? 'mount' : 'update', - finishedWork.actualDuration, - finishedWork.treeBaseDuration, - finishedWork.actualStartTime, - commitTime, + try { + recursivelyCommitLayoutEffects(child, finishedRoot); + } catch (error) { + captureCommitPhaseError(child, finishedWork, error); + } + } + } + child = child.sibling; + } + + const primaryFlags = flags & (Update | Callback); + if (primaryFlags !== NoFlags) { + if (enableProfilerTimer) { + if (__DEV__) { + const prevCurrentFiberInDEV = currentDebugFiberInDEV; + setCurrentDebugFiberInDEV(finishedWork); + invokeGuardedCallback( + null, + commitLayoutEffectsForProfiler, + null, + finishedWork, + finishedRoot, + ); + if (hasCaughtError()) { + const error = clearCaughtError(); + captureCommitPhaseError(finishedWork, finishedWork.return, error); + } + if (prevCurrentFiberInDEV !== null) { + setCurrentDebugFiberInDEV(prevCurrentFiberInDEV); + } else { + resetCurrentDebugFiberInDEV(); + } + } else { + try { + commitLayoutEffectsForProfiler(finishedWork, finishedRoot); + } catch (error) { + captureCommitPhaseError(finishedWork, finishedWork.return, error); + } + } + } + } + + if (enableProfilerTimer && enableProfilerCommitHooks) { + // 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. + if (prevProfilerOnStack !== null) { + prevProfilerOnStack.stateNode.effectDuration += + finishedWork.stateNode.effectDuration; + } + + nearestProfilerOnStack = prevProfilerOnStack; + } + break; + } + + // case Offscreen: { + // TODO: Fast path to invoke all nested layout effects when Offscren goes from hidden to visible. + // break; + // } + + default: { + let child = finishedWork.child; + while (child !== null) { + const primarySubtreeFlags = finishedWork.subtreeFlags & LayoutMask; + if (primarySubtreeFlags !== NoFlags) { + if (__DEV__) { + const prevCurrentFiberInDEV = currentDebugFiberInDEV; + setCurrentDebugFiberInDEV(child); + invokeGuardedCallback( + null, + recursivelyCommitLayoutEffects, + null, + child, + finishedRoot, + ); + if (hasCaughtError()) { + const error = clearCaughtError(); + captureCommitPhaseError(child, finishedWork, error); + } + if (prevCurrentFiberInDEV !== null) { + setCurrentDebugFiberInDEV(prevCurrentFiberInDEV); + } else { + resetCurrentDebugFiberInDEV(); + } + } else { + try { + recursivelyCommitLayoutEffects(child, finishedRoot); + } catch (error) { + captureCommitPhaseError(child, finishedWork, error); + } + } + } + child = child.sibling; + } + + const primaryFlags = flags & (Update | Callback); + if (primaryFlags !== NoFlags) { + switch (tag) { + case FunctionComponent: + case ForwardRef: + case SimpleMemoComponent: + case Block: { + if ( + enableProfilerTimer && + enableProfilerCommitHooks && + finishedWork.mode & ProfileMode + ) { + try { + startLayoutEffectTimer(); + commitHookEffectListMount( + HookLayout | HookHasEffect, + finishedWork, + ); + } finally { + recordLayoutEffectDuration(finishedWork); + } + } else { + commitHookEffectListMount( + HookLayout | HookHasEffect, + finishedWork, + ); + } + break; + } + case ClassComponent: { + // NOTE: Layout effect durations are measured within this function. + commitLayoutEffectsForClassComponent(finishedWork); + break; + } + case HostRoot: { + commitLayoutEffectsForHostRoot(finishedWork); + break; + } + case HostComponent: { + commitLayoutEffectsForHostComponent(finishedWork); + break; + } + case SuspenseComponent: { + commitSuspenseHydrationCallbacks(finishedRoot, finishedWork); + break; + } + case FundamentalComponent: + case HostPortal: + case HostText: + case IncompleteClassComponent: + case LegacyHiddenComponent: + case OffscreenComponent: + case ScopeComponent: + case SuspenseListComponent: { + // We have no life-cycles associated with these component types. + break; + } + default: { + invariant( + false, + 'This unit of work tag should not have side-effects. This error is ' + + 'likely caused by a bug in React. Please file an issue.', + ); + } + } + } + + if (enableScopeAPI) { + // TODO: This is a temporary solution that allowed us to transition away from React Flare on www. + if (flags & Ref && tag !== ScopeComponent) { + commitAttachRef(finishedWork); + } + } else { + if (flags & Ref) { + commitAttachRef(finishedWork); + } + } + break; + } + } +} + +function iterativelyCommitLayoutEffects_begin( + subtreeRoot: Fiber, + finishedRoot: FiberRoot, +) { + while (nextEffect !== null) { + const finishedWork: Fiber = nextEffect; + const firstChild = finishedWork.child; + + if ( + (finishedWork.subtreeFlags & LayoutMask) !== NoFlags && + firstChild !== null + ) { + if ( + enableProfilerTimer && + enableProfilerCommitHooks && + finishedWork.tag === Profiler + ) { + const prevProfilerOnStack = nearestProfilerOnStack; + nearestProfilerOnStack = finishedWork; + + let child = firstChild; + while (child !== null) { + nextEffect = child; + iterativelyCommitLayoutEffects_begin(child, finishedRoot); + child = child.sibling; + } + nextEffect = finishedWork; + + if ((finishedWork.flags & LayoutMask) !== NoFlags) { + if (__DEV__) { + setCurrentDebugFiberInDEV(finishedWork); + invokeGuardedCallback( + null, + commitLayoutEffectsForProfiler, + null, + finishedWork, + finishedRoot, ); + if (hasCaughtError()) { + const error = clearCaughtError(); + captureCommitPhaseError(finishedWork, finishedWork.return, error); + } + resetCurrentDebugFiberInDEV(); + } else { + try { + commitLayoutEffectsForProfiler(finishedWork, finishedRoot); + } catch (error) { + captureCommitPhaseError(finishedWork, finishedWork.return, error); + } + } + } + + // 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. + if (prevProfilerOnStack !== null) { + prevProfilerOnStack.stateNode.effectDuration += + finishedWork.stateNode.effectDuration; + } + nearestProfilerOnStack = prevProfilerOnStack; + + if (finishedWork === subtreeRoot) { + nextEffect = null; + return; + } + const sibling = finishedWork.sibling; + if (sibling !== null) { + sibling.return = finishedWork.return; + nextEffect = sibling; + } else { + nextEffect = finishedWork.return; + iterativelyCommitLayoutEffects_complete(subtreeRoot, finishedRoot); + } + } else { + firstChild.return = finishedWork; + nextEffect = firstChild; + } + } else { + iterativelyCommitLayoutEffects_complete(subtreeRoot, finishedRoot); + } + } +} + +function iterativelyCommitLayoutEffects_complete( + subtreeRoot: Fiber, + finishedRoot: FiberRoot, +) { + while (nextEffect !== null) { + const fiber = nextEffect; + + if ((fiber.flags & LayoutMask) !== NoFlags) { + if (__DEV__) { + setCurrentDebugFiberInDEV(fiber); + invokeGuardedCallback( + null, + commitLayoutEffectsOnFiber, + null, + finishedRoot, + fiber, + ); + if (hasCaughtError()) { + const error = clearCaughtError(); + captureCommitPhaseError(fiber, fiber.return, error); + } + resetCurrentDebugFiberInDEV(); + } else { + try { + commitLayoutEffectsOnFiber(finishedRoot, fiber); + } catch (error) { + captureCommitPhaseError(fiber, fiber.return, error); + } + } + } + + if (fiber === subtreeRoot) { + nextEffect = null; + return; + } + + const sibling = fiber.sibling; + if (sibling !== null) { + sibling.return = fiber.return; + nextEffect = sibling; + return; + } + + nextEffect = nextEffect.return; + } +} + +function commitLayoutEffectsOnFiber( + finishedRoot: FiberRoot, + finishedWork: Fiber, +) { + const tag = finishedWork.tag; + const flags = finishedWork.flags; + if ((flags & (Update | Callback)) !== NoFlags) { + switch (tag) { + case FunctionComponent: + case ForwardRef: + case SimpleMemoComponent: + case Block: { + if ( + enableProfilerTimer && + enableProfilerCommitHooks && + finishedWork.mode & ProfileMode + ) { + try { + startLayoutEffectTimer(); + commitHookEffectListMount(HookLayout | HookHasEffect, finishedWork); + } finally { + recordLayoutEffectDuration(finishedWork); } + } else { + commitHookEffectListMount(HookLayout | HookHasEffect, finishedWork); } + break; + } + case ClassComponent: { + // NOTE: Layout effect durations are measured within this function. + commitLayoutEffectsForClassComponent(finishedWork); + break; + } + case HostRoot: { + commitLayoutEffectsForHostRoot(finishedWork); + break; + } + case HostComponent: { + commitLayoutEffectsForHostComponent(finishedWork); + break; + } + case Profiler: { + commitLayoutEffectsForProfiler(finishedWork, finishedRoot); + break; + } + case SuspenseComponent: { + commitSuspenseHydrationCallbacks(finishedRoot, finishedWork); + break; + } + case FundamentalComponent: + case HostPortal: + case HostText: + case IncompleteClassComponent: + case LegacyHiddenComponent: + case OffscreenComponent: + case ScopeComponent: + case SuspenseListComponent: { + // We have no life-cycles associated with these component types. + break; + } + default: { + invariant( + false, + 'This unit of work tag should not have side-effects. This error is ' + + 'likely caused by a bug in React. Please file an issue.', + ); + } + } + } + + if (enableScopeAPI) { + // TODO: This is a temporary solution that allowed us to transition away from React Flare on www. + if (flags & Ref && tag !== ScopeComponent) { + commitAttachRef(finishedWork); + } + } else { + if (flags & Ref) { + commitAttachRef(finishedWork); + } + } +} + +/** @noinline */ +function commitLayoutEffectsForProfiler( + finishedWork: Fiber, + finishedRoot: FiberRoot, +) { + if (enableProfilerTimer) { + const flags = finishedWork.flags; + const current = finishedWork.alternate; + + const {onCommit, onRender} = finishedWork.memoizedProps; + const {effectDuration} = finishedWork.stateNode; + + const commitTime = getCommitTime(); + + const OnRenderFlag = Update; + const OnCommitFlag = Callback; + + if ((flags & OnRenderFlag) !== NoFlags && typeof onRender === 'function') { + if (enableSchedulerTracing) { + onRender( + finishedWork.memoizedProps.id, + current === null ? 'mount' : 'update', + finishedWork.actualDuration, + finishedWork.treeBaseDuration, + finishedWork.actualStartTime, + commitTime, + finishedRoot.memoizedInteractions, + ); + } else { + onRender( + finishedWork.memoizedProps.id, + current === null ? 'mount' : 'update', + finishedWork.actualDuration, + finishedWork.treeBaseDuration, + finishedWork.actualStartTime, + commitTime, + ); + } + } + + if (enableProfilerCommitHooks) { + if ( + (flags & OnCommitFlag) !== NoFlags && + typeof onCommit === 'function' + ) { + if (enableSchedulerTracing) { + onCommit( + finishedWork.memoizedProps.id, + current === null ? 'mount' : 'update', + effectDuration, + commitTime, + finishedRoot.memoizedInteractions, + ); + } else { + onCommit( + finishedWork.memoizedProps.id, + current === null ? 'mount' : 'update', + effectDuration, + commitTime, + ); + } + } + } + } +} + +/** @noinline */ +function commitLayoutEffectsForClassComponent(finishedWork: Fiber) { + const instance = finishedWork.stateNode; + const current = finishedWork.alternate; + if (finishedWork.flags & Update) { + if (current === null) { + // We could update instance props and state here, + // but instead we rely on them being set during last render. + // TODO: revisit this when we implement resuming. + if (__DEV__) { + if ( + finishedWork.type === finishedWork.elementType && + !didWarnAboutReassigningProps + ) { + if (instance.props !== finishedWork.memoizedProps) { + console.error( + 'Expected %s props to match memoized props before ' + + 'componentDidMount. ' + + 'This might either be because of a bug in React, or because ' + + 'a component reassigns its own `this.props`. ' + + 'Please file an issue.', + getComponentName(finishedWork.type) || 'instance', + ); + } + if (instance.state !== finishedWork.memoizedState) { + console.error( + 'Expected %s state to match memoized state before ' + + 'componentDidMount. ' + + 'This might either be because of a bug in React, or because ' + + 'a component reassigns its own `this.state`. ' + + 'Please file an issue.', + getComponentName(finishedWork.type) || 'instance', + ); + } + } + } + if ( + enableProfilerTimer && + enableProfilerCommitHooks && + finishedWork.mode & ProfileMode + ) { + try { + startLayoutEffectTimer(); + instance.componentDidMount(); + } finally { + recordLayoutEffectDuration(finishedWork); + } + } else { + instance.componentDidMount(); + } + } else { + const prevProps = + finishedWork.elementType === finishedWork.type + ? current.memoizedProps + : resolveDefaultProps(finishedWork.type, current.memoizedProps); + const prevState = current.memoizedState; + // We could update instance props and state here, + // but instead we rely on them being set during last render. + // TODO: revisit this when we implement resuming. + if (__DEV__) { + if ( + finishedWork.type === finishedWork.elementType && + !didWarnAboutReassigningProps + ) { + if (instance.props !== finishedWork.memoizedProps) { + console.error( + 'Expected %s props to match memoized props before ' + + 'componentDidUpdate. ' + + 'This might either be because of a bug in React, or because ' + + 'a component reassigns its own `this.props`. ' + + 'Please file an issue.', + getComponentName(finishedWork.type) || 'instance', + ); + } + if (instance.state !== finishedWork.memoizedState) { + console.error( + 'Expected %s state to match memoized state before ' + + 'componentDidUpdate. ' + + 'This might either be because of a bug in React, or because ' + + 'a component reassigns its own `this.state`. ' + + 'Please file an issue.', + getComponentName(finishedWork.type) || 'instance', + ); + } + } + } + if ( + enableProfilerTimer && + enableProfilerCommitHooks && + finishedWork.mode & ProfileMode + ) { + try { + startLayoutEffectTimer(); + instance.componentDidUpdate( + prevProps, + prevState, + instance.__reactInternalSnapshotBeforeUpdate, + ); + } finally { + recordLayoutEffectDuration(finishedWork); + } + } else { + instance.componentDidUpdate( + prevProps, + prevState, + instance.__reactInternalSnapshotBeforeUpdate, + ); + } + } + } + + // TODO: I think this is now always non-null by the time it reaches the + // commit phase. Consider removing the type check. + const updateQueue: UpdateQueue<*> | null = (finishedWork.updateQueue: any); + if (updateQueue !== null) { + if (__DEV__) { + if ( + finishedWork.type === finishedWork.elementType && + !didWarnAboutReassigningProps + ) { + if (instance.props !== finishedWork.memoizedProps) { + console.error( + 'Expected %s props to match memoized props before ' + + 'processing the update queue. ' + + 'This might either be because of a bug in React, or because ' + + 'a component reassigns its own `this.props`. ' + + 'Please file an issue.', + getComponentName(finishedWork.type) || 'instance', + ); + } + if (instance.state !== finishedWork.memoizedState) { + console.error( + 'Expected %s state to match memoized state before ' + + 'processing the update queue. ' + + 'This might either be because of a bug in React, or because ' + + 'a component reassigns its own `this.state`. ' + + 'Please file an issue.', + getComponentName(finishedWork.type) || 'instance', + ); + } + } + } + // We could update instance props and state here, + // but instead we rely on them being set during last render. + // TODO: revisit this when we implement resuming. + commitUpdateQueue(finishedWork, updateQueue, instance); + } +} + +/** @noinline */ +function commitLayoutEffectsForHostRoot(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<*> | null = (finishedWork.updateQueue: any); + if (updateQueue !== null) { + let instance = null; + if (finishedWork.child !== null) { + switch (finishedWork.child.tag) { + case HostComponent: + instance = getPublicInstance(finishedWork.child.stateNode); + break; + case ClassComponent: + instance = finishedWork.child.stateNode; + break; + } + } + commitUpdateQueue(finishedWork, updateQueue, instance); + } +} + +/** @noinline */ +function commitLayoutEffectsForHostComponent(finishedWork: Fiber) { + const instance: Instance = finishedWork.stateNode; + const current = finishedWork.alternate; + + // Renderers may schedule work to be done after host components are mounted + // (eg DOM renderer may schedule auto-focus for inputs and form controls). + // These effects should only be committed when components are first mounted, + // aka when there is no current/alternate. + if (current === null && finishedWork.flags & Update) { + const type = finishedWork.type; + const props = finishedWork.memoizedProps; + commitMount(instance, type, props, finishedWork); + } +} + +/** @noinline */ +function hideOrUnhideAllChildren(finishedWork, isHidden) { + if (supportsMutation) { + // We only have the top Fiber that was inserted but we need to recurse down its + // children to find all the terminal nodes. + let node: Fiber = finishedWork; + while (true) { + if (node.tag === HostComponent) { + const instance = node.stateNode; + if (isHidden) { + hideInstance(instance); + } else { + unhideInstance(node.stateNode, node.memoizedProps); + } + } else if (node.tag === HostText) { + const instance = node.stateNode; + if (isHidden) { + hideTextInstance(instance); + } else { + unhideTextInstance(instance, node.memoizedProps); + } + } else if ( + (node.tag === OffscreenComponent || + node.tag === LegacyHiddenComponent) && + (node.memoizedState: OffscreenState) !== null && + node !== finishedWork + ) { + // Found a nested Offscreen component that is hidden. Don't search + // any deeper. This tree should remain hidden. + } else if (node.child !== null) { + node.child.return = node; + node = node.child; + continue; + } + if (node === finishedWork) { + return; + } + while (node.sibling === null) { + if (node.return === null || node.return === finishedWork) { + return; + } + node = node.return; + } + node.sibling.return = node.return; + node = node.sibling; + } + } +} + +export function commitPassiveMountEffects( + root: FiberRoot, + firstChild: Fiber, +): void { + if (enableRecursiveCommitTraversal) { + recursivelyCommitPassiveMountEffects(root, firstChild); + } else { + nextEffect = firstChild; + iterativelyCommitPassiveMountEffects_begin(firstChild, root); + } +} + +function recursivelyCommitPassiveMountEffects( + root: FiberRoot, + firstChild: Fiber, +): void { + let fiber = firstChild; + while (fiber !== null) { + let prevProfilerOnStack = null; + if (enableProfilerTimer && enableProfilerCommitHooks) { + if (fiber.tag === Profiler) { + prevProfilerOnStack = nearestProfilerOnStack; + nearestProfilerOnStack = fiber; + } + } + + const primarySubtreeFlags = fiber.subtreeFlags & PassiveMask; + + if (fiber.child !== null && primarySubtreeFlags !== NoFlags) { + recursivelyCommitPassiveMountEffects(root, fiber.child); + } + + if ((fiber.flags & Passive) !== NoFlags) { + if (__DEV__) { + setCurrentDebugFiberInDEV(fiber); + invokeGuardedCallback( + null, + commitPassiveMountOnFiber, + null, + root, + fiber, + ); + if (hasCaughtError()) { + const error = clearCaughtError(); + captureCommitPhaseError(fiber, fiber.return, error); + } + resetCurrentDebugFiberInDEV(); + } else { + try { + commitPassiveMountOnFiber(root, fiber); + } catch (error) { + captureCommitPhaseError(fiber, fiber.return, error); + } + } + } + + if (enableProfilerTimer && enableProfilerCommitHooks) { + if (fiber.tag === Profiler) { + // Bubble times to the next nearest ancestor Profiler. + // After we process that Profiler, we'll bubble further up. + if (prevProfilerOnStack !== null) { + prevProfilerOnStack.stateNode.passiveEffectDuration += + fiber.stateNode.passiveEffectDuration; + } + + nearestProfilerOnStack = prevProfilerOnStack; + } + } + + fiber = fiber.sibling; + } +} + +function iterativelyCommitPassiveMountEffects_begin( + subtreeRoot: Fiber, + root: FiberRoot, +) { + while (nextEffect !== null) { + const fiber = nextEffect; + const firstChild = fiber.child; + if ((fiber.subtreeFlags & PassiveMask) !== NoFlags && firstChild !== null) { + if ( + enableProfilerTimer && + enableProfilerCommitHooks && + fiber.tag === Profiler + ) { + const prevProfilerOnStack = nearestProfilerOnStack; + nearestProfilerOnStack = fiber; + + let child = firstChild; + while (child !== null) { + nextEffect = child; + iterativelyCommitPassiveMountEffects_begin(child, root); + child = child.sibling; + } + nextEffect = fiber; + + if ((fiber.flags & PassiveMask) !== NoFlags) { + if (__DEV__) { + setCurrentDebugFiberInDEV(fiber); + invokeGuardedCallback( + null, + commitProfilerPassiveEffect, + null, + root, + fiber, + ); + if (hasCaughtError()) { + const error = clearCaughtError(); + captureCommitPhaseError(fiber, fiber.return, error); + } + resetCurrentDebugFiberInDEV(); + } else { + try { + commitProfilerPassiveEffect(root, fiber); + } catch (error) { + captureCommitPhaseError(fiber, fiber.return, error); + } + } + } + + // Bubble times to the next nearest ancestor Profiler. + // After we process that Profiler, we'll bubble further up. + if (prevProfilerOnStack !== null) { + prevProfilerOnStack.stateNode.passiveEffectDuration += + fiber.stateNode.passiveEffectDuration; + } + + nearestProfilerOnStack = prevProfilerOnStack; + + if (fiber === subtreeRoot) { + nextEffect = null; + return; + } + const sibling = fiber.sibling; + if (sibling !== null) { + sibling.return = fiber.return; + nextEffect = sibling; + } else { + nextEffect = fiber.return; + iterativelyCommitPassiveMountEffects_complete(subtreeRoot, root); + } + } else { + firstChild.return = fiber; + nextEffect = firstChild; + } + } else { + iterativelyCommitPassiveMountEffects_complete(subtreeRoot, root); + } + } +} + +function iterativelyCommitPassiveMountEffects_complete( + subtreeRoot: Fiber, + root: FiberRoot, +) { + while (nextEffect !== null) { + const fiber = nextEffect; + if ((fiber.flags & Passive) !== NoFlags) { + if (__DEV__) { + setCurrentDebugFiberInDEV(fiber); + invokeGuardedCallback( + null, + commitPassiveMountOnFiber, + null, + root, + fiber, + ); + if (hasCaughtError()) { + const error = clearCaughtError(); + captureCommitPhaseError(fiber, fiber.return, error); + } + resetCurrentDebugFiberInDEV(); + } else { + try { + commitPassiveMountOnFiber(root, fiber); + } catch (error) { + captureCommitPhaseError(fiber, fiber.return, error); + } + } + } + + if (fiber === subtreeRoot) { + nextEffect = null; + return; + } + + const sibling = fiber.sibling; + if (sibling !== null) { + sibling.return = fiber.return; + nextEffect = sibling; + return; + } + + nextEffect = fiber.return; + } +} + +export function commitPassiveUnmountEffects(firstChild: Fiber): void { + if (enableRecursiveCommitTraversal) { + recursivelyCommitPassiveUnmountEffects(firstChild); + } else { + nextEffect = firstChild; + iterativelyCommitPassiveUnmountEffects_begin(); + } +} + +function recursivelyCommitPassiveUnmountEffects(firstChild: Fiber): void { + let fiber = firstChild; + while (fiber !== null) { + const deletions = fiber.deletions; + if (deletions !== null) { + for (let i = 0; i < deletions.length; i++) { + const fiberToDelete = deletions[i]; + recursivelyCommitPassiveUnmountEffectsInsideOfDeletedTree( + fiberToDelete, + fiber, + ); + + // Now that passive effects have been processed, it's safe to detach lingering pointers. + detachFiberAfterEffects(fiberToDelete); + } + } + + const child = fiber.child; + if (child !== null) { + // If any children have passive effects then traverse the subtree. + // Note that this requires checking subtreeFlags of the current Fiber, + // rather than the subtreeFlags/effectsTag of the first child, + // since that would not cover passive effects in siblings. + const passiveFlags = fiber.subtreeFlags & PassiveMask; + if (passiveFlags !== NoFlags) { + recursivelyCommitPassiveUnmountEffects(child); + } + } + + const primaryFlags = fiber.flags & Passive; + if (primaryFlags !== NoFlags) { + setCurrentDebugFiberInDEV(fiber); + commitPassiveUnmountOnFiber(fiber); + resetCurrentDebugFiberInDEV(); + } + + fiber = fiber.sibling; + } +} + +function iterativelyCommitPassiveUnmountEffects_begin() { + while (nextEffect !== null) { + const fiber = nextEffect; + const child = fiber.child; + + // 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 fiberToDelete = deletions[i]; + nextEffect = fiberToDelete; + iterativelyCommitPassiveUnmountEffectsInsideOfDeletedTree_begin( + fiberToDelete, + fiber, + ); + + // Now that passive effects have been processed, it's safe to detach lingering pointers. + detachFiberAfterEffects(fiberToDelete); + } + nextEffect = fiber; + } + + if ((fiber.subtreeFlags & PassiveMask) !== NoFlags && child !== null) { + child.return = fiber; + nextEffect = child; + } else { + iterativelyCommitPassiveUnmountEffects_complete(); + } + } +} + +function iterativelyCommitPassiveUnmountEffects_complete() { + while (nextEffect !== null) { + const fiber = nextEffect; + if ((fiber.flags & Passive) !== NoFlags) { + setCurrentDebugFiberInDEV(fiber); + commitPassiveUnmountOnFiber(fiber); + resetCurrentDebugFiberInDEV(); + } + + const sibling = fiber.sibling; + if (sibling !== null) { + sibling.return = fiber.return; + nextEffect = sibling; + return; + } + + nextEffect = fiber.return; + } +} + +function recursivelyCommitPassiveUnmountEffectsInsideOfDeletedTree( + fiberToDelete: Fiber, + nearestMountedAncestor: Fiber, +): void { + if ((fiberToDelete.subtreeFlags & PassiveStatic) !== NoFlags) { + // If any children have passive effects then traverse the subtree. + // Note that this requires checking subtreeFlags of the current Fiber, + // rather than the subtreeFlags/effectsTag of the first child, + // since that would not cover passive effects in siblings. + let child = fiberToDelete.child; + while (child !== null) { + recursivelyCommitPassiveUnmountEffectsInsideOfDeletedTree( + child, + nearestMountedAncestor, + ); + child = child.sibling; + } + } + + if ((fiberToDelete.flags & PassiveStatic) !== NoFlags) { + setCurrentDebugFiberInDEV(fiberToDelete); + commitPassiveUnmountInsideDeletedTreeOnFiber( + fiberToDelete, + nearestMountedAncestor, + ); + resetCurrentDebugFiberInDEV(); + } +} - if (enableProfilerCommitHooks) { - if (typeof onCommit === 'function') { - if (enableSchedulerTracing) { - onCommit( - finishedWork.memoizedProps.id, - current === null ? 'mount' : 'update', - effectDuration, - commitTime, - finishedRoot.memoizedInteractions, - ); - } else { - onCommit( - finishedWork.memoizedProps.id, - current === null ? 'mount' : 'update', - effectDuration, - commitTime, - ); - } - } +function iterativelyCommitPassiveUnmountEffectsInsideOfDeletedTree_begin( + deletedSubtreeRoot: Fiber, + nearestMountedAncestor: Fiber, +) { + while (nextEffect !== null) { + const fiber = nextEffect; + const child = fiber.child; + if ((fiber.subtreeFlags & PassiveStatic) !== NoFlags && child !== null) { + child.return = fiber; + nextEffect = child; + } else { + iterativelyCommitPassiveUnmountEffectsInsideOfDeletedTree_complete( + deletedSubtreeRoot, + nearestMountedAncestor, + ); + } + } +} - // Schedule a passive effect for this Profiler to call onPostCommit hooks. - // This effect should be scheduled even if there is no onPostCommit callback for this Profiler, - // because the effect is also where times bubble to parent Profilers. - enqueuePendingPassiveProfilerEffect(finishedWork); - - // Propagate layout effect durations to the next nearest Profiler ancestor. - // Do not reset these values until the next render so DevTools has a chance to read them first. - let parentFiber = finishedWork.return; - while (parentFiber !== null) { - if (parentFiber.tag === Profiler) { - const parentStateNode = parentFiber.stateNode; - parentStateNode.effectDuration += effectDuration; - break; - } - parentFiber = parentFiber.return; - } - } - } - return; +function iterativelyCommitPassiveUnmountEffectsInsideOfDeletedTree_complete( + deletedSubtreeRoot: Fiber, + nearestMountedAncestor: Fiber, +) { + while (nextEffect !== null) { + const fiber = nextEffect; + if ((fiber.flags & PassiveStatic) !== NoFlags) { + setCurrentDebugFiberInDEV(fiber); + commitPassiveUnmountInsideDeletedTreeOnFiber( + fiber, + nearestMountedAncestor, + ); + resetCurrentDebugFiberInDEV(); } - case SuspenseComponent: { - commitSuspenseHydrationCallbacks(finishedRoot, finishedWork); + + if (fiber === deletedSubtreeRoot) { + nextEffect = null; return; } - case SuspenseListComponent: - case IncompleteClassComponent: - case FundamentalComponent: - case ScopeComponent: - case OffscreenComponent: - case LegacyHiddenComponent: + + const sibling = fiber.sibling; + if (sibling !== null) { + sibling.return = fiber.return; + nextEffect = sibling; return; + } + + nextEffect = fiber.return; } - invariant( - false, - 'This unit of work tag should not have side-effects. This error is ' + - 'likely caused by a bug in React. Please file an issue.', - ); } -function hideOrUnhideAllChildren(finishedWork, isHidden) { - if (supportsMutation) { - // We only have the top Fiber that was inserted but we need to recurse down its - // children to find all the terminal nodes. - let node: Fiber = finishedWork; - while (true) { - if (node.tag === HostComponent) { - const instance = node.stateNode; - if (isHidden) { - hideInstance(instance); - } else { - unhideInstance(node.stateNode, node.memoizedProps); - } - } else if (node.tag === HostText) { - const instance = node.stateNode; - if (isHidden) { - hideTextInstance(instance); - } else { - unhideTextInstance(instance, node.memoizedProps); - } - } else if ( - (node.tag === OffscreenComponent || - node.tag === LegacyHiddenComponent) && - (node.memoizedState: OffscreenState) !== null && - node !== finishedWork - ) { - // Found a nested Offscreen component that is hidden. Don't search - // any deeper. This tree should remain hidden. - } else if (node.child !== null) { - node.child.return = node; - node = node.child; - continue; - } - if (node === finishedWork) { - return; - } - while (node.sibling === null) { - if (node.return === null || node.return === finishedWork) { - return; - } - node = node.return; - } - node.sibling.return = node.return; - node = node.sibling; - } +function detachFiberAfterEffects(fiber: Fiber): void { + // Null out fields to improve GC for references that may be lingering (e.g. DevTools). + // Note that we already cleared the return pointer in detachFiberMutation(). + fiber.child = null; + fiber.deletions = null; + fiber.dependencies = null; + fiber.memoizedProps = null; + fiber.memoizedState = null; + fiber.pendingProps = null; + fiber.sibling = null; + fiber.stateNode = null; + fiber.updateQueue = null; + + if (__DEV__) { + fiber._debugOwner = null; } } @@ -916,6 +2122,7 @@ function commitDetachRef(current: Fiber) { function commitUnmount( finishedRoot: FiberRoot, current: Fiber, + nearestMountedAncestor: Fiber, renderPriorityLevel: ReactPriorityLevel, ): void { onCommitUnmount(current); @@ -936,19 +2143,17 @@ function commitUnmount( do { const {destroy, tag} = effect; if (destroy !== undefined) { - if ((tag & HookPassive) !== NoHookEffect) { - enqueuePendingPassiveHookEffectUnmount(current, effect); - } else { + if ((tag & HookLayout) !== NoHookEffect) { if ( enableProfilerTimer && enableProfilerCommitHooks && current.mode & ProfileMode ) { startLayoutEffectTimer(); - safelyCallDestroy(current, destroy); + safelyCallDestroy(current, nearestMountedAncestor, destroy); recordLayoutEffectDuration(current); } else { - safelyCallDestroy(current, destroy); + safelyCallDestroy(current, nearestMountedAncestor, destroy); } } } @@ -959,15 +2164,19 @@ function commitUnmount( return; } case ClassComponent: { - safelyDetachRef(current); + safelyDetachRef(current, nearestMountedAncestor); const instance = current.stateNode; if (typeof instance.componentWillUnmount === 'function') { - safelyCallComponentWillUnmount(current, instance); + safelyCallComponentWillUnmount( + current, + instance, + nearestMountedAncestor, + ); } return; } case HostComponent: { - safelyDetachRef(current); + safelyDetachRef(current, nearestMountedAncestor); return; } case HostPortal: { @@ -975,7 +2184,12 @@ function commitUnmount( // We are also not using this parent because // the portal will get pushed immediately. if (supportsMutation) { - unmountHostComponents(finishedRoot, current, renderPriorityLevel); + unmountHostComponents( + finishedRoot, + current, + nearestMountedAncestor, + renderPriorityLevel, + ); } else if (supportsPersistence) { emptyPortalContainer(current); } @@ -1005,7 +2219,7 @@ function commitUnmount( } case ScopeComponent: { if (enableScopeAPI) { - safelyDetachRef(current); + safelyDetachRef(current, nearestMountedAncestor); } return; } @@ -1015,6 +2229,7 @@ function commitUnmount( function commitNestedUnmounts( finishedRoot: FiberRoot, root: Fiber, + nearestMountedAncestor: Fiber, renderPriorityLevel: ReactPriorityLevel, ): void { // While we're inside a removed host node we don't want to call @@ -1024,7 +2239,12 @@ function commitNestedUnmounts( // we do an inner loop while we're still inside the host node. let node: Fiber = root; while (true) { - commitUnmount(finishedRoot, node, renderPriorityLevel); + commitUnmount( + finishedRoot, + node, + nearestMountedAncestor, + renderPriorityLevel, + ); // Visit children because they may contain more composite or host nodes. // Skip portals because commitUnmount() currently visits them recursively. if ( @@ -1052,32 +2272,24 @@ function commitNestedUnmounts( } function detachFiberMutation(fiber: Fiber) { - // Cut off the return pointers to disconnect it from the tree. Ideally, we - // should clear the child pointer of the parent alternate to let this + // 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: we cannot null out sibling here, otherwise it can cause issues - // with findDOMNode and how it requires the sibling field to carry out - // traversal in a later effect. See PR #16820. We now clear the sibling - // field after effects, see: detachFiberAfterEffects. + // 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. // - // Don't disconnect stateNode now; it will be detached in detachFiberAfterEffects. - // It may be required if the current component is an error boundary, - // and one of its descendants throws while unmounting a passive effect. - fiber.alternate = null; - fiber.child = null; - fiber.dependencies = null; - fiber.firstEffect = null; - fiber.lastEffect = null; - fiber.memoizedProps = null; - fiber.memoizedState = null; - fiber.pendingProps = null; - fiber.return = null; - fiber.updateQueue = null; - if (__DEV__) { - fiber._debugOwner = null; + // 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). + const alternate = fiber.alternate; + if (alternate !== null) { + alternate.return = null; + fiber.alternate = null; } + fiber.return = null; } function emptyPortalContainer(current: Fiber) { @@ -1315,6 +2527,7 @@ function insertOrAppendPlacementNode( function unmountHostComponents( finishedRoot: FiberRoot, current: Fiber, + nearestMountedAncestor: Fiber, renderPriorityLevel: ReactPriorityLevel, ): void { // We only have the top Fiber that was deleted but we need to recurse down its @@ -1364,7 +2577,12 @@ function unmountHostComponents( } if (node.tag === HostComponent || node.tag === HostText) { - commitNestedUnmounts(finishedRoot, node, renderPriorityLevel); + commitNestedUnmounts( + finishedRoot, + node, + nearestMountedAncestor, + renderPriorityLevel, + ); // After all the children have unmounted, it is now safe to remove the // node from the tree. if (currentParentIsContainer) { @@ -1381,7 +2599,12 @@ function unmountHostComponents( // Don't visit children because we already visited them. } else if (enableFundamentalAPI && node.tag === FundamentalComponent) { const fundamentalNode = node.stateNode.instance; - commitNestedUnmounts(finishedRoot, node, renderPriorityLevel); + commitNestedUnmounts( + finishedRoot, + node, + nearestMountedAncestor, + renderPriorityLevel, + ); // After all the children have unmounted, it is now safe to remove the // node from the tree. if (currentParentIsContainer) { @@ -1433,7 +2656,12 @@ function unmountHostComponents( continue; } } else { - commitUnmount(finishedRoot, node, renderPriorityLevel); + commitUnmount( + finishedRoot, + node, + nearestMountedAncestor, + renderPriorityLevel, + ); // Visit children because we may find more host components below. if (node.child !== null) { node.child.return = node; @@ -1463,15 +2691,26 @@ function unmountHostComponents( function commitDeletion( finishedRoot: FiberRoot, current: Fiber, + nearestMountedAncestor: Fiber, renderPriorityLevel: ReactPriorityLevel, ): void { if (supportsMutation) { // Recursively delete all host nodes from the parent. // Detach refs and call componentWillUnmount() on the whole subtree. - unmountHostComponents(finishedRoot, current, renderPriorityLevel); + unmountHostComponents( + finishedRoot, + current, + nearestMountedAncestor, + renderPriorityLevel, + ); } else { // Detach refs and call componentWillUnmount() on the whole subtree. - commitNestedUnmounts(finishedRoot, current, renderPriorityLevel); + commitNestedUnmounts( + finishedRoot, + current, + nearestMountedAncestor, + renderPriorityLevel, + ); } const alternate = current.alternate; detachFiberMutation(current); @@ -1503,12 +2742,17 @@ function commitWork(current: Fiber | null, finishedWork: Fiber): void { commitHookEffectListUnmount( HookLayout | HookHasEffect, finishedWork, + finishedWork.return, ); } finally { recordLayoutEffectDuration(finishedWork); } } else { - commitHookEffectListUnmount(HookLayout | HookHasEffect, finishedWork); + commitHookEffectListUnmount( + HookLayout | HookHasEffect, + finishedWork, + finishedWork.return, + ); } return; } @@ -1563,12 +2807,20 @@ function commitWork(current: Fiber | null, finishedWork: Fiber): void { ) { try { startLayoutEffectTimer(); - commitHookEffectListUnmount(HookLayout | HookHasEffect, finishedWork); + commitHookEffectListUnmount( + HookLayout | HookHasEffect, + finishedWork, + finishedWork.return, + ); } finally { recordLayoutEffectDuration(finishedWork); } } else { - commitHookEffectListUnmount(HookLayout | HookHasEffect, finishedWork); + commitHookEffectListUnmount( + HookLayout | HookHasEffect, + finishedWork, + finishedWork.return, + ); } return; } @@ -1710,6 +2962,7 @@ function commitSuspenseComponent(finishedWork: Fiber) { } } +/** @noinline */ function commitSuspenseHydrationCallbacks( finishedRoot: FiberRoot, finishedWork: Fiber, @@ -1771,7 +3024,7 @@ function attachSuspenseRetryListeners(finishedWork: Fiber) { // 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( +function isSuspenseBoundaryBeingHidden( current: Fiber | null, finishedWork: Fiber, ): boolean { @@ -1785,20 +3038,281 @@ export function isSuspenseBoundaryBeingHidden( return false; } -function commitResetTextContent(current: Fiber) { +function commitResetTextContent(current: Fiber): void { if (!supportsMutation) { return; } resetTextContent(current.stateNode); } -export { - commitBeforeMutationLifeCycles, - commitResetTextContent, - commitPlacement, - commitDeletion, - commitWork, - commitLifeCycles, - commitAttachRef, - commitDetachRef, -}; +function commitPassiveUnmountOnFiber(finishedWork: Fiber): void { + switch (finishedWork.tag) { + case FunctionComponent: + case ForwardRef: + case SimpleMemoComponent: + case Block: { + if ( + enableProfilerTimer && + enableProfilerCommitHooks && + finishedWork.mode & ProfileMode + ) { + startPassiveEffectTimer(); + commitHookEffectListUnmount( + HookPassive | HookHasEffect, + finishedWork, + finishedWork.return, + ); + recordPassiveEffectDuration(finishedWork); + } else { + commitHookEffectListUnmount( + HookPassive | HookHasEffect, + finishedWork, + finishedWork.return, + ); + } + break; + } + } +} + +function commitPassiveUnmountInsideDeletedTreeOnFiber( + current: Fiber, + nearestMountedAncestor: Fiber | null, +): void { + switch (current.tag) { + case FunctionComponent: + case ForwardRef: + case SimpleMemoComponent: + case Block: { + if ( + enableProfilerTimer && + enableProfilerCommitHooks && + current.mode & ProfileMode + ) { + startPassiveEffectTimer(); + commitHookEffectListUnmount( + HookPassive, + current, + nearestMountedAncestor, + ); + recordPassiveEffectDuration(current); + } else { + commitHookEffectListUnmount( + HookPassive, + current, + nearestMountedAncestor, + ); + } + break; + } + } +} + +function commitPassiveMountOnFiber( + finishedRoot: FiberRoot, + finishedWork: Fiber, +): void { + switch (finishedWork.tag) { + case FunctionComponent: + case ForwardRef: + case SimpleMemoComponent: + case Block: { + if ( + enableProfilerTimer && + enableProfilerCommitHooks && + finishedWork.mode & ProfileMode + ) { + startPassiveEffectTimer(); + try { + commitHookEffectListMount(HookPassive | HookHasEffect, finishedWork); + } finally { + recordPassiveEffectDuration(finishedWork); + } + } else { + commitHookEffectListMount(HookPassive | HookHasEffect, finishedWork); + } + break; + } + case Profiler: { + commitProfilerPassiveEffect(finishedRoot, finishedWork); + break; + } + } +} + +function invokeLayoutEffectMountInDEV(fiber: Fiber): void { + if (__DEV__ && enableDoubleInvokingEffects) { + // We don't need to re-check for legacy roots here. + // This function will not be called within legacy roots. + switch (fiber.tag) { + case FunctionComponent: + case ForwardRef: + case SimpleMemoComponent: + case Block: { + invokeGuardedCallback( + null, + commitHookEffectListMount, + null, + HookLayout | HookHasEffect, + fiber, + ); + if (hasCaughtError()) { + const mountError = clearCaughtError(); + captureCommitPhaseError(fiber, fiber.return, mountError); + } + break; + } + case ClassComponent: { + const instance = fiber.stateNode; + invokeGuardedCallback(null, instance.componentDidMount, instance); + if (hasCaughtError()) { + const mountError = clearCaughtError(); + captureCommitPhaseError(fiber, fiber.return, mountError); + } + break; + } + } + } +} + +function invokePassiveEffectMountInDEV(fiber: Fiber): void { + if (__DEV__ && enableDoubleInvokingEffects) { + // We don't need to re-check for legacy roots here. + // This function will not be called within legacy roots. + switch (fiber.tag) { + case FunctionComponent: + case ForwardRef: + case SimpleMemoComponent: + case Block: { + invokeGuardedCallback( + null, + commitHookEffectListMount, + null, + HookPassive | HookHasEffect, + fiber, + ); + if (hasCaughtError()) { + const mountError = clearCaughtError(); + captureCommitPhaseError(fiber, fiber.return, mountError); + } + break; + } + } + } +} + +function invokeLayoutEffectUnmountInDEV(fiber: Fiber): void { + if (__DEV__ && enableDoubleInvokingEffects) { + // We don't need to re-check for legacy roots here. + // This function will not be called within legacy roots. + switch (fiber.tag) { + case FunctionComponent: + case ForwardRef: + case SimpleMemoComponent: + case Block: { + invokeGuardedCallback( + null, + commitHookEffectListUnmount, + null, + HookLayout | HookHasEffect, + fiber, + fiber.return, + ); + if (hasCaughtError()) { + const unmountError = clearCaughtError(); + captureCommitPhaseError(fiber, fiber.return, unmountError); + } + break; + } + case ClassComponent: { + const instance = fiber.stateNode; + if (typeof instance.componentWillUnmount === 'function') { + safelyCallComponentWillUnmount(fiber, instance, fiber.return); + } + break; + } + } + } +} + +function invokePassiveEffectUnmountInDEV(fiber: Fiber): void { + if (__DEV__ && enableDoubleInvokingEffects) { + // We don't need to re-check for legacy roots here. + // This function will not be called within legacy roots. + switch (fiber.tag) { + case FunctionComponent: + case ForwardRef: + case SimpleMemoComponent: + case Block: { + invokeGuardedCallback( + null, + commitHookEffectListUnmount, + null, + HookPassive | HookHasEffect, + fiber, + fiber.return, + ); + if (hasCaughtError()) { + const unmountError = clearCaughtError(); + captureCommitPhaseError(fiber, fiber.return, unmountError); + } + break; + } + } + } +} + +// TODO: Convert this to iteration instead of recursion, too. Leaving this for +// a follow up because the flag is off. +export function commitDoubleInvokeEffectsInDEV( + fiber: Fiber, + hasPassiveEffects: boolean, +) { + if (__DEV__ && enableDoubleInvokingEffects) { + // Never double-invoke effects for legacy roots. + if ((fiber.mode & (BlockingMode | ConcurrentMode)) === NoMode) { + return; + } + + setCurrentDebugFiberInDEV(fiber); + invokeEffectsInDev(fiber, MountLayoutDev, invokeLayoutEffectUnmountInDEV); + if (hasPassiveEffects) { + invokeEffectsInDev( + fiber, + MountPassiveDev, + invokePassiveEffectUnmountInDEV, + ); + } + + invokeEffectsInDev(fiber, MountLayoutDev, invokeLayoutEffectMountInDEV); + if (hasPassiveEffects) { + invokeEffectsInDev(fiber, MountPassiveDev, invokePassiveEffectMountInDEV); + } + resetCurrentDebugFiberInDEV(); + } +} + +function invokeEffectsInDev( + firstChild: Fiber, + fiberFlags: Flags, + invokeEffectFn: (fiber: Fiber) => void, +): void { + if (__DEV__ && enableDoubleInvokingEffects) { + // We don't need to re-check for legacy roots here. + // This function will not be called within legacy roots. + let fiber = firstChild; + while (fiber !== null) { + if (fiber.child !== null) { + const primarySubtreeFlag = fiber.subtreeFlags & fiberFlags; + if (primarySubtreeFlag !== NoFlags) { + invokeEffectsInDev(fiber.child, fiberFlags, invokeEffectFn); + } + } + + if ((fiber.flags & fiberFlags) !== NoFlags) { + invokeEffectFn(fiber); + } + fiber = fiber.sibling; + } + } +} diff --git a/packages/react-reconciler/src/ReactFiberCompleteWork.new.js b/packages/react-reconciler/src/ReactFiberCompleteWork.new.js index e32b0d394f5b6..f3709599d3576 100644 --- a/packages/react-reconciler/src/ReactFiberCompleteWork.new.js +++ b/packages/react-reconciler/src/ReactFiberCompleteWork.new.js @@ -180,12 +180,18 @@ function hadNoMutationsEffects(current: null | Fiber, completedWork: Fiber) { return true; } + if ((completedWork.flags & Deletion) !== NoFlags) { + return false; + } + + // TODO: If we move the `hadNoMutationsEffects` call after `bubbleProperties` + // then we only have to check the `completedWork.subtreeFlags`. let child = completedWork.child; while (child !== null) { - if ((child.flags & MutationMask) !== NoFlags) { + if ((child.flags & (MutationMask | Deletion)) !== NoFlags) { return false; } - if ((child.subtreeFlags & MutationMask) !== NoFlags) { + if ((child.subtreeFlags & (MutationMask | Deletion)) !== NoFlags) { return false; } child = child.sibling; diff --git a/packages/react-reconciler/src/ReactFiberCompleteWork.old.js b/packages/react-reconciler/src/ReactFiberCompleteWork.old.js index 618705accff86..889122bce7194 100644 --- a/packages/react-reconciler/src/ReactFiberCompleteWork.old.js +++ b/packages/react-reconciler/src/ReactFiberCompleteWork.old.js @@ -8,7 +8,7 @@ */ import type {Fiber} from './ReactInternalTypes'; -import type {Lanes} from './ReactFiberLane'; +import type {Lanes, Lane} from './ReactFiberLane'; import type { ReactFundamentalComponentInstance, ReactScopeInstance, @@ -58,8 +58,28 @@ import { OffscreenComponent, LegacyHiddenComponent, } from './ReactWorkTags'; -import {NoMode, BlockingMode, ProfileMode} from './ReactTypeOfMode'; -import {Ref, Update, NoFlags, DidCapture, Snapshot} from './ReactFiberFlags'; +import { + NoMode, + BlockingMode, + ConcurrentMode, + ProfileMode, +} from './ReactTypeOfMode'; +import { + Ref, + Update, + Callback, + Passive, + Deletion, + NoFlags, + DidCapture, + Snapshot, + Visibility, + MutationMask, + LayoutMask, + PassiveMask, + StaticMask, + PerformedWork, +} from './ReactFiberFlags'; import invariant from 'shared/invariant'; import { @@ -130,9 +150,16 @@ import { renderHasNotSuspendedYet, popRenderLanes, getRenderTargetTime, + subtreeRenderLanes, } from './ReactFiberWorkLoop.old'; import {createFundamentalStateInstance} from './ReactFiberFundamental.old'; -import {OffscreenLane, SomeRetryLane} from './ReactFiberLane'; +import { + OffscreenLane, + SomeRetryLane, + NoLanes, + includesSomeLane, + mergeLanes, +} from './ReactFiberLane'; import {resetChildFibers} from './ReactChildFiber.old'; import {createScopeInstance} from './ReactFiberScope.old'; import {transferActualDuration} from './ReactProfilerTimer.old'; @@ -147,6 +174,31 @@ function markRef(workInProgress: Fiber) { workInProgress.flags |= Ref; } +function hadNoMutationsEffects(current: null | Fiber, completedWork: Fiber) { + const didBailout = current !== null && current.child === completedWork.child; + if (didBailout) { + return true; + } + + if ((completedWork.flags & Deletion) !== NoFlags) { + return false; + } + + // TODO: If we move the `hadNoMutationsEffects` call after `bubbleProperties` + // then we only have to check the `completedWork.subtreeFlags`. + let child = completedWork.child; + while (child !== null) { + if ((child.flags & (MutationMask | Deletion)) !== NoFlags) { + return false; + } + if ((child.subtreeFlags & (MutationMask | Deletion)) !== NoFlags) { + return false; + } + child = child.sibling; + } + return true; +} + let appendAllChildren; let updateHostContainer; let updateHostComponent; @@ -191,7 +243,7 @@ if (supportsMutation) { } }; - updateHostContainer = function(workInProgress: Fiber) { + updateHostContainer = function(current: null | Fiber, workInProgress: Fiber) { // Noop }; updateHostComponent = function( @@ -435,13 +487,13 @@ if (supportsMutation) { node = node.sibling; } }; - updateHostContainer = function(workInProgress: Fiber) { + updateHostContainer = function(current: null | Fiber, workInProgress: Fiber) { const portalOrRoot: { containerInfo: Container, pendingChildren: ChildSet, ... } = workInProgress.stateNode; - const childrenUnchanged = workInProgress.firstEffect === null; + const childrenUnchanged = hadNoMutationsEffects(current, workInProgress); if (childrenUnchanged) { // No changes, just reuse the existing instance. } else { @@ -466,7 +518,7 @@ if (supportsMutation) { const oldProps = current.memoizedProps; // If there are no effects associated with this node, then none of our children had any updates. // This guarantees that we can reuse all of them. - const childrenUnchanged = workInProgress.firstEffect === null; + const childrenUnchanged = hadNoMutationsEffects(current, workInProgress); if (childrenUnchanged && oldProps === newProps) { // No changes, just reuse the existing instance. // Note that this might release a previous clone. @@ -549,7 +601,7 @@ if (supportsMutation) { }; } else { // No host operations - updateHostContainer = function(workInProgress: Fiber) { + updateHostContainer = function(current: null | Fiber, workInProgress: Fiber) { // Noop }; updateHostComponent = function( @@ -642,6 +694,116 @@ function cutOffTailIfNeeded( } } +function bubbleProperties(completedWork: Fiber) { + const didBailout = + completedWork.alternate !== null && + completedWork.alternate.child === completedWork.child; + + let newChildLanes = NoLanes; + let subtreeFlags = NoFlags; + + if (!didBailout) { + // Bubble up the earliest expiration time. + if (enableProfilerTimer && (completedWork.mode & ProfileMode) !== NoMode) { + // In profiling mode, resetChildExpirationTime is also used to reset + // profiler durations. + let actualDuration = completedWork.actualDuration; + let treeBaseDuration = ((completedWork.selfBaseDuration: any): number); + + let child = completedWork.child; + while (child !== null) { + newChildLanes = mergeLanes( + newChildLanes, + mergeLanes(child.lanes, child.childLanes), + ); + + subtreeFlags |= child.subtreeFlags; + subtreeFlags |= child.flags; + + // When a fiber is cloned, its actualDuration is reset to 0. This value will + // only be updated if work is done on the fiber (i.e. it doesn't bailout). + // When work is done, it should bubble to the parent's actualDuration. If + // the fiber has not been cloned though, (meaning no work was done), then + // this value will reflect the amount of time spent working on a previous + // render. In that case it should not bubble. We determine whether it was + // cloned by comparing the child pointer. + actualDuration += child.actualDuration; + + treeBaseDuration += child.treeBaseDuration; + child = child.sibling; + } + + completedWork.actualDuration = actualDuration; + completedWork.treeBaseDuration = treeBaseDuration; + } else { + let child = completedWork.child; + while (child !== null) { + newChildLanes = mergeLanes( + newChildLanes, + mergeLanes(child.lanes, child.childLanes), + ); + + subtreeFlags |= child.subtreeFlags; + subtreeFlags |= child.flags; + + child = child.sibling; + } + } + + completedWork.subtreeFlags |= subtreeFlags; + } else { + // Bubble up the earliest expiration time. + if (enableProfilerTimer && (completedWork.mode & ProfileMode) !== NoMode) { + // In profiling mode, resetChildExpirationTime is also used to reset + // profiler durations. + let treeBaseDuration = ((completedWork.selfBaseDuration: any): number); + + let child = completedWork.child; + while (child !== null) { + newChildLanes = mergeLanes( + newChildLanes, + mergeLanes(child.lanes, child.childLanes), + ); + + // "Static" flags share the lifetime of the fiber/hook they belong to, + // so we should bubble those up even during a bailout. All the other + // flags have a lifetime only of a single render + commit, so we should + // ignore them. + subtreeFlags |= child.subtreeFlags & StaticMask; + subtreeFlags |= child.flags & StaticMask; + + treeBaseDuration += child.treeBaseDuration; + child = child.sibling; + } + + completedWork.treeBaseDuration = treeBaseDuration; + } else { + let child = completedWork.child; + while (child !== null) { + newChildLanes = mergeLanes( + newChildLanes, + mergeLanes(child.lanes, child.childLanes), + ); + + // "Static" flags share the lifetime of the fiber/hook they belong to, + // so we should bubble those up even during a bailout. All the other + // flags have a lifetime only of a single render + commit, so we should + // ignore them. + subtreeFlags |= child.subtreeFlags & StaticMask; + subtreeFlags |= child.flags & StaticMask; + + child = child.sibling; + } + } + + completedWork.subtreeFlags |= subtreeFlags; + } + + completedWork.childLanes = newChildLanes; + + return didBailout; +} + function completeWork( current: Fiber | null, workInProgress: Fiber, @@ -657,15 +819,16 @@ function completeWork( case ForwardRef: case Fragment: case Mode: - case Profiler: case ContextConsumer: case MemoComponent: + bubbleProperties(workInProgress); return null; case ClassComponent: { const Component = workInProgress.type; if (isLegacyContextProvider(Component)) { popLegacyContext(workInProgress); } + bubbleProperties(workInProgress); return null; } case HostRoot: { @@ -693,7 +856,8 @@ function completeWork( workInProgress.flags |= Snapshot; } } - updateHostContainer(workInProgress); + updateHostContainer(current, workInProgress); + bubbleProperties(workInProgress); return null; } case HostComponent: { @@ -720,6 +884,7 @@ function completeWork( 'caused by a bug in React. Please file an issue.', ); // This can happen when we abort work. + bubbleProperties(workInProgress); return null; } @@ -777,6 +942,7 @@ function completeWork( markRef(workInProgress); } } + bubbleProperties(workInProgress); return null; } case HostText: { @@ -811,6 +977,58 @@ function completeWork( ); } } + bubbleProperties(workInProgress); + return null; + } + case Profiler: { + const didBailout = bubbleProperties(workInProgress); + if (!didBailout) { + // Use subtreeFlags to determine which commit callbacks should fire. + // TODO: Move this logic to the commit phase, since we already check if + // a fiber's subtree contains effects. Refactor the commit phase's + // depth-first traversal so that we can put work tag-specific logic + // before or after committing a subtree's effects. + const OnRenderFlag = Update; + const OnCommitFlag = Callback; + const OnPostCommitFlag = Passive; + const subtreeFlags = workInProgress.subtreeFlags; + const flags = workInProgress.flags; + let newFlags = flags; + + // Call onRender any time this fiber or its subtree are worked on. + if ( + (flags & PerformedWork) !== NoFlags || + (subtreeFlags & PerformedWork) !== NoFlags + ) { + newFlags |= OnRenderFlag; + } + + // Call onCommit only if the subtree contains layout work, or if it + // contains deletions, since those might result in unmount work, which + // we include in the same measure. + // TODO: Can optimize by using a static flag to track whether a tree + // contains layout effects, like we do for passive effects. + if ( + (flags & (LayoutMask | Deletion)) !== NoFlags || + (subtreeFlags & (LayoutMask | Deletion)) !== NoFlags + ) { + newFlags |= OnCommitFlag; + } + + // Call onPostCommit only if the subtree contains passive work. + // Don't have to check for deletions, because Deletion is already + // a passive flag. + if ( + (flags & PassiveMask) !== NoFlags || + (subtreeFlags & PassiveMask) !== NoFlags + ) { + newFlags |= OnPostCommitFlag; + } + workInProgress.flags = newFlags; + } else { + // This fiber and its subtree bailed out, so don't fire any callbacks. + } + return null; } case SuspenseComponent: { @@ -830,6 +1048,20 @@ function completeWork( if (enableSchedulerTracing) { markSpawnedWork(OffscreenLane); } + bubbleProperties(workInProgress); + if (enableProfilerTimer) { + if ((workInProgress.mode & ProfileMode) !== NoMode) { + const isTimedOutSuspense = nextState !== null; + if (isTimedOutSuspense) { + // Don't count time spent in a timed out Suspense subtree as part of the base duration. + const primaryChildFragment = workInProgress.child; + if (primaryChildFragment !== null) { + // $FlowFixMe Flow doens't support type casting in combiation with the -= operator + workInProgress.treeBaseDuration -= ((primaryChildFragment.treeBaseDuration: any): number); + } + } + } + } return null; } else { // We should never have been in a hydration state if we didn't have a current. @@ -846,6 +1078,20 @@ function completeWork( // If something suspended, schedule an effect to attach retry listeners. // So we might as well always mark this. workInProgress.flags |= Update; + bubbleProperties(workInProgress); + if (enableProfilerTimer) { + if ((workInProgress.mode & ProfileMode) !== NoMode) { + const isTimedOutSuspense = nextState !== null; + if (isTimedOutSuspense) { + // Don't count time spent in a timed out Suspense subtree as part of the base duration. + const primaryChildFragment = workInProgress.child; + if (primaryChildFragment !== null) { + // $FlowFixMe Flow doens't support type casting in combiation with the -= operator + workInProgress.treeBaseDuration -= ((primaryChildFragment.treeBaseDuration: any): number); + } + } + } + } return null; } } @@ -861,6 +1107,7 @@ function completeWork( ) { transferActualDuration(workInProgress); } + // Don't bubble properties in this case. return workInProgress; } @@ -914,8 +1161,8 @@ function completeWork( // TODO: Only schedule updates if not prevDidTimeout. if (nextDidTimeout) { // If this boundary just timed out, schedule an effect to attach a - // retry listener to the promise. This flag is also used to hide the - // primary children. + // retry listener to the promise. + // TODO: Move to passive phase workInProgress.flags |= Update; } } @@ -927,7 +1174,7 @@ function completeWork( // primary children. In mutation mode, we also need the flag to // *unhide* children that were previously hidden, so check if this // is currently timed out, too. - workInProgress.flags |= Update; + workInProgress.flags |= Update | Visibility; } } if ( @@ -936,20 +1183,36 @@ function completeWork( workInProgress.memoizedProps.suspenseCallback != null ) { // Always notify the callback + // TODO: Move to passive phase workInProgress.flags |= Update; } + bubbleProperties(workInProgress); + if (enableProfilerTimer) { + if ((workInProgress.mode & ProfileMode) !== NoMode) { + if (nextDidTimeout) { + // Don't count time spent in a timed out Suspense subtree as part of the base duration. + const primaryChildFragment = workInProgress.child; + if (primaryChildFragment !== null) { + // $FlowFixMe Flow doens't support type casting in combiation with the -= operator + workInProgress.treeBaseDuration -= ((primaryChildFragment.treeBaseDuration: any): number); + } + } + } + } return null; } case HostPortal: popHostContainer(workInProgress); - updateHostContainer(workInProgress); + updateHostContainer(current, workInProgress); if (current === null) { preparePortalMount(workInProgress.stateNode.containerInfo); } + bubbleProperties(workInProgress); return null; case ContextProvider: // Pop provider fiber popProvider(workInProgress); + bubbleProperties(workInProgress); return null; case IncompleteClassComponent: { // Same as class component case. I put it down here so that the tags are @@ -958,6 +1221,7 @@ function completeWork( if (isLegacyContextProvider(Component)) { popLegacyContext(workInProgress); } + bubbleProperties(workInProgress); return null; } case SuspenseListComponent: { @@ -969,6 +1233,7 @@ function completeWork( if (renderState === null) { // We're running in the default, "independent" mode. // We don't do anything in this mode. + bubbleProperties(workInProgress); return null; } @@ -1021,12 +1286,8 @@ function completeWork( // Rerender the whole list, but this time, we'll force fallbacks // to stay in place. - // Reset the effect list before doing the second pass since that's now invalid. - if (renderState.lastEffect === null) { - workInProgress.firstEffect = null; - } - workInProgress.lastEffect = renderState.lastEffect; // Reset the child fibers to their original state. + workInProgress.subtreeFlags = NoFlags; resetChildFibers(workInProgress, renderLanes); // Set up the Suspense Context to force suspense and immediately @@ -1038,6 +1299,7 @@ function completeWork( ForceSuspenseFallback, ), ); + // Don't bubble properties in this case. return workInProgress.child; } row = row.sibling; @@ -1094,16 +1356,8 @@ function completeWork( !renderedTail.alternate && !getIsHydrating() // We don't cut it if we're hydrating. ) { - // We need to delete the row we just rendered. - // Reset the effect list to what it was before we rendered this - // child. The nested children have already appended themselves. - const lastEffect = (workInProgress.lastEffect = - renderState.lastEffect); - // Remove any effects that were appended after this point. - if (lastEffect !== null) { - lastEffect.nextEffect = null; - } // We're done. + bubbleProperties(workInProgress); return null; } } else if ( @@ -1123,13 +1377,10 @@ function completeWork( cutOffTailIfNeeded(renderState, false); // Since nothing actually suspended, there will nothing to ping this - // to get it started back up to attempt the next item. While in terms - // of priority this work has the same priority as this current render, - // it's not part of the same transition once the transition has - // committed. If it's sync, we still want to yield so that it can be - // painted. Conceptually, this is really the same as pinging. - // We can use any RetryLane even if it's the one currently rendering - // since we're leaving it behind on this node. + // to get it started back up to attempt the next item. If we can show + // them, then they really have the same priority as this render. + // So we'll pick it back up the very next render pass once we've had + // an opportunity to yield for paint. workInProgress.lanes = SomeRetryLane; if (enableSchedulerTracing) { markSpawnedWork(SomeRetryLane); @@ -1161,7 +1412,6 @@ function completeWork( const next = renderState.tail; renderState.rendering = next; renderState.tail = next.sibling; - renderState.lastEffect = workInProgress.lastEffect; renderState.renderingStartTime = now(); next.sibling = null; @@ -1179,8 +1429,10 @@ function completeWork( } pushSuspenseContext(workInProgress, suspenseContext); // Do a pass over the next row. + // Don't bubble properties in this case. return next; } + bubbleProperties(workInProgress); return null; } case FundamentalComponent: { @@ -1208,6 +1460,7 @@ function completeWork( ): any): Instance); fundamentalInstance.instance = instance; if (fundamentalImpl.reconcileChildren === false) { + bubbleProperties(workInProgress); return null; } appendAllChildren(instance, workInProgress, false, false); @@ -1230,6 +1483,7 @@ function completeWork( markUpdate(workInProgress); } } + bubbleProperties(workInProgress); return null; } break; @@ -1252,31 +1506,44 @@ function completeWork( markRef(workInProgress); } } + bubbleProperties(workInProgress); return null; } break; } case Block: if (enableBlocksAPI) { + bubbleProperties(workInProgress); return null; } break; case OffscreenComponent: case LegacyHiddenComponent: { popRenderLanes(workInProgress); + const nextState: OffscreenState | null = workInProgress.memoizedState; + const nextIsHidden = nextState !== null; + if (current !== null) { - const nextState: OffscreenState | null = workInProgress.memoizedState; const prevState: OffscreenState | null = current.memoizedState; const prevIsHidden = prevState !== null; - const nextIsHidden = nextState !== null; if ( prevIsHidden !== nextIsHidden && newProps.mode !== 'unstable-defer-without-hiding' ) { - workInProgress.flags |= Update; + workInProgress.flags |= Update | Visibility; } } + + // Don't bubble properties for hidden children. + if ( + !nextIsHidden || + includesSomeLane(subtreeRenderLanes, (OffscreenLane: Lane)) || + (workInProgress.mode & ConcurrentMode) === NoMode + ) { + bubbleProperties(workInProgress); + } + return null; } } diff --git a/packages/react-reconciler/src/ReactFiberHooks.old.js b/packages/react-reconciler/src/ReactFiberHooks.old.js index ccd69abae6aab..307cc8080cb04 100644 --- a/packages/react-reconciler/src/ReactFiberHooks.old.js +++ b/packages/react-reconciler/src/ReactFiberHooks.old.js @@ -26,10 +26,16 @@ import { enableSchedulingProfiler, enableNewReconciler, decoupleUpdatePriorityFromScheduler, + enableDoubleInvokingEffects, enableUseRefAccessWarning, } from 'shared/ReactFeatureFlags'; -import {NoMode, BlockingMode, DebugTracingMode} from './ReactTypeOfMode'; +import { + NoMode, + BlockingMode, + ConcurrentMode, + DebugTracingMode, +} from './ReactTypeOfMode'; import { NoLane, NoLanes, @@ -48,6 +54,9 @@ import {readContext} from './ReactFiberNewContext.old'; import { Update as UpdateEffect, Passive as PassiveEffect, + PassiveStatic as PassiveStaticEffect, + MountLayoutDev as MountLayoutDevEffect, + MountPassiveDev as MountPassiveDevEffect, } from './ReactFiberFlags'; import { HasEffect as HookHasEffect, @@ -482,7 +491,20 @@ export function bailoutHooks( lanes: Lanes, ) { workInProgress.updateQueue = current.updateQueue; - workInProgress.flags &= ~(PassiveEffect | UpdateEffect); + if ( + __DEV__ && + enableDoubleInvokingEffects && + (workInProgress.mode & (BlockingMode | ConcurrentMode)) !== NoMode + ) { + workInProgress.flags &= ~( + MountPassiveDevEffect | + PassiveEffect | + MountLayoutDevEffect | + UpdateEffect + ); + } else { + workInProgress.flags &= ~(PassiveEffect | UpdateEffect); + } current.lanes = removeLanes(current.lanes, lanes); } @@ -1314,16 +1336,30 @@ function mountEffect( ): void { if (__DEV__) { // $FlowExpectedError - jest isn't a global, and isn't recognized outside of tests - if ('undefined' !== typeof jest) { + if (typeof jest !== 'undefined') { warnIfNotCurrentlyActingEffectsInDEV(currentlyRenderingFiber); } } - return mountEffectImpl( - UpdateEffect | PassiveEffect, - HookPassive, - create, - deps, - ); + + if ( + __DEV__ && + enableDoubleInvokingEffects && + (currentlyRenderingFiber.mode & (BlockingMode | ConcurrentMode)) !== NoMode + ) { + return mountEffectImpl( + MountPassiveDevEffect | PassiveEffect | PassiveStaticEffect, + HookPassive, + create, + deps, + ); + } else { + return mountEffectImpl( + PassiveEffect | PassiveStaticEffect, + HookPassive, + create, + deps, + ); + } } function updateEffect( @@ -1332,23 +1368,31 @@ function updateEffect( ): void { if (__DEV__) { // $FlowExpectedError - jest isn't a global, and isn't recognized outside of tests - if ('undefined' !== typeof jest) { + if (typeof jest !== 'undefined') { warnIfNotCurrentlyActingEffectsInDEV(currentlyRenderingFiber); } } - return updateEffectImpl( - UpdateEffect | PassiveEffect, - HookPassive, - create, - deps, - ); + return updateEffectImpl(PassiveEffect, HookPassive, create, deps); } function mountLayoutEffect( create: () => (() => void) | void, deps: Array | void | null, ): void { - return mountEffectImpl(UpdateEffect, HookLayout, create, deps); + if ( + __DEV__ && + enableDoubleInvokingEffects && + (currentlyRenderingFiber.mode & (BlockingMode | ConcurrentMode)) !== NoMode + ) { + return mountEffectImpl( + MountLayoutDevEffect | UpdateEffect, + HookLayout, + create, + deps, + ); + } else { + return mountEffectImpl(UpdateEffect, HookLayout, create, deps); + } } function updateLayoutEffect( @@ -1407,12 +1451,25 @@ function mountImperativeHandle( const effectDeps = deps !== null && deps !== undefined ? deps.concat([ref]) : null; - return mountEffectImpl( - UpdateEffect, - HookLayout, - imperativeHandleEffect.bind(null, create, ref), - effectDeps, - ); + if ( + __DEV__ && + enableDoubleInvokingEffects && + (currentlyRenderingFiber.mode & (BlockingMode | ConcurrentMode)) !== NoMode + ) { + return mountEffectImpl( + MountLayoutDevEffect | UpdateEffect, + HookLayout, + imperativeHandleEffect.bind(null, create, ref), + effectDeps, + ); + } else { + return mountEffectImpl( + UpdateEffect, + HookLayout, + imperativeHandleEffect.bind(null, create, ref), + effectDeps, + ); + } } function updateImperativeHandle( @@ -1693,7 +1750,12 @@ function mountOpaqueIdentifier(): OpaqueIDType | void { const setId = mountState(id)[1]; if ((currentlyRenderingFiber.mode & BlockingMode) === NoMode) { - currentlyRenderingFiber.flags |= UpdateEffect | PassiveEffect; + if (__DEV__ && enableDoubleInvokingEffects) { + currentlyRenderingFiber.flags |= + MountPassiveDevEffect | PassiveEffect | PassiveStaticEffect; + } else { + currentlyRenderingFiber.flags |= PassiveEffect | PassiveStaticEffect; + } pushEffect( HookHasEffect | HookPassive, () => { @@ -1809,7 +1871,7 @@ function dispatchAction( } if (__DEV__) { // $FlowExpectedError - jest isn't a global, and isn't recognized outside of tests - if ('undefined' !== typeof jest) { + if (typeof jest !== 'undefined') { warnIfNotScopedWithMatchingAct(fiber); warnIfNotCurrentlyActingUpdatesInDev(fiber); } diff --git a/packages/react-reconciler/src/ReactFiberHydrationContext.old.js b/packages/react-reconciler/src/ReactFiberHydrationContext.old.js index 7576bd5ac0811..42701438b56ca 100644 --- a/packages/react-reconciler/src/ReactFiberHydrationContext.old.js +++ b/packages/react-reconciler/src/ReactFiberHydrationContext.old.js @@ -24,7 +24,7 @@ import { HostRoot, SuspenseComponent, } from './ReactWorkTags'; -import {Deletion, Placement, Hydrating} from './ReactFiberFlags'; +import {Deletion, Hydrating, Placement} from './ReactFiberFlags'; import invariant from 'shared/invariant'; import { @@ -124,18 +124,14 @@ function deleteHydratableInstance( const childToDelete = createFiberFromHostInstanceForDeletion(); childToDelete.stateNode = instance; childToDelete.return = returnFiber; - childToDelete.flags = Deletion; - // This might seem like it belongs on progressedFirstDeletion. However, - // these children are not part of the reconciliation list of children. - // Even if we abort and rereconcile the children, that will try to hydrate - // again and the nodes are still in the host tree so these will be - // recreated. - if (returnFiber.lastEffect !== null) { - returnFiber.lastEffect.nextEffect = childToDelete; - returnFiber.lastEffect = childToDelete; + const deletions = returnFiber.deletions; + if (deletions === null) { + returnFiber.deletions = [childToDelete]; + // TODO (effects) Rename this to better reflect its new usage (e.g. ChildDeletions) + returnFiber.flags |= Deletion; } else { - returnFiber.firstEffect = returnFiber.lastEffect = childToDelete; + deletions.push(childToDelete); } } diff --git a/packages/react-reconciler/src/ReactFiberSuspenseComponent.old.js b/packages/react-reconciler/src/ReactFiberSuspenseComponent.old.js index 6e14aacd77def..385a2ebdfa67d 100644 --- a/packages/react-reconciler/src/ReactFiberSuspenseComponent.old.js +++ b/packages/react-reconciler/src/ReactFiberSuspenseComponent.old.js @@ -60,9 +60,6 @@ export type SuspenseListRenderState = {| tail: null | Fiber, // Tail insertions setting. tailMode: SuspenseListTailMode, - // Last Effect before we rendered the "rendering" item. - // Used to remove new effects added by the rendered item. - lastEffect: null | Fiber, |}; export function shouldCaptureSuspense( diff --git a/packages/react-reconciler/src/ReactFiberThrow.old.js b/packages/react-reconciler/src/ReactFiberThrow.old.js index 8d338d552d7d6..64b1856d97aaf 100644 --- a/packages/react-reconciler/src/ReactFiberThrow.old.js +++ b/packages/react-reconciler/src/ReactFiberThrow.old.js @@ -185,8 +185,6 @@ function throwException( ) { // The source fiber did not complete. sourceFiber.flags |= Incomplete; - // Its effect list is no longer valid. - sourceFiber.firstEffect = sourceFiber.lastEffect = null; if ( value !== null && diff --git a/packages/react-reconciler/src/ReactFiberWorkLoop.old.js b/packages/react-reconciler/src/ReactFiberWorkLoop.old.js index cece268e3d017..b2bc6cde40e4d 100644 --- a/packages/react-reconciler/src/ReactFiberWorkLoop.old.js +++ b/packages/react-reconciler/src/ReactFiberWorkLoop.old.js @@ -13,22 +13,22 @@ import type {Lanes, Lane} from './ReactFiberLane'; import type {ReactPriorityLevel} from './ReactInternalTypes'; import type {Interaction} from 'scheduler/src/Tracing'; import type {SuspenseState} from './ReactFiberSuspenseComponent.old'; -import type {Effect as HookEffect} from './ReactFiberHooks.old'; import type {StackCursor} from './ReactFiberStack.old'; +import type {FunctionComponentUpdateQueue} from './ReactFiberHooks.old'; import { warnAboutDeprecatedLifecycles, enableSuspenseServerRenderer, replayFailedUnitOfWorkWithInvokeGuardedCallback, enableProfilerTimer, - enableProfilerCommitHooks, enableSchedulerTracing, warnAboutUnmockedScheduler, deferRenderPhaseUpdateToNextBatch, decoupleUpdatePriorityFromScheduler, enableDebugTracing, enableSchedulingProfiler, - enableScopeAPI, + skipUnmountedBoundaries, + enableDoubleInvokingEffects, } from 'shared/ReactFeatureFlags'; import ReactSharedInternals from 'shared/ReactSharedInternals'; import invariant from 'shared/invariant'; @@ -48,6 +48,10 @@ import { flushSyncCallbackQueue, scheduleSyncCallback, } from './SchedulerWithReactIntegration.old'; +import { + NoFlags as NoHookEffect, + Passive as HookPassive, +} from './ReactHookEffectTags'; import { logCommitStarted, logCommitStopped, @@ -76,13 +80,11 @@ import * as Scheduler from 'scheduler'; import {__interactionsRef, __subscriberRef} from 'scheduler/tracing'; import { - prepareForCommit, resetAfterCommit, scheduleTimeout, cancelTimeout, noTimeout, warnsIfNotActing, - beforeActiveInstanceBlur, afterActiveInstanceBlur, clearContainer, } from './ReactFiberHostConfig'; @@ -109,28 +111,19 @@ import { MemoComponent, SimpleMemoComponent, Block, - OffscreenComponent, - LegacyHiddenComponent, - ScopeComponent, } from './ReactWorkTags'; import {LegacyRoot} from './ReactRootTags'; import { NoFlags, - PerformedWork, Placement, - Update, - PlacementAndUpdate, - Deletion, - Ref, - ContentReset, - Snapshot, - Callback, - Passive, - PassiveUnmountPendingDev, + PassiveStatic, Incomplete, HostEffectMask, Hydrating, - HydratingAndUpdate, + BeforeMutationMask, + MutationMask, + LayoutMask, + PassiveMask, } from './ReactFiberFlags'; import { NoLanePriority, @@ -142,7 +135,6 @@ import { NoLane, SyncLane, SyncBatchedLane, - OffscreenLane, NoTimestamp, findUpdateLane, findTransitionLane, @@ -182,16 +174,12 @@ import { createClassErrorUpdate, } from './ReactFiberThrow.old'; import { - commitBeforeMutationLifeCycles as commitBeforeMutationEffectOnFiber, - commitLifeCycles as commitLayoutEffectOnFiber, - commitPlacement, - commitWork, - commitDeletion, - commitDetachRef, - commitAttachRef, - commitPassiveEffectDurations, - commitResetTextContent, - isSuspenseBoundaryBeingHidden, + commitBeforeMutationEffects, + commitMutationEffects, + commitLayoutEffects, + commitPassiveMountEffects, + commitPassiveUnmountEffects, + commitDoubleInvokeEffectsInDEV, } from './ReactFiberCommitWork.old'; import {enqueueUpdate} from './ReactUpdateQueue.old'; import {resetContextDependencies} from './ReactFiberNewContext.old'; @@ -209,8 +197,6 @@ import { import { recordCommitTime, - recordPassiveEffectDuration, - startPassiveEffectTimer, startProfilerTimer, stopProfilerTimerIfRunningAndRecordDelta, } from './ReactProfilerTimer.old'; @@ -234,7 +220,6 @@ import {onCommitRoot as onCommitRootTestSelector} from './ReactTestSelectors'; // Used by `act` import enqueueTask from 'shared/enqueueTask'; -import {doesFiberContain} from './ReactFiberTreeReflection'; const ceil = Math.ceil; @@ -280,7 +265,7 @@ let workInProgressRootRenderLanes: Lanes = NoLanes; // // Most things in the work loop should deal with workInProgressRootRenderLanes. // Most things in begin/complete phases should deal with subtreeRenderLanes. -let subtreeRenderLanes: Lanes = NoLanes; +export let subtreeRenderLanes: Lanes = NoLanes; const subtreeRenderLanesCursor: StackCursor = createCursor(NoLanes); // Whether to root completed, errored, suspended, etc. @@ -322,18 +307,13 @@ export function getRenderTargetTime(): number { return workInProgressRootRenderTargetTime; } -let nextEffect: Fiber | null = null; let hasUncaughtError = false; let firstUncaughtError = null; let legacyErrorBoundariesThatAlreadyFailed: Set | null = null; -let rootDoesHavePassiveEffects: boolean = false; let rootWithPendingPassiveEffects: FiberRoot | null = null; let pendingPassiveEffectsRenderPriority: ReactPriorityLevel = NoSchedulerPriority; let pendingPassiveEffectsLanes: Lanes = NoLanes; -let pendingPassiveHookEffectsMount: Array = []; -let pendingPassiveHookEffectsUnmount: Array = []; -let pendingPassiveProfilerEffects: Array = []; let rootsWithPendingDiscreteUpdates: Set | null = null; @@ -363,9 +343,6 @@ let currentEventPendingLanes: Lanes = NoLanes; // We warn about state updates for unmounted components differently in this case. let isFlushingPassiveEffects = false; -let focusedInstanceHandle: null | Fiber = null; -let shouldFireAfterActiveInstanceBlur: boolean = false; - export function getWorkInProgressRoot(): FiberRoot | null { return workInProgressRoot; } @@ -1700,47 +1677,6 @@ function completeUnitOfWork(unitOfWork: Fiber): void { workInProgress = next; return; } - - resetChildLanes(completedWork); - - if ( - returnFiber !== null && - // Do not append effects to parents if a sibling failed to complete - (returnFiber.flags & Incomplete) === NoFlags - ) { - // Append all the effects of the subtree and this fiber onto the effect - // list of the parent. The completion order of the children affects the - // side-effect order. - if (returnFiber.firstEffect === null) { - returnFiber.firstEffect = completedWork.firstEffect; - } - if (completedWork.lastEffect !== null) { - if (returnFiber.lastEffect !== null) { - returnFiber.lastEffect.nextEffect = completedWork.firstEffect; - } - returnFiber.lastEffect = completedWork.lastEffect; - } - - // If this fiber had side-effects, we append it AFTER the children's - // side-effects. We can perform certain side-effects earlier if needed, - // by doing multiple passes over the effect list. We don't want to - // schedule our own side-effect on our own list because if end up - // reusing children we'll schedule this effect onto itself since we're - // at the end. - const flags = completedWork.flags; - - // Skip both NoWork and PerformedWork tags when creating the effect - // list. PerformedWork effect is read by React DevTools but shouldn't be - // committed. - if (flags > PerformedWork) { - if (returnFiber.lastEffect !== null) { - returnFiber.lastEffect.nextEffect = completedWork; - } else { - returnFiber.firstEffect = completedWork; - } - returnFiber.lastEffect = completedWork; - } - } } else { // This fiber did not complete because something threw. Pop values off // the stack without entering the complete phase. If this is a boundary, @@ -1777,9 +1713,10 @@ function completeUnitOfWork(unitOfWork: Fiber): void { } if (returnFiber !== null) { - // Mark the parent fiber as incomplete and clear its effect list. - returnFiber.firstEffect = returnFiber.lastEffect = null; + // Mark the parent fiber as incomplete returnFiber.flags |= Incomplete; + returnFiber.subtreeFlags = NoFlags; + returnFiber.deletions = null; } } @@ -1801,81 +1738,6 @@ function completeUnitOfWork(unitOfWork: Fiber): void { } } -function resetChildLanes(completedWork: Fiber) { - if ( - // TODO: Move this check out of the hot path by moving `resetChildLanes` - // to switch statement in `completeWork`. - (completedWork.tag === LegacyHiddenComponent || - completedWork.tag === OffscreenComponent) && - completedWork.memoizedState !== null && - !includesSomeLane(subtreeRenderLanes, (OffscreenLane: Lane)) && - (completedWork.mode & ConcurrentMode) !== NoLanes - ) { - // The children of this component are hidden. Don't bubble their - // expiration times. - return; - } - - let newChildLanes = NoLanes; - - // Bubble up the earliest expiration time. - if (enableProfilerTimer && (completedWork.mode & ProfileMode) !== NoMode) { - // In profiling mode, resetChildExpirationTime is also used to reset - // profiler durations. - let actualDuration = completedWork.actualDuration; - let treeBaseDuration = ((completedWork.selfBaseDuration: any): number); - - // When a fiber is cloned, its actualDuration is reset to 0. This value will - // only be updated if work is done on the fiber (i.e. it doesn't bailout). - // When work is done, it should bubble to the parent's actualDuration. If - // the fiber has not been cloned though, (meaning no work was done), then - // this value will reflect the amount of time spent working on a previous - // render. In that case it should not bubble. We determine whether it was - // cloned by comparing the child pointer. - const shouldBubbleActualDurations = - completedWork.alternate === null || - completedWork.child !== completedWork.alternate.child; - - let child = completedWork.child; - while (child !== null) { - newChildLanes = mergeLanes( - newChildLanes, - mergeLanes(child.lanes, child.childLanes), - ); - if (shouldBubbleActualDurations) { - actualDuration += child.actualDuration; - } - treeBaseDuration += child.treeBaseDuration; - child = child.sibling; - } - - const isTimedOutSuspense = - completedWork.tag === SuspenseComponent && - completedWork.memoizedState !== null; - if (isTimedOutSuspense) { - // Don't count time spent in a timed out Suspense subtree as part of the base duration. - const primaryChildFragment = completedWork.child; - if (primaryChildFragment !== null) { - treeBaseDuration -= ((primaryChildFragment.treeBaseDuration: any): number); - } - } - - completedWork.actualDuration = actualDuration; - completedWork.treeBaseDuration = treeBaseDuration; - } else { - let child = completedWork.child; - while (child !== null) { - newChildLanes = mergeLanes( - newChildLanes, - mergeLanes(child.lanes, child.childLanes), - ); - child = child.sibling; - } - } - - completedWork.childLanes = newChildLanes; -} - function commitRoot(root) { const renderPriorityLevel = getCurrentPriorityLevel(); runWithPriority( @@ -1969,25 +1831,37 @@ function commitRootImpl(root, renderPriorityLevel) { // times out. } - // Get the list of effects. - let firstEffect; - if (finishedWork.flags > PerformedWork) { - // A fiber's effect list consists only of its children, not itself. So if - // the root has an effect, we need to add it to the end of the list. The - // resulting list is the set that would belong to the root's parent, if it - // had one; that is, all the effects in the tree including the root. - if (finishedWork.lastEffect !== null) { - finishedWork.lastEffect.nextEffect = finishedWork; - firstEffect = finishedWork.firstEffect; - } else { - firstEffect = finishedWork; - } - } else { - // There is no effect on the root. - firstEffect = finishedWork.firstEffect; + // If there are pending passive effects, schedule a callback to process them. + // Do this as early as possible, so it is queued before anything else that + // might get scheduled in the commit phase. (See #16714.) + const rootDoesHavePassiveEffects = + (finishedWork.subtreeFlags & PassiveMask) !== NoFlags || + (finishedWork.flags & PassiveMask) !== NoFlags; + if (rootDoesHavePassiveEffects) { + rootWithPendingPassiveEffects = root; + pendingPassiveEffectsLanes = lanes; + pendingPassiveEffectsRenderPriority = renderPriorityLevel; + scheduleCallback(NormalSchedulerPriority, () => { + flushPassiveEffects(); + return null; + }); } - if (firstEffect !== null) { + // Check if there are any effects in the whole tree. + // TODO: This is left over from the effect list implementation, where we had + // to check for the existence of `firstEffect` to satsify Flow. I think the + // only other reason this optimization exists is because it affects profiling. + // Reconsider whether this is necessary. + const subtreeHasEffects = + (finishedWork.subtreeFlags & + (BeforeMutationMask | MutationMask | LayoutMask | PassiveMask)) !== + NoFlags; + const rootHasEffect = + (finishedWork.flags & + (BeforeMutationMask | MutationMask | LayoutMask | PassiveMask)) !== + NoFlags; + + if (subtreeHasEffects || rootHasEffect) { let previousLanePriority; if (decoupleUpdatePriorityFromScheduler) { previousLanePriority = getCurrentUpdateLanePriority(); @@ -2008,32 +1882,10 @@ function commitRootImpl(root, renderPriorityLevel) { // The first phase a "before mutation" phase. We use this phase to read the // state of the host tree right before we mutate it. This is where // getSnapshotBeforeUpdate is called. - focusedInstanceHandle = prepareForCommit(root.containerInfo); - shouldFireAfterActiveInstanceBlur = false; - - nextEffect = firstEffect; - do { - if (__DEV__) { - invokeGuardedCallback(null, commitBeforeMutationEffects, null); - if (hasCaughtError()) { - invariant(nextEffect !== null, 'Should be working on an effect.'); - const error = clearCaughtError(); - captureCommitPhaseError(nextEffect, error); - nextEffect = nextEffect.nextEffect; - } - } else { - try { - commitBeforeMutationEffects(); - } catch (error) { - invariant(nextEffect !== null, 'Should be working on an effect.'); - captureCommitPhaseError(nextEffect, error); - nextEffect = nextEffect.nextEffect; - } - } - } while (nextEffect !== null); - - // We no longer need to track the active instance fiber - focusedInstanceHandle = null; + const shouldFireAfterActiveInstanceBlur = commitBeforeMutationEffects( + root, + finishedWork, + ); if (enableProfilerTimer) { // Mark the current commit time to be shared by all Profilers in this @@ -2042,32 +1894,7 @@ function commitRootImpl(root, renderPriorityLevel) { } // The next phase is the mutation phase, where we mutate the host tree. - nextEffect = firstEffect; - do { - if (__DEV__) { - invokeGuardedCallback( - null, - commitMutationEffects, - null, - root, - renderPriorityLevel, - ); - if (hasCaughtError()) { - invariant(nextEffect !== null, 'Should be working on an effect.'); - const error = clearCaughtError(); - captureCommitPhaseError(nextEffect, error); - nextEffect = nextEffect.nextEffect; - } - } else { - try { - commitMutationEffects(root, renderPriorityLevel); - } catch (error) { - invariant(nextEffect !== null, 'Should be working on an effect.'); - captureCommitPhaseError(nextEffect, error); - nextEffect = nextEffect.nextEffect; - } - } - } while (nextEffect !== null); + commitMutationEffects(finishedWork, root, renderPriorityLevel); if (shouldFireAfterActiveInstanceBlur) { afterActiveInstanceBlur(); @@ -2083,28 +1910,26 @@ function commitRootImpl(root, renderPriorityLevel) { // The next phase is the layout phase, where we call effects that read // the host tree after it's been mutated. The idiomatic use case for this is // layout, but class component lifecycles also fire here for legacy reasons. - nextEffect = firstEffect; - do { - if (__DEV__) { - invokeGuardedCallback(null, commitLayoutEffects, null, root, lanes); - if (hasCaughtError()) { - invariant(nextEffect !== null, 'Should be working on an effect.'); - const error = clearCaughtError(); - captureCommitPhaseError(nextEffect, error); - nextEffect = nextEffect.nextEffect; - } - } else { - try { - commitLayoutEffects(root, lanes); - } catch (error) { - invariant(nextEffect !== null, 'Should be working on an effect.'); - captureCommitPhaseError(nextEffect, error); - nextEffect = nextEffect.nextEffect; - } + + if (__DEV__) { + if (enableDebugTracing) { + logLayoutEffectsStarted(lanes); } - } while (nextEffect !== null); + } + if (enableSchedulingProfiler) { + markLayoutEffectsStarted(lanes); + } - nextEffect = null; + commitLayoutEffects(finishedWork, root); + + if (__DEV__) { + if (enableDebugTracing) { + logLayoutEffectsStopped(); + } + } + if (enableSchedulingProfiler) { + markLayoutEffectsStopped(); + } // Tell Scheduler to yield at the end of the frame, so the browser has an // opportunity to paint. @@ -2130,30 +1955,6 @@ function commitRootImpl(root, renderPriorityLevel) { } } - const rootDidHavePassiveEffects = rootDoesHavePassiveEffects; - - if (rootDoesHavePassiveEffects) { - // This commit has passive effects. Stash a reference to them. But don't - // schedule a callback until after flushing layout work. - rootDoesHavePassiveEffects = false; - rootWithPendingPassiveEffects = root; - pendingPassiveEffectsLanes = lanes; - pendingPassiveEffectsRenderPriority = renderPriorityLevel; - } else { - // We are done with the effect chain at this point so let's clear the - // nextEffect pointers to assist with GC. If we have passive effects, we'll - // clear this in flushPassiveEffects. - nextEffect = firstEffect; - while (nextEffect !== null) { - const nextNextEffect = nextEffect.nextEffect; - nextEffect.nextEffect = null; - if (nextEffect.flags & Deletion) { - detachFiberAfterEffects(nextEffect); - } - nextEffect = nextNextEffect; - } - } - // Read this again, since an effect might have updated it remainingLanes = root.pendingLanes; @@ -2179,8 +1980,14 @@ function commitRootImpl(root, renderPriorityLevel) { legacyErrorBoundariesThatAlreadyFailed = null; } + if (__DEV__ && enableDoubleInvokingEffects) { + if (!rootDoesHavePassiveEffects) { + commitDoubleInvokeEffectsInDEV(root.current, false); + } + } + if (enableSchedulerTracing) { - if (!rootDidHavePassiveEffects) { + if (!rootDoesHavePassiveEffects) { // If there are no passive effects, then we can complete the pending interactions. // Otherwise, we'll wait until after the passive effects are flushed. // Wait to do this until after remaining work has been scheduled, @@ -2253,184 +2060,6 @@ function commitRootImpl(root, renderPriorityLevel) { return null; } -function commitBeforeMutationEffects() { - while (nextEffect !== null) { - const current = nextEffect.alternate; - - if (!shouldFireAfterActiveInstanceBlur && focusedInstanceHandle !== null) { - if ((nextEffect.flags & Deletion) !== NoFlags) { - if (doesFiberContain(nextEffect, focusedInstanceHandle)) { - shouldFireAfterActiveInstanceBlur = true; - beforeActiveInstanceBlur(nextEffect); - } - } else { - // TODO: Move this out of the hot path using a dedicated effect tag. - if ( - nextEffect.tag === SuspenseComponent && - isSuspenseBoundaryBeingHidden(current, nextEffect) && - doesFiberContain(nextEffect, focusedInstanceHandle) - ) { - shouldFireAfterActiveInstanceBlur = true; - beforeActiveInstanceBlur(nextEffect); - } - } - } - - const flags = nextEffect.flags; - if ((flags & Snapshot) !== NoFlags) { - setCurrentDebugFiberInDEV(nextEffect); - - commitBeforeMutationEffectOnFiber(current, nextEffect); - - resetCurrentDebugFiberInDEV(); - } - if ((flags & Passive) !== NoFlags) { - // If there are passive effects, schedule a callback to flush at - // the earliest opportunity. - if (!rootDoesHavePassiveEffects) { - rootDoesHavePassiveEffects = true; - scheduleCallback(NormalSchedulerPriority, () => { - flushPassiveEffects(); - return null; - }); - } - } - nextEffect = nextEffect.nextEffect; - } -} - -function commitMutationEffects( - root: FiberRoot, - renderPriorityLevel: ReactPriorityLevel, -) { - // TODO: Should probably move the bulk of this function to commitWork. - while (nextEffect !== null) { - setCurrentDebugFiberInDEV(nextEffect); - - const flags = nextEffect.flags; - - if (flags & ContentReset) { - commitResetTextContent(nextEffect); - } - - if (flags & Ref) { - const current = nextEffect.alternate; - if (current !== null) { - commitDetachRef(current); - } - if (enableScopeAPI) { - // TODO: This is a temporary solution that allowed us to transition away - // from React Flare on www. - if (nextEffect.tag === ScopeComponent) { - commitAttachRef(nextEffect); - } - } - } - - // The following switch statement is only concerned about placement, - // updates, and deletions. To avoid needing to add a case for every possible - // bitmap value, we remove the secondary effects from the effect tag and - // switch on that value. - const primaryFlags = flags & (Placement | Update | Deletion | Hydrating); - switch (primaryFlags) { - case Placement: { - commitPlacement(nextEffect); - // 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. - nextEffect.flags &= ~Placement; - break; - } - case PlacementAndUpdate: { - // Placement - commitPlacement(nextEffect); - // Clear the "placement" from effect tag so that we know that this is - // inserted, before any life-cycles like componentDidMount gets called. - nextEffect.flags &= ~Placement; - - // Update - const current = nextEffect.alternate; - commitWork(current, nextEffect); - break; - } - case Hydrating: { - nextEffect.flags &= ~Hydrating; - break; - } - case HydratingAndUpdate: { - nextEffect.flags &= ~Hydrating; - - // Update - const current = nextEffect.alternate; - commitWork(current, nextEffect); - break; - } - case Update: { - const current = nextEffect.alternate; - commitWork(current, nextEffect); - break; - } - case Deletion: { - commitDeletion(root, nextEffect, renderPriorityLevel); - break; - } - } - - resetCurrentDebugFiberInDEV(); - nextEffect = nextEffect.nextEffect; - } -} - -function commitLayoutEffects(root: FiberRoot, committedLanes: Lanes) { - if (__DEV__) { - if (enableDebugTracing) { - logLayoutEffectsStarted(committedLanes); - } - } - - if (enableSchedulingProfiler) { - markLayoutEffectsStarted(committedLanes); - } - - // TODO: Should probably move the bulk of this function to commitWork. - while (nextEffect !== null) { - setCurrentDebugFiberInDEV(nextEffect); - - const flags = nextEffect.flags; - - if (flags & (Update | Callback)) { - const current = nextEffect.alternate; - commitLayoutEffectOnFiber(root, current, nextEffect, committedLanes); - } - - if (enableScopeAPI) { - // TODO: This is a temporary solution that allowed us to transition away - // from React Flare on www. - if (flags & Ref && nextEffect.tag !== ScopeComponent) { - commitAttachRef(nextEffect); - } - } else { - if (flags & Ref) { - commitAttachRef(nextEffect); - } - } - - resetCurrentDebugFiberInDEV(); - nextEffect = nextEffect.nextEffect; - } - - if (__DEV__) { - if (enableDebugTracing) { - logLayoutEffectsStopped(); - } - } - - if (enableSchedulingProfiler) { - markLayoutEffectsStopped(); - } -} - export function flushPassiveEffects(): boolean { // Returns whether passive effects were flushed. if (pendingPassiveEffectsRenderPriority !== NoSchedulerPriority) { @@ -2456,59 +2085,6 @@ export function flushPassiveEffects(): boolean { return false; } -export function enqueuePendingPassiveProfilerEffect(fiber: Fiber): void { - if (enableProfilerTimer && enableProfilerCommitHooks) { - pendingPassiveProfilerEffects.push(fiber); - if (!rootDoesHavePassiveEffects) { - rootDoesHavePassiveEffects = true; - scheduleCallback(NormalSchedulerPriority, () => { - flushPassiveEffects(); - return null; - }); - } - } -} - -export function enqueuePendingPassiveHookEffectMount( - fiber: Fiber, - effect: HookEffect, -): void { - pendingPassiveHookEffectsMount.push(effect, fiber); - if (!rootDoesHavePassiveEffects) { - rootDoesHavePassiveEffects = true; - scheduleCallback(NormalSchedulerPriority, () => { - flushPassiveEffects(); - return null; - }); - } -} - -export function enqueuePendingPassiveHookEffectUnmount( - fiber: Fiber, - effect: HookEffect, -): void { - pendingPassiveHookEffectsUnmount.push(effect, fiber); - if (__DEV__) { - fiber.flags |= PassiveUnmountPendingDev; - const alternate = fiber.alternate; - if (alternate !== null) { - alternate.flags |= PassiveUnmountPendingDev; - } - } - if (!rootDoesHavePassiveEffects) { - rootDoesHavePassiveEffects = true; - scheduleCallback(NormalSchedulerPriority, () => { - flushPassiveEffects(); - return null; - }); - } -} - -function invokePassiveEffectCreate(effect: HookEffect): void { - const create = effect.create; - effect.destroy = create(); -} - function flushPassiveEffectsImpl() { if (rootWithPendingPassiveEffects === null) { return false; @@ -2548,156 +2124,30 @@ function flushPassiveEffectsImpl() { // e.g. a destroy function in one component may unintentionally override a ref // value set by a create function in another component. // Layout effects have the same constraint. + commitPassiveUnmountEffects(root.current); + commitPassiveMountEffects(root, root.current); - // First pass: Destroy stale passive effects. - const unmountEffects = pendingPassiveHookEffectsUnmount; - pendingPassiveHookEffectsUnmount = []; - for (let i = 0; i < unmountEffects.length; i += 2) { - const effect = ((unmountEffects[i]: any): HookEffect); - const fiber = ((unmountEffects[i + 1]: any): Fiber); - const destroy = effect.destroy; - effect.destroy = undefined; - - if (__DEV__) { - fiber.flags &= ~PassiveUnmountPendingDev; - const alternate = fiber.alternate; - if (alternate !== null) { - alternate.flags &= ~PassiveUnmountPendingDev; - } - } - - if (typeof destroy === 'function') { - if (__DEV__) { - setCurrentDebugFiberInDEV(fiber); - if ( - enableProfilerTimer && - enableProfilerCommitHooks && - fiber.mode & ProfileMode - ) { - startPassiveEffectTimer(); - invokeGuardedCallback(null, destroy, null); - recordPassiveEffectDuration(fiber); - } else { - invokeGuardedCallback(null, destroy, null); - } - if (hasCaughtError()) { - invariant(fiber !== null, 'Should be working on an effect.'); - const error = clearCaughtError(); - captureCommitPhaseError(fiber, error); - } - resetCurrentDebugFiberInDEV(); - } else { - try { - if ( - enableProfilerTimer && - enableProfilerCommitHooks && - fiber.mode & ProfileMode - ) { - try { - startPassiveEffectTimer(); - destroy(); - } finally { - recordPassiveEffectDuration(fiber); - } - } else { - destroy(); - } - } catch (error) { - invariant(fiber !== null, 'Should be working on an effect.'); - captureCommitPhaseError(fiber, error); - } - } - } - } - // Second pass: Create new passive effects. - const mountEffects = pendingPassiveHookEffectsMount; - pendingPassiveHookEffectsMount = []; - for (let i = 0; i < mountEffects.length; i += 2) { - const effect = ((mountEffects[i]: any): HookEffect); - const fiber = ((mountEffects[i + 1]: any): Fiber); - if (__DEV__) { - setCurrentDebugFiberInDEV(fiber); - if ( - enableProfilerTimer && - enableProfilerCommitHooks && - fiber.mode & ProfileMode - ) { - startPassiveEffectTimer(); - invokeGuardedCallback(null, invokePassiveEffectCreate, null, effect); - recordPassiveEffectDuration(fiber); - } else { - invokeGuardedCallback(null, invokePassiveEffectCreate, null, effect); - } - if (hasCaughtError()) { - invariant(fiber !== null, 'Should be working on an effect.'); - const error = clearCaughtError(); - captureCommitPhaseError(fiber, error); - } - resetCurrentDebugFiberInDEV(); - } else { - try { - const create = effect.create; - if ( - enableProfilerTimer && - enableProfilerCommitHooks && - fiber.mode & ProfileMode - ) { - try { - startPassiveEffectTimer(); - effect.destroy = create(); - } finally { - recordPassiveEffectDuration(fiber); - } - } else { - effect.destroy = create(); - } - } catch (error) { - invariant(fiber !== null, 'Should be working on an effect.'); - captureCommitPhaseError(fiber, error); - } - } - } - - // Note: This currently assumes there are no passive effects on the root fiber - // because the root is not part of its own effect list. - // This could change in the future. - let effect = root.current.firstEffect; - while (effect !== null) { - const nextNextEffect = effect.nextEffect; - // Remove nextEffect pointer to assist GC - effect.nextEffect = null; - if (effect.flags & Deletion) { - detachFiberAfterEffects(effect); + if (__DEV__) { + if (enableDebugTracing) { + logPassiveEffectsStopped(); } - effect = nextNextEffect; } - if (enableProfilerTimer && enableProfilerCommitHooks) { - const profilerEffects = pendingPassiveProfilerEffects; - pendingPassiveProfilerEffects = []; - for (let i = 0; i < profilerEffects.length; i++) { - const fiber = ((profilerEffects[i]: any): Fiber); - commitPassiveEffectDurations(root, fiber); - } + if (enableSchedulingProfiler) { + markPassiveEffectsStopped(); } - if (enableSchedulerTracing) { - popInteractions(((prevInteractions: any): Set)); - finishPendingInteractions(root, lanes); + if (__DEV__ && enableDoubleInvokingEffects) { + commitDoubleInvokeEffectsInDEV(root.current, true); } if (__DEV__) { isFlushingPassiveEffects = false; } - if (__DEV__) { - if (enableDebugTracing) { - logPassiveEffectsStopped(); - } - } - - if (enableSchedulingProfiler) { - markPassiveEffectsStopped(); + if (enableSchedulerTracing) { + popInteractions(((prevInteractions: any): Set)); + finishPendingInteractions(root, lanes); } executionContext = prevExecutionContext; @@ -2752,7 +2202,11 @@ function captureCommitPhaseErrorOnRoot( } } -export function captureCommitPhaseError(sourceFiber: Fiber, error: mixed) { +export function captureCommitPhaseError( + sourceFiber: Fiber, + nearestMountedAncestor: Fiber | null, + error: mixed, +) { if (sourceFiber.tag === HostRoot) { // Error was thrown at the root. There is no parent, so the root // itself should capture it. @@ -2760,7 +2214,12 @@ export function captureCommitPhaseError(sourceFiber: Fiber, error: mixed) { return; } - let fiber = sourceFiber.return; + let fiber = null; + if (skipUnmountedBoundaries) { + fiber = nearestMountedAncestor; + } else { + fiber = sourceFiber.return; + } while (fiber !== null) { if (fiber.tag === HostRoot) { @@ -2787,24 +2246,6 @@ export function captureCommitPhaseError(sourceFiber: Fiber, error: mixed) { markRootUpdated(root, SyncLane, eventTime); ensureRootIsScheduled(root, eventTime); schedulePendingInteractions(root, SyncLane); - } else { - // This component has already been unmounted. - // We can't schedule any follow up work for the root because the fiber is already unmounted, - // but we can still call the log-only boundary so the error isn't swallowed. - // - // TODO This is only a temporary bandaid for the old reconciler fork. - // We can delete this special case once the new fork is merged. - if ( - typeof instance.componentDidCatch === 'function' && - !isAlreadyFailedLegacyErrorBoundary(instance) - ) { - try { - instance.componentDidCatch(error, errorInfo); - } catch (errorToIgnore) { - // TODO Ignore this error? Rethrow it? - // This is kind of an edge case. - } - } } return; } @@ -3061,10 +2502,24 @@ function warnAboutUpdateOnUnmountedFiberInDEV(fiber) { return; } - // If there are pending passive effects unmounts for this Fiber, - // we can assume that they would have prevented this update. - if ((fiber.flags & PassiveUnmountPendingDev) !== NoFlags) { - return; + if ((fiber.flags & PassiveStatic) !== NoFlags) { + const updateQueue: FunctionComponentUpdateQueue | null = (fiber.updateQueue: any); + if (updateQueue !== null) { + const lastEffect = updateQueue.lastEffect; + if (lastEffect !== null) { + const firstEffect = lastEffect.next; + + let effect = firstEffect; + do { + if (effect.destroy !== undefined) { + if ((effect.tag & HookPassive) !== NoHookEffect) { + return; + } + } + effect = effect.next; + } while (effect !== firstEffect); + } + } } // We show the whole stack but dedupe on the top component's name because @@ -3754,8 +3209,3 @@ export function act(callback: () => Thenable): Thenable { }; } } - -function detachFiberAfterEffects(fiber: Fiber): void { - fiber.sibling = null; - fiber.stateNode = null; -} diff --git a/packages/react-reconciler/src/ReactStrictModeWarnings.old.js b/packages/react-reconciler/src/ReactStrictModeWarnings.old.js index 5cb33579c7be5..5dd09a8cc80da 100644 --- a/packages/react-reconciler/src/ReactStrictModeWarnings.old.js +++ b/packages/react-reconciler/src/ReactStrictModeWarnings.old.js @@ -64,7 +64,7 @@ if (__DEV__) { fiber: Fiber, instance: any, ) => { - // Dedup strategy: Warn once per component. + // Dedupe strategy: Warn once per component. if (didWarnAboutUnsafeLifecycles.has(fiber.type)) { return; } diff --git a/packages/react-reconciler/src/SchedulerWithReactIntegration.old.js b/packages/react-reconciler/src/SchedulerWithReactIntegration.old.js index 606a90252077e..f73ae2f8998f4 100644 --- a/packages/react-reconciler/src/SchedulerWithReactIntegration.old.js +++ b/packages/react-reconciler/src/SchedulerWithReactIntegration.old.js @@ -165,13 +165,13 @@ export function cancelCallback(callbackNode: mixed) { } } -export function flushSyncCallbackQueue() { +export function flushSyncCallbackQueue(): boolean { if (immediateQueueCallbackNode !== null) { const node = immediateQueueCallbackNode; immediateQueueCallbackNode = null; Scheduler_cancelCallback(node); } - flushSyncCallbackQueueImpl(); + return flushSyncCallbackQueueImpl(); } function flushSyncCallbackQueueImpl() { @@ -237,5 +237,8 @@ function flushSyncCallbackQueueImpl() { isFlushingSyncQueue = false; } } + return true; + } else { + return false; } } diff --git a/packages/react-reconciler/src/__tests__/DebugTracing-test.internal.js b/packages/react-reconciler/src/__tests__/DebugTracing-test.internal.js index 2df04c8c88544..716a025ab7417 100644 --- a/packages/react-reconciler/src/__tests__/DebugTracing-test.internal.js +++ b/packages/react-reconciler/src/__tests__/DebugTracing-test.internal.js @@ -376,14 +376,14 @@ describe('DebugTracing', () => { expect(logs).toEqual([ 'group: ⚛️ render (0b0000000000000000000001000000000)', 'log: ⚛️ Example updated state (0b0000000000000000000001000000000)', - 'log: ⚛️ Example updated state (0b0000000000000000000001000000000)', // debugRenderPhaseSideEffectsForStrictMode + 'log: ⚛️ Example updated state (0b0000000000000000000001000000000)', 'groupEnd: ⚛️ render (0b0000000000000000000001000000000)', ]); } else { expect(logs).toEqual([ 'group: ⚛️ render (0b0000000000000000000001000000000)', 'log: ⚛️ Example updated state (0b0000000000000000000010000000000)', - 'log: ⚛️ Example updated state (0b0000000000000000000010000000000)', // debugRenderPhaseSideEffectsForStrictMode + 'log: ⚛️ Example updated state (0b0000000000000000000010000000000)', 'groupEnd: ⚛️ render (0b0000000000000000000001000000000)', ]); } diff --git a/packages/react-reconciler/src/__tests__/ReactHooksWithNoopRenderer-test.js b/packages/react-reconciler/src/__tests__/ReactHooksWithNoopRenderer-test.js index 79794bbea7ff4..9a325154f1e89 100644 --- a/packages/react-reconciler/src/__tests__/ReactHooksWithNoopRenderer-test.js +++ b/packages/react-reconciler/src/__tests__/ReactHooksWithNoopRenderer-test.js @@ -2356,7 +2356,6 @@ describe('ReactHooksWithNoopRenderer', () => { describe('errors thrown in passive destroy function within unmounted trees', () => { let BrokenUseEffectCleanup; let ErrorBoundary; - let DerivedStateOnlyErrorBoundary; let LogOnlyErrorBoundary; beforeEach(() => { @@ -2395,28 +2394,6 @@ describe('ReactHooksWithNoopRenderer', () => { } }; - DerivedStateOnlyErrorBoundary = class extends React.Component { - state = {error: null}; - static getDerivedStateFromError(error) { - Scheduler.unstable_yieldValue( - `DerivedStateOnlyErrorBoundary static getDerivedStateFromError`, - ); - return {error}; - } - render() { - if (this.state.error) { - Scheduler.unstable_yieldValue( - 'DerivedStateOnlyErrorBoundary render error', - ); - return ; - } - Scheduler.unstable_yieldValue( - 'DerivedStateOnlyErrorBoundary render success', - ); - return this.props.children || null; - } - }; - LogOnlyErrorBoundary = class extends React.Component { componentDidCatch(error, info) { Scheduler.unstable_yieldValue( @@ -2430,162 +2407,7 @@ describe('ReactHooksWithNoopRenderer', () => { }; }); - // @gate old - it('should call componentDidCatch() for the nearest unmounted log-only boundary', () => { - function Conditional({showChildren}) { - if (showChildren) { - return ( - - - - ); - } else { - return null; - } - } - - act(() => { - ReactNoop.render( - - - , - ); - }); - - expect(Scheduler).toHaveYielded([ - 'ErrorBoundary render success', - 'LogOnlyErrorBoundary render', - 'BrokenUseEffectCleanup useEffect', - ]); - - act(() => { - ReactNoop.render( - - - , - ); - expect(Scheduler).toFlushAndYieldThrough([ - 'ErrorBoundary render success', - ]); - }); - - expect(Scheduler).toHaveYielded([ - 'BrokenUseEffectCleanup useEffect destroy', - 'LogOnlyErrorBoundary componentDidCatch', - ]); - }); - - // @gate old - it('should call componentDidCatch() for the nearest unmounted logging-capable boundary', () => { - function Conditional({showChildren}) { - if (showChildren) { - return ( - - - - ); - } else { - return null; - } - } - - act(() => { - ReactNoop.render( - - - , - ); - }); - - expect(Scheduler).toHaveYielded([ - 'ErrorBoundary render success', - 'ErrorBoundary render success', - 'BrokenUseEffectCleanup useEffect', - ]); - - act(() => { - ReactNoop.render( - - - , - ); - expect(Scheduler).toFlushAndYieldThrough([ - 'ErrorBoundary render success', - ]); - }); - - expect(Scheduler).toHaveYielded([ - 'BrokenUseEffectCleanup useEffect destroy', - 'ErrorBoundary componentDidCatch', - ]); - }); - - // @gate old - it('should not call getDerivedStateFromError for unmounted error boundaries', () => { - function Conditional({showChildren}) { - if (showChildren) { - return ( - - - - ); - } else { - return null; - } - } - - act(() => { - ReactNoop.render(); - }); - - expect(Scheduler).toHaveYielded([ - 'ErrorBoundary render success', - 'BrokenUseEffectCleanup useEffect', - ]); - - act(() => { - ReactNoop.render(); - }); - - expect(Scheduler).toHaveYielded([ - 'BrokenUseEffectCleanup useEffect destroy', - 'ErrorBoundary componentDidCatch', - ]); - }); - - // @gate old - it('should not throw if there are no unmounted logging-capable boundaries to call', () => { - function Conditional({showChildren}) { - if (showChildren) { - return ( - - - - ); - } else { - return null; - } - } - - act(() => { - ReactNoop.render(); - }); - - expect(Scheduler).toHaveYielded([ - 'DerivedStateOnlyErrorBoundary render success', - 'BrokenUseEffectCleanup useEffect', - ]); - - act(() => { - ReactNoop.render(); - }); - - expect(Scheduler).toHaveYielded([ - 'BrokenUseEffectCleanup useEffect destroy', - ]); - }); - - // @gate new + // @gate skipUnmountedBoundaries it('should use the nearest still-mounted boundary if there are no unmounted boundaries', () => { act(() => { ReactNoop.render( @@ -2611,8 +2433,8 @@ describe('ReactHooksWithNoopRenderer', () => { ]); }); - // @gate new - it('should skip unmounted boundaries and use the nearest still-mounted boundary', () => { + // @gate skipUnmountedBoundaries + it('should skip unmounted boundaries and use the nearest still-mounted boundary', () => { function Conditional({showChildren}) { if (showChildren) { return ( @@ -2654,7 +2476,7 @@ describe('ReactHooksWithNoopRenderer', () => { ]); }); - // @gate new + // @gate skipUnmountedBoundaries it('should call getDerivedStateFromError in the nearest still-mounted boundary', () => { function Conditional({showChildren}) { if (showChildren) { @@ -2698,7 +2520,7 @@ describe('ReactHooksWithNoopRenderer', () => { ]); }); - // @gate new + // @gate skipUnmountedBoundaries it('should rethrow error if there are no still-mounted boundaries', () => { function Conditional({showChildren}) { if (showChildren) { diff --git a/packages/react/src/__tests__/ReactDOMTracing-test.internal.js b/packages/react/src/__tests__/ReactDOMTracing-test.internal.js index 7be6513a5737d..db062edd16f04 100644 --- a/packages/react/src/__tests__/ReactDOMTracing-test.internal.js +++ b/packages/react/src/__tests__/ReactDOMTracing-test.internal.js @@ -152,16 +152,7 @@ describe('ReactDOMTracing', () => { onInteractionScheduledWorkCompleted, ).toHaveBeenLastNotifiedOfInteraction(interaction); - if (gate(flags => flags.new)) { - expect(onRender).toHaveBeenCalledTimes(3); - } else { - // TODO: This is 4 instead of 3 because this update was scheduled at - // idle priority, and idle updates are slightly higher priority than - // offscreen work. So it takes two render passes to finish it. Profiler - // calls `onRender` for the first render even though everything - // bails out. - expect(onRender).toHaveBeenCalledTimes(4); - } + expect(onRender).toHaveBeenCalledTimes(3); expect(onRender).toHaveLastRenderedWithInteractions( new Set([interaction]), ); @@ -310,16 +301,7 @@ describe('ReactDOMTracing', () => { expect( onInteractionScheduledWorkCompleted, ).toHaveBeenLastNotifiedOfInteraction(interaction); - if (gate(flags => flags.new)) { - expect(onRender).toHaveBeenCalledTimes(3); - } else { - // TODO: This is 4 instead of 3 because this update was scheduled at - // idle priority, and idle updates are slightly higher priority than - // offscreen work. So it takes two render passes to finish it. Profiler - // calls `onRender` for the first render even though everything - // bails out. - expect(onRender).toHaveBeenCalledTimes(4); - } + expect(onRender).toHaveBeenCalledTimes(3); expect(onRender).toHaveLastRenderedWithInteractions( new Set([interaction]), ); diff --git a/packages/react/src/__tests__/ReactProfiler-test.internal.js b/packages/react/src/__tests__/ReactProfiler-test.internal.js index 73812de49a3b0..d0102395235cc 100644 --- a/packages/react/src/__tests__/ReactProfiler-test.internal.js +++ b/packages/react/src/__tests__/ReactProfiler-test.internal.js @@ -347,7 +347,7 @@ describe('Profiler', () => { expect(callback).toHaveBeenCalledTimes(1); - let call = callback.mock.calls[0]; + const call = callback.mock.calls[0]; expect(call).toHaveLength(enableSchedulerTracing ? 7 : 6); expect(call[0]).toBe('test'); @@ -364,31 +364,9 @@ describe('Profiler', () => { renderer.update(); - if (gate(flags => flags.new)) { - // None of the Profiler's subtree was rendered because App bailed out before the Profiler. - // So we expect onRender not to be called. - expect(callback).not.toHaveBeenCalled(); - } else { - // Updating a parent reports a re-render, - // since React technically did a little bit of work between the Profiler and the bailed out subtree. - // This is not optimal but it's how the old reconciler fork works. - expect(callback).toHaveBeenCalledTimes(1); - - call = callback.mock.calls[0]; - - expect(call).toHaveLength(enableSchedulerTracing ? 7 : 6); - expect(call[0]).toBe('test'); - expect(call[1]).toBe('update'); - expect(call[2]).toBe(0); // actual time - expect(call[3]).toBe(10); // base time - expect(call[4]).toBe(30); // start time - expect(call[5]).toBe(30); // commit time - expect(call[6]).toEqual( - enableSchedulerTracing ? new Set() : undefined, - ); // interaction events - - callback.mockReset(); - } + // None of the Profiler's subtree was rendered because App bailed out before the Profiler. + // So we expect onRender not to be called. + expect(callback).not.toHaveBeenCalled(); Scheduler.unstable_advanceTime(20); // 30 -> 50 @@ -3714,15 +3692,11 @@ describe('Profiler', () => { wrappedCascadingFn(); expect(Scheduler).toHaveYielded(['onPostCommit', 'render']); - // The new reconciler does not call onPostCommit again - // because the resolved suspended subtree doesn't contain any passive effects. - // If or its decendents had a passive effect, - // onPostCommit would be called again. - if (gate(flags => flags.new)) { - expect(Scheduler).toFlushAndYield([]); - } else { - expect(Scheduler).toFlushAndYield(['onPostCommit']); - } + // Does not call onPostCommit again because the resolved suspended + // subtree doesn't contain any passive effects. If + // or its decendents had a passive + // effect, onPostCommit would be called again. + expect(Scheduler).toFlushAndYield([]); expect(onInteractionScheduledWorkCompleted).toHaveBeenCalledTimes(1); expect( @@ -4209,7 +4183,6 @@ describe('Profiler', () => { }); if (__DEV__) { - // @gate new it('double invoking does not disconnect wrapped async work', () => { ReactFeatureFlags.enableDoubleInvokingEffects = true;