diff --git a/packages/react-dom/src/__tests__/ReactDOMServerPartialHydration-test.internal.js b/packages/react-dom/src/__tests__/ReactDOMServerPartialHydration-test.internal.js index 9a897789b47ca..c8e2a0f5ea988 100644 --- a/packages/react-dom/src/__tests__/ReactDOMServerPartialHydration-test.internal.js +++ b/packages/react-dom/src/__tests__/ReactDOMServerPartialHydration-test.internal.js @@ -367,7 +367,7 @@ describe('ReactDOMServerPartialHydration', () => { // This is a new node. expect(span).not.toBe(span2); - if (gate(flags => flags.new)) { + if (gate(flags => flags.dfsEffectsRefactor)) { // 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); diff --git a/packages/react-dom/src/__tests__/ReactWrongReturnPointer-test.js b/packages/react-dom/src/__tests__/ReactWrongReturnPointer-test.js index 688fb5c926ed5..72cb43d75386c 100644 --- a/packages/react-dom/src/__tests__/ReactWrongReturnPointer-test.js +++ b/packages/react-dom/src/__tests__/ReactWrongReturnPointer-test.js @@ -16,6 +16,7 @@ beforeEach(() => { }); // Don't feel too guilty if you have to delete this test. +// @gate dfsEffectsRefactor // @gate new // @gate __DEV__ test('warns in DEV if return pointer is inconsistent', async () => { diff --git a/packages/react-reconciler/src/ReactChildFiber.new.js b/packages/react-reconciler/src/ReactChildFiber.new.js index e9048959fd7a6..601f3b21e4395 100644 --- a/packages/react-reconciler/src/ReactChildFiber.new.js +++ b/packages/react-reconciler/src/ReactChildFiber.new.js @@ -13,7 +13,7 @@ import type {Fiber} from './ReactInternalTypes'; import type {Lanes} from './ReactFiberLane'; import getComponentName from 'shared/getComponentName'; -import {Deletion, Placement} from './ReactFiberFlags'; +import {Placement, Deletion} from './ReactFiberFlags'; import { getIteratorFn, REACT_ELEMENT_TYPE, @@ -256,13 +256,20 @@ function ChildReconciler(shouldTrackSideEffects) { // Noop. return; } - const deletions = returnFiber.deletions; - if (deletions === null) { - returnFiber.deletions = [childToDelete]; - returnFiber.flags |= Deletion; + // 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; } else { - deletions.push(childToDelete); + returnFiber.firstEffect = returnFiber.lastEffect = childToDelete; } + childToDelete.nextEffect = null; + childToDelete.flags = Deletion; } function deleteRemainingChildren( diff --git a/packages/react-reconciler/src/ReactFiber.new.js b/packages/react-reconciler/src/ReactFiber.new.js index ccad1371d8f40..5426568b96611 100644 --- a/packages/react-reconciler/src/ReactFiber.new.js +++ b/packages/react-reconciler/src/ReactFiber.new.js @@ -28,7 +28,7 @@ import { enableFundamentalAPI, enableScopeAPI, } from 'shared/ReactFeatureFlags'; -import {NoFlags, Placement, StaticMask} from './ReactFiberFlags'; +import {NoFlags, Placement} from './ReactFiberFlags'; import {ConcurrentRoot, BlockingRoot} from './ReactRootTags'; import { IndeterminateComponent, @@ -279,6 +279,13 @@ 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; @@ -292,9 +299,6 @@ 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; @@ -360,6 +364,11 @@ 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. diff --git a/packages/react-reconciler/src/ReactFiberBeginWork.new.js b/packages/react-reconciler/src/ReactFiberBeginWork.new.js index 163984840a21f..52bbc5ac405ba 100644 --- a/packages/react-reconciler/src/ReactFiberBeginWork.new.js +++ b/packages/react-reconciler/src/ReactFiberBeginWork.new.js @@ -58,10 +58,10 @@ import { Hydrating, ContentReset, DidCapture, + Update, Ref, Deletion, ForceUpdateForLegacySuspense, - StaticMask, } from './ReactFiberFlags'; import ReactSharedInternals from 'shared/ReactSharedInternals'; import { @@ -671,6 +671,8 @@ 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; @@ -1077,9 +1079,6 @@ function updateHostComponent( workInProgress.flags |= ContentReset; } - // React DevTools reads this flag. - workInProgress.flags |= PerformedWork; - markRef(current, workInProgress); reconcileChildren(current, workInProgress, nextChildren, renderLanes); return workInProgress.child; @@ -2005,14 +2004,9 @@ function updateSuspensePrimaryChildren( primaryChildFragment.sibling = null; if (currentFallbackChildFragment !== null) { // Delete the fallback child fragment - 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); - } + currentFallbackChildFragment.nextEffect = null; + currentFallbackChildFragment.flags = Deletion; + workInProgress.firstEffect = workInProgress.lastEffect = currentFallbackChildFragment; } workInProgress.child = primaryChildFragment; @@ -2069,19 +2063,24 @@ 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. - workInProgress.deletions = null; + // 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; + } } 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) { @@ -2566,6 +2565,7 @@ function initSuspenseListRenderState( tail: null | Fiber, lastContentRow: null | Fiber, tailMode: SuspenseListTailMode, + lastEffectBeforeRendering: null | Fiber, ): void { const renderState: null | SuspenseListRenderState = workInProgress.memoizedState; @@ -2577,6 +2577,7 @@ function initSuspenseListRenderState( last: lastContentRow, tail: tail, tailMode: tailMode, + lastEffect: lastEffectBeforeRendering, }: SuspenseListRenderState); } else { // We can reuse the existing object from previous renders. @@ -2586,6 +2587,7 @@ function initSuspenseListRenderState( renderState.last = lastContentRow; renderState.tail = tail; renderState.tailMode = tailMode; + renderState.lastEffect = lastEffectBeforeRendering; } } @@ -2667,6 +2669,7 @@ function updateSuspenseListComponent( tail, lastContentRow, tailMode, + workInProgress.lastEffect, ); break; } @@ -2698,6 +2701,7 @@ function updateSuspenseListComponent( tail, null, // last tailMode, + workInProgress.lastEffect, ); break; } @@ -2708,6 +2712,7 @@ function updateSuspenseListComponent( null, // tail null, // last undefined, + workInProgress.lastEffect, ); break; } @@ -2967,14 +2972,15 @@ function remountFiber( // Delete the old fiber and place the new one. // Since the old fiber is disconnected, we have to schedule it manually. - 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; + const last = returnFiber.lastEffect; + if (last !== null) { + last.nextEffect = current; + returnFiber.lastEffect = current; } else { - deletions.push(current); + returnFiber.firstEffect = returnFiber.lastEffect = current; } + current.nextEffect = null; + current.flags = Deletion; newWorkInProgress.flags |= Placement; @@ -3059,6 +3065,15 @@ 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; @@ -3165,6 +3180,7 @@ 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.new.js b/packages/react-reconciler/src/ReactFiberClassComponent.new.js index 663b27dfed419..4e58dd71f3dc6 100644 --- a/packages/react-reconciler/src/ReactFiberClassComponent.new.js +++ b/packages/react-reconciler/src/ReactFiberClassComponent.new.js @@ -12,14 +12,13 @@ import type {Lanes} from './ReactFiberLane'; import type {UpdateQueue} from './ReactUpdateQueue.new'; import * as React from 'react'; -import {Update, Snapshot, MountLayoutDev} from './ReactFiberFlags'; +import {Update, Snapshot} from './ReactFiberFlags'; import { debugRenderPhaseSideEffectsForStrictMode, disableLegacyContext, enableDebugTracing, enableSchedulingProfiler, warnAboutDeprecatedLifecycles, - enableDoubleInvokingEffects, } from 'shared/ReactFeatureFlags'; import ReactStrictModeWarnings from './ReactStrictModeWarnings.new'; import {isMounted} from './ReactFiberTreeReflection'; @@ -30,13 +29,7 @@ import invariant from 'shared/invariant'; import {REACT_CONTEXT_TYPE, REACT_PROVIDER_TYPE} from 'shared/ReactSymbols'; import {resolveDefaultProps} from './ReactFiberLazyComponent.new'; -import { - BlockingMode, - ConcurrentMode, - DebugTracingMode, - NoMode, - StrictMode, -} from './ReactTypeOfMode'; +import {DebugTracingMode, StrictMode} from './ReactTypeOfMode'; import { enqueueUpdate, @@ -897,16 +890,7 @@ function mountClassInstance( } if (typeof instance.componentDidMount === 'function') { - if ( - __DEV__ && - enableDoubleInvokingEffects && - (workInProgress.mode & (BlockingMode | ConcurrentMode)) !== NoMode - ) { - // Never double-invoke effects for legacy roots. - workInProgress.flags |= MountLayoutDev | Update; - } else { - workInProgress.flags |= Update; - } + workInProgress.flags |= Update; } } @@ -976,15 +960,7 @@ 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') { - if ( - __DEV__ && - enableDoubleInvokingEffects && - (workInProgress.mode & (BlockingMode | ConcurrentMode)) !== NoMode - ) { - workInProgress.flags |= MountLayoutDev | Update; - } else { - workInProgress.flags |= Update; - } + workInProgress.flags |= Update; } return false; } @@ -1027,29 +1003,13 @@ function resumeMountClassInstance( } } if (typeof instance.componentDidMount === 'function') { - if ( - __DEV__ && - enableDoubleInvokingEffects && - (workInProgress.mode & (BlockingMode | ConcurrentMode)) !== NoMode - ) { - workInProgress.flags |= MountLayoutDev | Update; - } else { - workInProgress.flags |= Update; - } + 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') { - if ( - __DEV__ && - enableDoubleInvokingEffects && - (workInProgress.mode & (BlockingMode | ConcurrentMode)) !== NoMode - ) { - workInProgress.flags |= MountLayoutDev | Update; - } else { - workInProgress.flags |= Update; - } + workInProgress.flags |= Update; } // If shouldComponentUpdate returned false, we should still update the diff --git a/packages/react-reconciler/src/ReactFiberCommitWork.new.js b/packages/react-reconciler/src/ReactFiberCommitWork.new.js index 8f268e9261aa6..f16fb99435519 100644 --- a/packages/react-reconciler/src/ReactFiberCommitWork.new.js +++ b/packages/react-reconciler/src/ReactFiberCommitWork.new.js @@ -17,14 +17,13 @@ import type { } from './ReactFiberHostConfig'; import type {Fiber} from './ReactInternalTypes'; import type {FiberRoot} from './ReactInternalTypes'; +import type {Lanes} from './ReactFiberLane'; import type {SuspenseState} from './ReactFiberSuspenseComponent.new'; import type {UpdateQueue} from './ReactUpdateQueue.new'; import type {FunctionComponentUpdateQueue} from './ReactFiberHooks.new'; 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 { @@ -36,8 +35,6 @@ import { enableFundamentalAPI, enableSuspenseCallback, enableScopeAPI, - enableDoubleInvokingEffects, - enableRecursiveCommitTraversal, } from 'shared/ReactFeatureFlags'; import { FunctionComponent, @@ -69,29 +66,11 @@ 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.new'; import {resolveDefaultProps} from './ReactFiberLazyComponent.new'; import { @@ -99,23 +78,14 @@ import { getCommitTime, recordLayoutEffectDuration, startLayoutEffectTimer, - recordPassiveEffectDuration, - startPassiveEffectTimer, } from './ReactProfilerTimer.new'; -import { - NoMode, - BlockingMode, - ConcurrentMode, - ProfileMode, -} from './ReactTypeOfMode'; +import {ProfileMode} from './ReactTypeOfMode'; import {commitUpdateQueue} from './ReactUpdateQueue.new'; import { getPublicInstance, supportsMutation, supportsPersistence, supportsHydration, - prepareForCommit, - beforeActiveInstanceBlur, commitMount, commitUpdate, resetTextContent, @@ -145,6 +115,9 @@ import { captureCommitPhaseError, resolveRetryWakeable, markCommitTimeOfFallback, + enqueuePendingPassiveHookEffectMount, + enqueuePendingPassiveHookEffectUnmount, + enqueuePendingPassiveProfilerEffect, } from './ReactFiberWorkLoop.new'; import { NoFlags as NoHookEffect, @@ -153,12 +126,6 @@ import { Passive as HookPassive, } from './ReactHookEffectTags'; import {didWarnAboutReassigningProps} from './ReactFiberBeginWork.new'; -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__) { @@ -187,7 +154,7 @@ const callComponentWillUnmountWithTimer = function(current, instance) { }; // Capture errors so they don't interrupt unmounting. -function safelyCallComponentWillUnmount(current, instance) { +function safelyCallComponentWillUnmount(current: Fiber, instance: any) { if (__DEV__) { invokeGuardedCallback( null, @@ -256,7 +223,7 @@ function safelyDetachRef(current: Fiber) { } } -export function safelyCallDestroy(current: Fiber, destroy: () => void) { +function safelyCallDestroy(current: Fiber, destroy: () => void) { if (__DEV__) { invokeGuardedCallback(null, destroy, null); if (hasCaughtError()) { @@ -272,19 +239,110 @@ export function safelyCallDestroy(current: Fiber, destroy: () => void) { } } -function commitHookEffectListUnmount(flags: HookFlags, finishedWork: Fiber) { +function commitBeforeMutationLifeCycles( + current: Fiber | null, + finishedWork: Fiber, +): void { + switch (finishedWork.tag) { + case FunctionComponent: + case ForwardRef: + case SimpleMemoComponent: { + 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) { const updateQueue: FunctionComponentUpdateQueue | null = (finishedWork.updateQueue: any); const lastEffect = updateQueue !== null ? updateQueue.lastEffect : null; if (lastEffect !== null) { const firstEffect = lastEffect.next; let effect = firstEffect; do { - if ((effect.tag & flags) === flags) { + if ((effect.tag & tag) === tag) { // Unmount const destroy = effect.destroy; effect.destroy = undefined; if (destroy !== undefined) { - safelyCallDestroy(finishedWork, destroy); + destroy(); } } effect = effect.next; @@ -292,14 +350,14 @@ function commitHookEffectListUnmount(flags: HookFlags, finishedWork: Fiber) { } } -function commitHookEffectListMount(flags: HookFlags, finishedWork: Fiber) { +function commitHookEffectListMount(tag: number, finishedWork: Fiber) { const updateQueue: FunctionComponentUpdateQueue | null = (finishedWork.updateQueue: any); const lastEffect = updateQueue !== null ? updateQueue.lastEffect : null; if (lastEffect !== null) { const firstEffect = lastEffect.next; let effect = firstEffect; do { - if ((effect.tag & flags) === flags) { + if ((effect.tag & tag) === tag) { // Mount const create = effect.create; effect.destroy = create(); @@ -342,1657 +400,450 @@ function commitHookEffectListMount(flags: HookFlags, finishedWork: Fiber) { } } -function commitProfilerPassiveEffect( +function schedulePassiveEffects(finishedWork: Fiber) { + const updateQueue: FunctionComponentUpdateQueue | null = (finishedWork.updateQueue: any); + const lastEffect = updateQueue !== null ? updateQueue.lastEffect : null; + if (lastEffect !== null) { + const firstEffect = lastEffect.next; + let effect = firstEffect; + do { + const {next, tag} = effect; + if ( + (tag & HookPassive) !== NoHookEffect && + (tag & HookHasEffect) !== NoHookEffect + ) { + enqueuePendingPassiveHookEffectUnmount(finishedWork, effect); + enqueuePendingPassiveHookEffectMount(finishedWork, effect); + } + effect = next; + } while (effect !== firstEffect); + } +} + +export function commitPassiveEffectDurations( finishedRoot: FiberRoot, finishedWork: Fiber, ): void { if (enableProfilerTimer && enableProfilerCommitHooks) { - 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(); + // Only Profilers with work in their subtree will have an Update effect scheduled. + if ((finishedWork.flags & Update) !== NoFlags) { + switch (finishedWork.tag) { + case Profiler: { + const {passiveEffectDuration} = finishedWork.stateNode; + const {id, onPostCommit} = finishedWork.memoizedProps; + + // This value will still reflect the previous commit phase. + // It does not get reset until the start of the next commit phase. + const commitTime = getCommitTime(); + + let phase = finishedWork.alternate === null ? 'mount' : 'update'; + if (enableProfilerNestedUpdatePhase) { + if (isCurrentUpdateNested()) { + phase = 'nested-update'; + } + } - let phase = finishedWork.alternate === null ? 'mount' : 'update'; - if (enableProfilerNestedUpdatePhase) { - if (isCurrentUpdateNested()) { - phase = 'nested-update'; + if (typeof onPostCommit === 'function') { + if (enableSchedulerTracing) { + onPostCommit( + id, + phase, + passiveEffectDuration, + commitTime, + finishedRoot.memoizedInteractions, + ); + } else { + onPostCommit(id, phase, passiveEffectDuration, commitTime); + } } - } - if (typeof onPostCommit === 'function') { - if (enableSchedulerTracing) { - onPostCommit( - id, - phase, - passiveEffectDuration, - commitTime, - finishedRoot.memoizedInteractions, - ); - } else { - onPostCommit(id, phase, passiveEffectDuration, commitTime); + // Bubble times to the next nearest ancestor Profiler. + // After we process that Profiler, we'll bubble further up. + let parentFiber = finishedWork.return; + while (parentFiber !== null) { + if (parentFiber.tag === Profiler) { + const parentStateNode = parentFiber.stateNode; + parentStateNode.passiveEffectDuration += passiveEffectDuration; + break; + } + parentFiber = parentFiber.return; } + break; } - break; - } - default: - break; - } - } -} - -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, error); - } - resetCurrentDebugFiberInDEV(); - } else { - try { - commitBeforeMutationEffectsOnFiber(fiber); - } catch (error) { - captureCommitPhaseError(fiber, error); + default: + break; } } - 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 - ) { - warnIfWrongReturnPointer(fiber, child); - 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, error); - } - resetCurrentDebugFiberInDEV(); - } else { - try { - commitBeforeMutationEffectsOnFiber(fiber); - } catch (error) { - captureCommitPhaseError(fiber, error); +function commitLifeCycles( + finishedRoot: FiberRoot, + current: Fiber | null, + finishedWork: Fiber, + committedLanes: Lanes, +): void { + switch (finishedWork.tag) { + case FunctionComponent: + case ForwardRef: + case SimpleMemoComponent: { + // 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); } - } - const sibling = fiber.sibling; - if (sibling !== null) { - warnIfWrongReturnPointer(fiber.return, sibling); - nextEffect = sibling; + schedulePassiveEffects(finishedWork); return; } - - nextEffect = fiber.return; - } -} - -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: { - 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', - ); - } + 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', + ); } } - 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); + } + 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( - '%s.getSnapshotBeforeUpdate(): A snapshot value (or null) ' + - 'must be returned. You have returned undefined.', - getComponentName(finishedWork.type), + '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', ); } } - instance.__reactInternalSnapshotBeforeUpdate = snapshot; + } + if ( + enableProfilerTimer && + enableProfilerCommitHooks && + finishedWork.mode & ProfileMode + ) { + try { + startLayoutEffectTimer(); + instance.componentDidUpdate( + prevProps, + prevState, + instance.__reactInternalSnapshotBeforeUpdate, + ); + } finally { + recordLayoutEffectDuration(finishedWork); + } + } else { + instance.componentDidUpdate( + prevProps, + prevState, + instance.__reactInternalSnapshotBeforeUpdate, + ); } } - break; } - case HostRoot: { - if (supportsMutation) { - if (finishedWork.flags & Snapshot) { - const root = finishedWork.stateNode; - clearContainer(root.containerInfo); + + // 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', + ); + } } } - break; + // 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); } - 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.', - ); - } - resetCurrentDebugFiberInDEV(); - } -} - -function commitBeforeMutationEffectsDeletions(deletions: Array) { - for (let i = 0; i < deletions.length; i++) { - const fiber = deletions[i]; - - // TODO (effects) It would be nice to avoid calling doesFiberContain() - // Maybe we can repurpose one of the subtreeFlags positions for this instead? - // Use it to store which part of the tree the focused instance is in? - // This assumes we can safely determine that instance during the "render" phase. - - if (doesFiberContain(fiber, ((focusedInstanceHandle: any): Fiber))) { - shouldFireAfterActiveInstanceBlur = true; - beforeActiveInstanceBlur(fiber); + return; } - } -} - -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, root, renderPriorityLevel); - } - - if (fiber.child !== null) { - const mutationFlags = fiber.subtreeFlags & MutationMask; - if (mutationFlags !== NoFlags) { - recursivelyCommitMutationEffects( - fiber.child, - root, - renderPriorityLevel, - ); + case HostRoot: { + // TODO: I think this is now always non-null by the time it reaches the + // commit phase. Consider removing the type check. + const updateQueue: UpdateQueue< + *, + > | null = (finishedWork.updateQueue: any); + if (updateQueue !== null) { + let instance = null; + if (finishedWork.child !== null) { + switch (finishedWork.child.tag) { + case HostComponent: + instance = getPublicInstance(finishedWork.child.stateNode); + break; + case ClassComponent: + instance = finishedWork.child.stateNode; + break; + } + } + commitUpdateQueue(finishedWork, updateQueue, instance); } + return; } + case HostComponent: { + const instance: Instance = finishedWork.stateNode; - if (__DEV__) { - setCurrentDebugFiberInDEV(fiber); - invokeGuardedCallback( - null, - commitMutationEffectsOnFiber, - null, - fiber, - root, - renderPriorityLevel, - ); - if (hasCaughtError()) { - const error = clearCaughtError(); - captureCommitPhaseError(fiber, error); + // 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); } - resetCurrentDebugFiberInDEV(); - } else { - try { - commitMutationEffectsOnFiber(fiber, root, renderPriorityLevel); - } catch (error) { - captureCommitPhaseError(fiber, 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, root, renderPriorityLevel); - } - - const child = fiber.child; - if ((fiber.subtreeFlags & MutationMask) !== NoFlags && child !== null) { - warnIfWrongReturnPointer(fiber, child); - 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, error); - } - resetCurrentDebugFiberInDEV(); - } else { - try { - commitMutationEffectsOnFiber(fiber, root, renderPriorityLevel); - } catch (error) { - captureCommitPhaseError(fiber, error); - } - } - - const sibling = fiber.sibling; - if (sibling !== null) { - warnIfWrongReturnPointer(fiber.return, sibling); - nextEffect = sibling; - return; - } - - nextEffect = fiber.return; - } -} - -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; - } - } -} - -function commitMutationEffectsDeletions( - deletions: Array, - root: FiberRoot, - renderPriorityLevel, -) { - for (let i = 0; i < deletions.length; i++) { - const childToDelete = deletions[i]; - if (__DEV__) { - invokeGuardedCallback( - null, - commitDeletion, - null, - root, - childToDelete, - renderPriorityLevel, - ); - if (hasCaughtError()) { - const error = clearCaughtError(); - captureCommitPhaseError(childToDelete, error); - } - } else { - try { - commitDeletion(root, childToDelete, renderPriorityLevel); - } catch (error) { - captureCommitPhaseError(childToDelete, 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, error); - } - resetCurrentDebugFiberInDEV(); - } else { - try { - recursivelyCommitLayoutEffects(finishedWork, finishedRoot); - } catch (error) { - captureCommitPhaseError(finishedWork, 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, error); - } - if (prevCurrentFiberInDEV !== null) { - setCurrentDebugFiberInDEV(prevCurrentFiberInDEV); - } else { - resetCurrentDebugFiberInDEV(); - } - } else { - try { - recursivelyCommitLayoutEffects(child, finishedRoot); - } catch (error) { - captureCommitPhaseError(child, 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, error); - } - if (prevCurrentFiberInDEV !== null) { - setCurrentDebugFiberInDEV(prevCurrentFiberInDEV); - } else { - resetCurrentDebugFiberInDEV(); - } - } else { - try { - commitLayoutEffectsForProfiler(finishedWork, finishedRoot); - } catch (error) { - captureCommitPhaseError(finishedWork, 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, error); - } - if (prevCurrentFiberInDEV !== null) { - setCurrentDebugFiberInDEV(prevCurrentFiberInDEV); - } else { - resetCurrentDebugFiberInDEV(); - } - } else { - try { - recursivelyCommitLayoutEffects(child, finishedRoot); - } catch (error) { - captureCommitPhaseError(child, error); - } - } - } - child = child.sibling; - } - - const primaryFlags = flags & (Update | Callback); - if (primaryFlags !== NoFlags) { - switch (tag) { - case FunctionComponent: - case ForwardRef: - case SimpleMemoComponent: { - 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, error); - } - resetCurrentDebugFiberInDEV(); - } else { - try { - commitLayoutEffectsForProfiler(finishedWork, finishedRoot); - } catch (error) { - captureCommitPhaseError(finishedWork, 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) { - warnIfWrongReturnPointer(finishedWork.return, sibling); - nextEffect = sibling; - } else { - nextEffect = finishedWork.return; - iterativelyCommitLayoutEffects_complete(subtreeRoot, finishedRoot); - } - } else { - warnIfWrongReturnPointer(finishedWork, firstChild); - 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, error); - } - resetCurrentDebugFiberInDEV(); - } else { - try { - commitLayoutEffectsOnFiber(finishedRoot, fiber); - } catch (error) { - captureCommitPhaseError(fiber, error); - } - } - } - - if (fiber === subtreeRoot) { - nextEffect = null; - return; - } - - const sibling = fiber.sibling; - if (sibling !== null) { - warnIfWrongReturnPointer(fiber.return, sibling); - 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: { - 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); - } - } -} - -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; - - let phase = current === null ? 'mount' : 'update'; - if (enableProfilerNestedUpdatePhase) { - if (isCurrentUpdateNested()) { - phase = 'nested-update'; - } - } - - if ((flags & OnRenderFlag) !== NoFlags && typeof onRender === 'function') { - if (enableSchedulerTracing) { - onRender( - finishedWork.memoizedProps.id, - phase, - finishedWork.actualDuration, - finishedWork.treeBaseDuration, - finishedWork.actualStartTime, - commitTime, - finishedRoot.memoizedInteractions, - ); - } else { - onRender( - finishedWork.memoizedProps.id, - phase, - finishedWork.actualDuration, - finishedWork.treeBaseDuration, - finishedWork.actualStartTime, - commitTime, - ); - } - } - - if (enableProfilerCommitHooks) { - if ( - (flags & OnCommitFlag) !== NoFlags && - typeof onCommit === 'function' - ) { - if (enableSchedulerTracing) { - onCommit( - finishedWork.memoizedProps.id, - phase, - effectDuration, - commitTime, - finishedRoot.memoizedInteractions, - ); - } else { - onCommit( - finishedWork.memoizedProps.id, - phase, - effectDuration, - commitTime, - ); - } - } - } - } -} - -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); - } -} - -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); - } -} - -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); - } -} - -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, error); - } - resetCurrentDebugFiberInDEV(); - } else { - try { - commitPassiveMountOnFiber(root, fiber); - } catch (error) { - captureCommitPhaseError(fiber, 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, error); - } - resetCurrentDebugFiberInDEV(); - } else { - try { - commitProfilerPassiveEffect(root, fiber); - } catch (error) { - captureCommitPhaseError(fiber, 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) { - warnIfWrongReturnPointer(fiber.return, sibling); - nextEffect = sibling; - } else { - nextEffect = fiber.return; - iterativelyCommitPassiveMountEffects_complete(subtreeRoot, root); - } - } else { - warnIfWrongReturnPointer(fiber, firstChild); - 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, error); - } - resetCurrentDebugFiberInDEV(); - } else { - try { - commitPassiveMountOnFiber(root, fiber); - } catch (error) { - captureCommitPhaseError(fiber, error); - } - } - } - if (fiber === subtreeRoot) { - nextEffect = null; return; } - - const sibling = fiber.sibling; - if (sibling !== null) { - warnIfWrongReturnPointer(fiber.return, sibling); - nextEffect = sibling; + case HostText: { + // We have no life-cycles associated with text. 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, - ); - - // 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, - ); - - // 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) { - warnIfWrongReturnPointer(fiber, child); - 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) { - warnIfWrongReturnPointer(fiber.return, sibling); - nextEffect = sibling; + case HostPortal: { + // We have no life-cycles associated with portals. return; } + case Profiler: { + if (enableProfilerTimer) { + const {onCommit, onRender} = finishedWork.memoizedProps; + const {effectDuration} = finishedWork.stateNode; - nextEffect = fiber.return; - } -} - -function recursivelyCommitPassiveUnmountEffectsInsideOfDeletedTree( - fiberToDelete: 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); - child = child.sibling; - } - } - - if ((fiberToDelete.flags & PassiveStatic) !== NoFlags) { - setCurrentDebugFiberInDEV(fiberToDelete); - commitPassiveUnmountInsideDeletedTreeOnFiber(fiberToDelete); - resetCurrentDebugFiberInDEV(); - } -} + const commitTime = getCommitTime(); -function iterativelyCommitPassiveUnmountEffectsInsideOfDeletedTree_begin( - deletedSubtreeRoot: Fiber, -) { - while (nextEffect !== null) { - const fiber = nextEffect; - const child = fiber.child; - if ((fiber.subtreeFlags & PassiveStatic) !== NoFlags && child !== null) { - warnIfWrongReturnPointer(fiber, child); - nextEffect = child; - } else { - iterativelyCommitPassiveUnmountEffectsInsideOfDeletedTree_complete( - deletedSubtreeRoot, - ); - } - } -} + let phase = current === null ? 'mount' : 'update'; + if (enableProfilerNestedUpdatePhase) { + if (isCurrentUpdateNested()) { + phase = 'nested-update'; + } + } -function iterativelyCommitPassiveUnmountEffectsInsideOfDeletedTree_complete( - deletedSubtreeRoot: Fiber, -) { - while (nextEffect !== null) { - const fiber = nextEffect; - if ((fiber.flags & PassiveStatic) !== NoFlags) { - setCurrentDebugFiberInDEV(fiber); - commitPassiveUnmountInsideDeletedTreeOnFiber(fiber); - resetCurrentDebugFiberInDEV(); - } + if (typeof onRender === 'function') { + if (enableSchedulerTracing) { + onRender( + finishedWork.memoizedProps.id, + phase, + finishedWork.actualDuration, + finishedWork.treeBaseDuration, + finishedWork.actualStartTime, + commitTime, + finishedRoot.memoizedInteractions, + ); + } else { + onRender( + finishedWork.memoizedProps.id, + phase, + finishedWork.actualDuration, + finishedWork.treeBaseDuration, + finishedWork.actualStartTime, + commitTime, + ); + } + } + + if (enableProfilerCommitHooks) { + if (typeof onCommit === 'function') { + if (enableSchedulerTracing) { + onCommit( + finishedWork.memoizedProps.id, + phase, + effectDuration, + commitTime, + finishedRoot.memoizedInteractions, + ); + } else { + onCommit( + finishedWork.memoizedProps.id, + phase, + effectDuration, + commitTime, + ); + } + } - if (fiber === deletedSubtreeRoot) { - nextEffect = null; + // 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; } - - const sibling = fiber.sibling; - if (sibling !== null) { - warnIfWrongReturnPointer(fiber.return, sibling); - nextEffect = sibling; + case SuspenseComponent: { + commitSuspenseHydrationCallbacks(finishedRoot, finishedWork); return; } - - nextEffect = fiber.return; + case SuspenseListComponent: + case IncompleteClassComponent: + case FundamentalComponent: + case ScopeComponent: + case OffscreenComponent: + case LegacyHiddenComponent: + return; } + invariant( + false, + 'This unit of work tag should not have side-effects. This error is ' + + 'likely caused by a bug in React. Please file an issue.', + ); } -function 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; +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; + } } } @@ -2092,7 +943,9 @@ function commitUnmount( do { const {destroy, tag} = effect; if (destroy !== undefined) { - if ((tag & HookLayout) !== NoHookEffect) { + if ((tag & HookPassive) !== NoHookEffect) { + enqueuePendingPassiveHookEffectUnmount(current, effect); + } else { if ( enableProfilerTimer && enableProfilerCommitHooks && @@ -2206,24 +1059,32 @@ function commitNestedUnmounts( } function detachFiberMutation(fiber: Fiber) { - // Cut off the return pointer to disconnect it from the tree. - // This enables us to detect and warn against state updates on an unmounted component. - // It also prevents events from bubbling from within disconnected components. - // - // Ideally, we should also clear the child pointer of the parent alternate to let this + // Cut off the return pointers to disconnect it from the tree. Ideally, we + // should clear the child pointer of the parent alternate to let this // get GC:ed but we don't know which for sure which parent is the current - // one so we'll settle for GC:ing the subtree of this child. - // This child itself will be GC:ed when the parent updates the next time. + // 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. // - // 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; - } + // Don't disconnect stateNode now; it will be detached in detachFiberAfterEffects. + // It may be required if the current component is an error boundary, + // and one of its descendants throws while unmounting a passive effect. + fiber.alternate = null; + fiber.child = null; + fiber.dependencies = null; + fiber.firstEffect = null; + fiber.lastEffect = null; + fiber.memoizedProps = null; + fiber.memoizedState = null; + fiber.pendingProps = null; fiber.return = null; + fiber.updateQueue = null; + if (__DEV__) { + fiber._debugOwner = null; + } } function emptyPortalContainer(current: Fiber) { @@ -2459,9 +1320,9 @@ function insertOrAppendPlacementNode( } function unmountHostComponents( - finishedRoot, - current, - renderPriorityLevel, + finishedRoot: FiberRoot, + current: Fiber, + renderPriorityLevel: ReactPriorityLevel, ): void { // We only have the top Fiber that was deleted but we need to recurse down its // children to find all the terminal nodes. @@ -2915,7 +1776,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. -function isSuspenseBoundaryBeingHidden( +export function isSuspenseBoundaryBeingHidden( current: Fiber | null, finishedWork: Fiber, ): boolean { @@ -2929,266 +1790,20 @@ function isSuspenseBoundaryBeingHidden( return false; } -function commitResetTextContent(current: Fiber): void { +function commitResetTextContent(current: Fiber) { if (!supportsMutation) { return; } resetTextContent(current.stateNode); } -function commitPassiveUnmountOnFiber(finishedWork: Fiber): void { - switch (finishedWork.tag) { - case FunctionComponent: - case ForwardRef: - case SimpleMemoComponent: { - if ( - enableProfilerTimer && - enableProfilerCommitHooks && - finishedWork.mode & ProfileMode - ) { - startPassiveEffectTimer(); - commitHookEffectListUnmount(HookPassive | HookHasEffect, finishedWork); - recordPassiveEffectDuration(finishedWork); - } else { - commitHookEffectListUnmount(HookPassive | HookHasEffect, finishedWork); - } - break; - } - } -} - -function commitPassiveUnmountInsideDeletedTreeOnFiber(current: Fiber): void { - switch (current.tag) { - case FunctionComponent: - case ForwardRef: - case SimpleMemoComponent: { - if ( - enableProfilerTimer && - enableProfilerCommitHooks && - current.mode & ProfileMode - ) { - startPassiveEffectTimer(); - commitHookEffectListUnmount(HookPassive, current); - recordPassiveEffectDuration(current); - } else { - commitHookEffectListUnmount(HookPassive, current); - } - break; - } - } -} - -function commitPassiveMountOnFiber( - finishedRoot: FiberRoot, - finishedWork: Fiber, -): void { - switch (finishedWork.tag) { - case FunctionComponent: - case ForwardRef: - case SimpleMemoComponent: { - 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: { - invokeGuardedCallback( - null, - commitHookEffectListMount, - null, - HookLayout | HookHasEffect, - fiber, - ); - if (hasCaughtError()) { - const mountError = clearCaughtError(); - captureCommitPhaseError(fiber, mountError); - } - break; - } - case ClassComponent: { - const instance = fiber.stateNode; - invokeGuardedCallback(null, instance.componentDidMount, instance); - if (hasCaughtError()) { - const mountError = clearCaughtError(); - captureCommitPhaseError(fiber, 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: { - invokeGuardedCallback( - null, - commitHookEffectListMount, - null, - HookPassive | HookHasEffect, - fiber, - ); - if (hasCaughtError()) { - const mountError = clearCaughtError(); - captureCommitPhaseError(fiber, 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: { - invokeGuardedCallback( - null, - commitHookEffectListUnmount, - null, - HookLayout | HookHasEffect, - fiber, - ); - if (hasCaughtError()) { - const unmountError = clearCaughtError(); - captureCommitPhaseError(fiber, unmountError); - } - break; - } - case ClassComponent: { - const instance = fiber.stateNode; - if (typeof instance.componentWillUnmount === 'function') { - safelyCallComponentWillUnmount(fiber, instance); - } - 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: { - invokeGuardedCallback( - null, - commitHookEffectListUnmount, - null, - HookPassive | HookHasEffect, - fiber, - ); - if (hasCaughtError()) { - const unmountError = clearCaughtError(); - captureCommitPhaseError(fiber, 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; - } - } -} - -let didWarnWrongReturnPointer = false; -function warnIfWrongReturnPointer(returnFiber, child) { - if (__DEV__) { - if (!didWarnWrongReturnPointer && child.return !== returnFiber) { - didWarnWrongReturnPointer = true; - console.error( - 'Internal React error: Return pointer is inconsistent ' + - 'with parent.', - ); - } - } -} +export { + commitBeforeMutationLifeCycles, + commitResetTextContent, + commitPlacement, + commitDeletion, + commitWork, + commitLifeCycles, + commitAttachRef, + commitDetachRef, +}; diff --git a/packages/react-reconciler/src/ReactFiberCompleteWork.new.js b/packages/react-reconciler/src/ReactFiberCompleteWork.new.js index da98e0c4de34f..89d2193176c27 100644 --- a/packages/react-reconciler/src/ReactFiberCompleteWork.new.js +++ b/packages/react-reconciler/src/ReactFiberCompleteWork.new.js @@ -8,7 +8,7 @@ */ import type {Fiber} from './ReactInternalTypes'; -import type {Lanes, Lane} from './ReactFiberLane'; +import type {Lanes} from './ReactFiberLane'; import type { ReactFundamentalComponentInstance, ReactScopeInstance, @@ -57,28 +57,8 @@ import { OffscreenComponent, LegacyHiddenComponent, } from './ReactWorkTags'; -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 {NoMode, BlockingMode, ProfileMode} from './ReactTypeOfMode'; +import {Ref, Update, NoFlags, DidCapture, Snapshot} from './ReactFiberFlags'; import invariant from 'shared/invariant'; import { @@ -148,16 +128,9 @@ import { renderHasNotSuspendedYet, popRenderLanes, getRenderTargetTime, - subtreeRenderLanes, } from './ReactFiberWorkLoop.new'; import {createFundamentalStateInstance} from './ReactFiberFundamental.new'; -import { - OffscreenLane, - SomeRetryLane, - NoLanes, - includesSomeLane, - mergeLanes, -} from './ReactFiberLane'; +import {OffscreenLane, SomeRetryLane} from './ReactFiberLane'; import {resetChildFibers} from './ReactChildFiber.new'; import {createScopeInstance} from './ReactFiberScope.new'; import {transferActualDuration} from './ReactProfilerTimer.new'; @@ -172,31 +145,6 @@ 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; @@ -241,7 +189,7 @@ if (supportsMutation) { } }; - updateHostContainer = function(current: null | Fiber, workInProgress: Fiber) { + updateHostContainer = function(workInProgress: Fiber) { // Noop }; updateHostComponent = function( @@ -485,13 +433,13 @@ if (supportsMutation) { node = node.sibling; } }; - updateHostContainer = function(current: null | Fiber, workInProgress: Fiber) { + updateHostContainer = function(workInProgress: Fiber) { const portalOrRoot: { containerInfo: Container, pendingChildren: ChildSet, ... } = workInProgress.stateNode; - const childrenUnchanged = hadNoMutationsEffects(current, workInProgress); + const childrenUnchanged = workInProgress.firstEffect === null; if (childrenUnchanged) { // No changes, just reuse the existing instance. } else { @@ -516,7 +464,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 = hadNoMutationsEffects(current, workInProgress); + const childrenUnchanged = workInProgress.firstEffect === null; if (childrenUnchanged && oldProps === newProps) { // No changes, just reuse the existing instance. // Note that this might release a previous clone. @@ -599,7 +547,7 @@ if (supportsMutation) { }; } else { // No host operations - updateHostContainer = function(current: null | Fiber, workInProgress: Fiber) { + updateHostContainer = function(workInProgress: Fiber) { // Noop }; updateHostComponent = function( @@ -692,126 +640,6 @@ 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; - - // Update the return pointer so the tree is consistent. This is a code - // smell because it assumes the commit phase is never concurrent with - // the render phase. Will address during refactor to alternate model. - child.return = completedWork; - - child = child.sibling; - } - } - - completedWork.subtreeFlags |= subtreeFlags; - } else { - // Bubble up the earliest expiration time. - if (enableProfilerTimer && (completedWork.mode & ProfileMode) !== NoMode) { - // In profiling mode, resetChildExpirationTime is also used to reset - // profiler durations. - let treeBaseDuration = ((completedWork.selfBaseDuration: any): number); - - let child = completedWork.child; - while (child !== null) { - newChildLanes = mergeLanes( - newChildLanes, - mergeLanes(child.lanes, child.childLanes), - ); - - // "Static" flags share the lifetime of the fiber/hook they belong to, - // so we should bubble those up even during a bailout. All the other - // flags have a lifetime only of a single render + commit, so we should - // ignore them. - subtreeFlags |= child.subtreeFlags & StaticMask; - subtreeFlags |= child.flags & StaticMask; - - treeBaseDuration += child.treeBaseDuration; - child = child.sibling; - } - - completedWork.treeBaseDuration = treeBaseDuration; - } else { - let child = completedWork.child; - while (child !== null) { - newChildLanes = mergeLanes( - newChildLanes, - mergeLanes(child.lanes, child.childLanes), - ); - - // "Static" flags share the lifetime of the fiber/hook they belong to, - // so we should bubble those up even during a bailout. All the other - // flags have a lifetime only of a single render + commit, so we should - // ignore them. - subtreeFlags |= child.subtreeFlags & StaticMask; - subtreeFlags |= child.flags & StaticMask; - - // Update the return pointer so the tree is consistent. This is a code - // smell because it assumes the commit phase is never concurrent with - // the render phase. Will address during refactor to alternate model. - child.return = completedWork; - - child = child.sibling; - } - } - - completedWork.subtreeFlags |= subtreeFlags; - } - - completedWork.childLanes = newChildLanes; - - return didBailout; -} - function completeWork( current: Fiber | null, workInProgress: Fiber, @@ -827,16 +655,15 @@ 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: { @@ -864,8 +691,7 @@ function completeWork( workInProgress.flags |= Snapshot; } } - updateHostContainer(current, workInProgress); - bubbleProperties(workInProgress); + updateHostContainer(workInProgress); return null; } case HostComponent: { @@ -892,7 +718,6 @@ function completeWork( 'caused by a bug in React. Please file an issue.', ); // This can happen when we abort work. - bubbleProperties(workInProgress); return null; } @@ -950,7 +775,6 @@ function completeWork( markRef(workInProgress); } } - bubbleProperties(workInProgress); return null; } case HostText: { @@ -985,58 +809,6 @@ 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: { @@ -1056,20 +828,6 @@ 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. @@ -1086,20 +844,6 @@ 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; } } @@ -1115,7 +859,6 @@ function completeWork( ) { transferActualDuration(workInProgress); } - // Don't bubble properties in this case. return workInProgress; } @@ -1169,8 +912,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. - // TODO: Move to passive phase + // retry listener to the promise. This flag is also used to hide the + // primary children. workInProgress.flags |= Update; } } @@ -1182,7 +925,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 | Visibility; + workInProgress.flags |= Update; } } if ( @@ -1191,36 +934,20 @@ 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(current, workInProgress); + updateHostContainer(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 @@ -1229,7 +956,6 @@ function completeWork( if (isLegacyContextProvider(Component)) { popLegacyContext(workInProgress); } - bubbleProperties(workInProgress); return null; } case SuspenseListComponent: { @@ -1241,7 +967,6 @@ 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; } @@ -1294,8 +1019,12 @@ 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 @@ -1307,7 +1036,6 @@ function completeWork( ForceSuspenseFallback, ), ); - // Don't bubble properties in this case. return workInProgress.child; } row = row.sibling; @@ -1364,8 +1092,16 @@ 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 ( @@ -1385,10 +1121,13 @@ 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. 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. + // to get it started back up to attempt the next item. While in terms + // of priority this work has the same priority as this current render, + // it's not part of the same transition once the transition has + // committed. If it's sync, we still want to yield so that it can be + // painted. Conceptually, this is really the same as pinging. + // We can use any RetryLane even if it's the one currently rendering + // since we're leaving it behind on this node. workInProgress.lanes = SomeRetryLane; if (enableSchedulerTracing) { markSpawnedWork(SomeRetryLane); @@ -1420,6 +1159,7 @@ function completeWork( const next = renderState.tail; renderState.rendering = next; renderState.tail = next.sibling; + renderState.lastEffect = workInProgress.lastEffect; renderState.renderingStartTime = now(); next.sibling = null; @@ -1437,10 +1177,8 @@ 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: { @@ -1468,7 +1206,6 @@ function completeWork( ): any): Instance); fundamentalInstance.instance = instance; if (fundamentalImpl.reconcileChildren === false) { - bubbleProperties(workInProgress); return null; } appendAllChildren(instance, workInProgress, false, false); @@ -1491,7 +1228,6 @@ function completeWork( markUpdate(workInProgress); } } - bubbleProperties(workInProgress); return null; } break; @@ -1514,7 +1250,6 @@ function completeWork( markRef(workInProgress); } } - bubbleProperties(workInProgress); return null; } break; @@ -1522,30 +1257,19 @@ function completeWork( 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 | Visibility; + workInProgress.flags |= Update; } } - - // 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.new.js b/packages/react-reconciler/src/ReactFiberHooks.new.js index 37b141839b972..6f2cb9ca400c1 100644 --- a/packages/react-reconciler/src/ReactFiberHooks.new.js +++ b/packages/react-reconciler/src/ReactFiberHooks.new.js @@ -26,16 +26,10 @@ import { enableSchedulingProfiler, enableNewReconciler, decoupleUpdatePriorityFromScheduler, - enableDoubleInvokingEffects, enableUseRefAccessWarning, } from 'shared/ReactFeatureFlags'; -import { - NoMode, - BlockingMode, - ConcurrentMode, - DebugTracingMode, -} from './ReactTypeOfMode'; +import {NoMode, BlockingMode, DebugTracingMode} from './ReactTypeOfMode'; import { NoLane, NoLanes, @@ -54,9 +48,6 @@ import {readContext} from './ReactFiberNewContext.new'; import { Update as UpdateEffect, Passive as PassiveEffect, - PassiveStatic as PassiveStaticEffect, - MountLayoutDev as MountLayoutDevEffect, - MountPassiveDev as MountPassiveDevEffect, } from './ReactFiberFlags'; import { HasEffect as HookHasEffect, @@ -475,20 +466,7 @@ export function bailoutHooks( lanes: Lanes, ) { workInProgress.updateQueue = current.updateQueue; - if ( - __DEV__ && - enableDoubleInvokingEffects && - (workInProgress.mode & (BlockingMode | ConcurrentMode)) !== NoMode - ) { - workInProgress.flags &= ~( - MountPassiveDevEffect | - PassiveEffect | - MountLayoutDevEffect | - UpdateEffect - ); - } else { - workInProgress.flags &= ~(PassiveEffect | UpdateEffect); - } + workInProgress.flags &= ~(PassiveEffect | UpdateEffect); current.lanes = removeLanes(current.lanes, lanes); } @@ -1320,30 +1298,16 @@ function mountEffect( ): void { if (__DEV__) { // $FlowExpectedError - jest isn't a global, and isn't recognized outside of tests - if (typeof jest !== 'undefined') { + if ('undefined' !== typeof jest) { warnIfNotCurrentlyActingEffectsInDEV(currentlyRenderingFiber); } } - - if ( - __DEV__ && - enableDoubleInvokingEffects && - (currentlyRenderingFiber.mode & (BlockingMode | ConcurrentMode)) !== NoMode - ) { - return mountEffectImpl( - MountPassiveDevEffect | PassiveEffect | PassiveStaticEffect, - HookPassive, - create, - deps, - ); - } else { - return mountEffectImpl( - PassiveEffect | PassiveStaticEffect, - HookPassive, - create, - deps, - ); - } + return mountEffectImpl( + UpdateEffect | PassiveEffect, + HookPassive, + create, + deps, + ); } function updateEffect( @@ -1352,31 +1316,23 @@ function updateEffect( ): void { if (__DEV__) { // $FlowExpectedError - jest isn't a global, and isn't recognized outside of tests - if (typeof jest !== 'undefined') { + if ('undefined' !== typeof jest) { warnIfNotCurrentlyActingEffectsInDEV(currentlyRenderingFiber); } } - return updateEffectImpl(PassiveEffect, HookPassive, create, deps); + return updateEffectImpl( + UpdateEffect | PassiveEffect, + HookPassive, + create, + deps, + ); } function mountLayoutEffect( create: () => (() => void) | void, deps: Array | void | null, ): void { - if ( - __DEV__ && - enableDoubleInvokingEffects && - (currentlyRenderingFiber.mode & (BlockingMode | ConcurrentMode)) !== NoMode - ) { - return mountEffectImpl( - MountLayoutDevEffect | UpdateEffect, - HookLayout, - create, - deps, - ); - } else { - return mountEffectImpl(UpdateEffect, HookLayout, create, deps); - } + return mountEffectImpl(UpdateEffect, HookLayout, create, deps); } function updateLayoutEffect( @@ -1435,25 +1391,12 @@ function mountImperativeHandle( const effectDeps = deps !== null && deps !== undefined ? deps.concat([ref]) : null; - 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, - ); - } + return mountEffectImpl( + UpdateEffect, + HookLayout, + imperativeHandleEffect.bind(null, create, ref), + effectDeps, + ); } function updateImperativeHandle( @@ -1734,12 +1677,7 @@ function mountOpaqueIdentifier(): OpaqueIDType | void { const setId = mountState(id)[1]; if ((currentlyRenderingFiber.mode & BlockingMode) === NoMode) { - if (__DEV__ && enableDoubleInvokingEffects) { - currentlyRenderingFiber.flags |= - MountPassiveDevEffect | PassiveEffect | PassiveStaticEffect; - } else { - currentlyRenderingFiber.flags |= PassiveEffect | PassiveStaticEffect; - } + currentlyRenderingFiber.flags |= UpdateEffect | PassiveEffect; pushEffect( HookHasEffect | HookPassive, () => { @@ -1855,7 +1793,7 @@ function dispatchAction( } if (__DEV__) { // $FlowExpectedError - jest isn't a global, and isn't recognized outside of tests - if (typeof jest !== 'undefined') { + if ('undefined' !== typeof jest) { warnIfNotScopedWithMatchingAct(fiber); warnIfNotCurrentlyActingUpdatesInDev(fiber); } diff --git a/packages/react-reconciler/src/ReactFiberHydrationContext.new.js b/packages/react-reconciler/src/ReactFiberHydrationContext.new.js index da5e5cbfaa456..e90bc9768a9ae 100644 --- a/packages/react-reconciler/src/ReactFiberHydrationContext.new.js +++ b/packages/react-reconciler/src/ReactFiberHydrationContext.new.js @@ -24,7 +24,7 @@ import { HostRoot, SuspenseComponent, } from './ReactWorkTags'; -import {Deletion, Hydrating, Placement} from './ReactFiberFlags'; +import {Deletion, Placement, Hydrating} from './ReactFiberFlags'; import invariant from 'shared/invariant'; import { @@ -124,14 +124,18 @@ function deleteHydratableInstance( const childToDelete = createFiberFromHostInstanceForDeletion(); childToDelete.stateNode = instance; childToDelete.return = returnFiber; + childToDelete.flags = Deletion; - 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; + // 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; } else { - deletions.push(childToDelete); + returnFiber.firstEffect = returnFiber.lastEffect = childToDelete; } } diff --git a/packages/react-reconciler/src/ReactFiberSuspenseComponent.new.js b/packages/react-reconciler/src/ReactFiberSuspenseComponent.new.js index 385a2ebdfa67d..6e14aacd77def 100644 --- a/packages/react-reconciler/src/ReactFiberSuspenseComponent.new.js +++ b/packages/react-reconciler/src/ReactFiberSuspenseComponent.new.js @@ -60,6 +60,9 @@ 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.new.js b/packages/react-reconciler/src/ReactFiberThrow.new.js index af41cd639a072..9dd9df6d0cfe5 100644 --- a/packages/react-reconciler/src/ReactFiberThrow.new.js +++ b/packages/react-reconciler/src/ReactFiberThrow.new.js @@ -185,6 +185,8 @@ 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.new.js b/packages/react-reconciler/src/ReactFiberWorkLoop.new.js index 17346a73660f7..f7a4f39bb1cba 100644 --- a/packages/react-reconciler/src/ReactFiberWorkLoop.new.js +++ b/packages/react-reconciler/src/ReactFiberWorkLoop.new.js @@ -13,14 +13,15 @@ import type {Lanes, Lane} from './ReactFiberLane'; import type {ReactPriorityLevel} from './ReactInternalTypes'; import type {Interaction} from 'scheduler/src/Tracing'; import type {SuspenseState} from './ReactFiberSuspenseComponent.new'; +import type {Effect as HookEffect} from './ReactFiberHooks.new'; import type {StackCursor} from './ReactFiberStack.new'; -import type {FunctionComponentUpdateQueue} from './ReactFiberHooks.new'; import { warnAboutDeprecatedLifecycles, enableSuspenseServerRenderer, replayFailedUnitOfWorkWithInvokeGuardedCallback, enableProfilerTimer, + enableProfilerCommitHooks, enableProfilerNestedUpdatePhase, enableProfilerNestedUpdateScheduledHook, enableSchedulerTracing, @@ -29,7 +30,7 @@ import { decoupleUpdatePriorityFromScheduler, enableDebugTracing, enableSchedulingProfiler, - enableDoubleInvokingEffects, + enableScopeAPI, } from 'shared/ReactFeatureFlags'; import ReactSharedInternals from 'shared/ReactSharedInternals'; import invariant from 'shared/invariant'; @@ -49,10 +50,6 @@ import { flushSyncCallbackQueue, scheduleSyncCallback, } from './SchedulerWithReactIntegration.new'; -import { - NoFlags as NoHookEffect, - Passive as HookPassive, -} from './ReactHookEffectTags'; import { logCommitStarted, logCommitStopped, @@ -81,11 +78,13 @@ import * as Scheduler from 'scheduler'; import {__interactionsRef, __subscriberRef} from 'scheduler/tracing'; import { + prepareForCommit, resetAfterCommit, scheduleTimeout, cancelTimeout, noTimeout, warnsIfNotActing, + beforeActiveInstanceBlur, afterActiveInstanceBlur, clearContainer, } from './ReactFiberHostConfig'; @@ -111,20 +110,29 @@ import { ForwardRef, MemoComponent, SimpleMemoComponent, + OffscreenComponent, + LegacyHiddenComponent, + ScopeComponent, Profiler, } from './ReactWorkTags'; import {LegacyRoot} from './ReactRootTags'; import { NoFlags, + PerformedWork, Placement, - PassiveStatic, + Update, + PlacementAndUpdate, + Deletion, + Ref, + ContentReset, + Snapshot, + Callback, + Passive, + PassiveUnmountPendingDev, Incomplete, HostEffectMask, Hydrating, - BeforeMutationMask, - MutationMask, - LayoutMask, - PassiveMask, + HydratingAndUpdate, } from './ReactFiberFlags'; import { NoLanePriority, @@ -136,6 +144,7 @@ import { NoLane, SyncLane, SyncBatchedLane, + OffscreenLane, NoTimestamp, findUpdateLane, findTransitionLane, @@ -175,12 +184,16 @@ import { createClassErrorUpdate, } from './ReactFiberThrow.new'; import { - commitBeforeMutationEffects, - commitMutationEffects, - commitLayoutEffects, - commitPassiveMountEffects, - commitPassiveUnmountEffects, - commitDoubleInvokeEffectsInDEV, + commitBeforeMutationLifeCycles as commitBeforeMutationEffectOnFiber, + commitLifeCycles as commitLayoutEffectOnFiber, + commitPlacement, + commitWork, + commitDeletion, + commitDetachRef, + commitAttachRef, + commitPassiveEffectDurations, + commitResetTextContent, + isSuspenseBoundaryBeingHidden, } from './ReactFiberCommitWork.new'; import {enqueueUpdate} from './ReactUpdateQueue.new'; import {resetContextDependencies} from './ReactFiberNewContext.new'; @@ -199,7 +212,9 @@ import { import { markNestedUpdateScheduled, recordCommitTime, + recordPassiveEffectDuration, resetNestedUpdateFlag, + startPassiveEffectTimer, startProfilerTimer, stopProfilerTimerIfRunningAndRecordDelta, syncNestedUpdateFlag, @@ -224,6 +239,7 @@ import {onCommitRoot as onCommitRootTestSelector} from './ReactTestSelectors'; // Used by `act` import enqueueTask from 'shared/enqueueTask'; +import {doesFiberContain} from './ReactFiberTreeReflection'; const ceil = Math.ceil; @@ -261,10 +277,6 @@ let workInProgress: Fiber | null = null; // The lanes we're rendering let workInProgressRootRenderLanes: Lanes = NoLanes; -// Only used when enableProfilerNestedUpdateScheduledHook is true; -// to track which root is currently committing layout effects. -let rootCommittingMutationOrLayoutEffects: FiberRoot | null = null; - // Stack that allows components to change the render lanes for its subtree // This is a superset of the lanes we started working on at the root. The only // case where it's different from `workInProgressRootRenderLanes` is when we @@ -273,7 +285,7 @@ let rootCommittingMutationOrLayoutEffects: FiberRoot | null = null; // // Most things in the work loop should deal with workInProgressRootRenderLanes. // Most things in begin/complete phases should deal with subtreeRenderLanes. -export let subtreeRenderLanes: Lanes = NoLanes; +let subtreeRenderLanes: Lanes = NoLanes; const subtreeRenderLanesCursor: StackCursor = createCursor(NoLanes); // Whether to root completed, errored, suspended, etc. @@ -315,13 +327,22 @@ export function getRenderTargetTime(): number { return workInProgressRootRenderTargetTime; } +let nextEffect: Fiber | null = null; let hasUncaughtError = false; let firstUncaughtError = null; let legacyErrorBoundariesThatAlreadyFailed: Set | null = null; +// Only used when enableProfilerNestedUpdateScheduledHook is true; +// to track which root is currently committing layout effects. +let rootCommittingMutationOrLayoutEffects: FiberRoot | 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; @@ -351,6 +372,9 @@ 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; } @@ -1717,6 +1741,47 @@ 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, @@ -1753,10 +1818,9 @@ function completeUnitOfWork(unitOfWork: Fiber): void { } if (returnFiber !== null) { - // Mark the parent fiber as incomplete + // Mark the parent fiber as incomplete and clear its effect list. + returnFiber.firstEffect = returnFiber.lastEffect = null; returnFiber.flags |= Incomplete; - returnFiber.subtreeFlags = NoFlags; - returnFiber.deletions = null; } } @@ -1778,6 +1842,81 @@ 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( @@ -1871,37 +2010,25 @@ function commitRootImpl(root, renderPriorityLevel) { // times out. } - // 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; - }); + // 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; } - // 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) { + if (firstEffect !== null) { let previousLanePriority; if (decoupleUpdatePriorityFromScheduler) { previousLanePriority = getCurrentUpdateLanePriority(); @@ -1922,10 +2049,32 @@ 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. - const shouldFireAfterActiveInstanceBlur = commitBeforeMutationEffects( - root, - finishedWork, - ); + 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; if (enableProfilerTimer) { // Mark the current commit time to be shared by all Profilers in this @@ -1934,11 +2083,38 @@ function commitRootImpl(root, renderPriorityLevel) { } if (enableProfilerTimer && enableProfilerNestedUpdateScheduledHook) { + // Track the root here, rather than in commitLayoutEffects(), because of ref setters. + // Updates scheduled during ref detachment should also be flagged. rootCommittingMutationOrLayoutEffects = root; } // The next phase is the mutation phase, where we mutate the host tree. - commitMutationEffects(finishedWork, root, renderPriorityLevel); + 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); if (shouldFireAfterActiveInstanceBlur) { afterActiveInstanceBlur(); @@ -1954,26 +2130,28 @@ 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. - - if (__DEV__) { - if (enableDebugTracing) { - logLayoutEffectsStarted(lanes); + 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 (enableSchedulingProfiler) { - markLayoutEffectsStarted(lanes); - } + } while (nextEffect !== null); - commitLayoutEffects(finishedWork, root); - - if (__DEV__) { - if (enableDebugTracing) { - logLayoutEffectsStopped(); - } - } - if (enableSchedulingProfiler) { - markLayoutEffectsStopped(); - } + nextEffect = null; if (enableProfilerTimer && enableProfilerNestedUpdateScheduledHook) { rootCommittingMutationOrLayoutEffects = null; @@ -2003,6 +2181,30 @@ 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; @@ -2028,14 +2230,8 @@ function commitRootImpl(root, renderPriorityLevel) { legacyErrorBoundariesThatAlreadyFailed = null; } - if (__DEV__ && enableDoubleInvokingEffects) { - if (!rootDoesHavePassiveEffects) { - commitDoubleInvokeEffectsInDEV(root.current, false); - } - } - if (enableSchedulerTracing) { - if (!rootDoesHavePassiveEffects) { + if (!rootDidHavePassiveEffects) { // 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, @@ -2112,6 +2308,181 @@ 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) { + // 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) { @@ -2137,6 +2508,59 @@ 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; @@ -2176,25 +2600,137 @@ 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); - if (__DEV__) { - if (enableDebugTracing) { - logPassiveEffectsStopped(); + // 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 (enableSchedulingProfiler) { - markPassiveEffectsStopped(); + 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); + } + } } - if (__DEV__ && enableDoubleInvokingEffects) { - commitDoubleInvokeEffectsInDEV(root.current, true); + // 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); + } + effect = nextNextEffect; } - if (__DEV__) { - isFlushingPassiveEffects = false; + 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 (enableSchedulerTracing) { @@ -2202,6 +2738,20 @@ function flushPassiveEffectsImpl() { finishPendingInteractions(root, lanes); } + if (__DEV__) { + isFlushingPassiveEffects = false; + } + + if (__DEV__) { + if (enableDebugTracing) { + logPassiveEffectsStopped(); + } + } + + if (enableSchedulingProfiler) { + markPassiveEffectsStopped(); + } + executionContext = prevExecutionContext; flushSyncCallbackQueue(); @@ -2542,24 +3092,10 @@ function warnAboutUpdateOnUnmountedFiberInDEV(fiber) { 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); - } - } + // 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; } // We show the whole stack but dedupe on the top component's name because @@ -3249,3 +3785,8 @@ 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.new.js b/packages/react-reconciler/src/SchedulerWithReactIntegration.new.js index f73ae2f8998f4..606a90252077e 100644 --- a/packages/react-reconciler/src/SchedulerWithReactIntegration.new.js +++ b/packages/react-reconciler/src/SchedulerWithReactIntegration.new.js @@ -165,13 +165,13 @@ export function cancelCallback(callbackNode: mixed) { } } -export function flushSyncCallbackQueue(): boolean { +export function flushSyncCallbackQueue() { if (immediateQueueCallbackNode !== null) { const node = immediateQueueCallbackNode; immediateQueueCallbackNode = null; Scheduler_cancelCallback(node); } - return flushSyncCallbackQueueImpl(); + flushSyncCallbackQueueImpl(); } function flushSyncCallbackQueueImpl() { @@ -237,8 +237,5 @@ function flushSyncCallbackQueueImpl() { isFlushingSyncQueue = false; } } - return true; - } else { - return false; } } diff --git a/packages/react-reconciler/src/__tests__/ReactDoubleInvokeEvents-test.internal.js b/packages/react-reconciler/src/__tests__/ReactDoubleInvokeEvents-test.js similarity index 96% rename from packages/react-reconciler/src/__tests__/ReactDoubleInvokeEvents-test.internal.js rename to packages/react-reconciler/src/__tests__/ReactDoubleInvokeEvents-test.js index e5136af4e2406..5b6abbc4f6593 100644 --- a/packages/react-reconciler/src/__tests__/ReactDoubleInvokeEvents-test.internal.js +++ b/packages/react-reconciler/src/__tests__/ReactDoubleInvokeEvents-test.js @@ -10,7 +10,6 @@ 'use strict'; let React; -let ReactFeatureFlags; let ReactTestRenderer; let Scheduler; let act; @@ -19,13 +18,20 @@ describe('ReactDoubleInvokeEvents', () => { beforeEach(() => { jest.resetModules(); React = require('react'); - ReactFeatureFlags = require('shared/ReactFeatureFlags'); ReactTestRenderer = require('react-test-renderer'); Scheduler = require('scheduler'); - ReactFeatureFlags.enableDoubleInvokingEffects = __VARIANT__; act = ReactTestRenderer.unstable_concurrentAct; }); + function supportsDoubleInvokeEffects() { + return gate( + flags => + flags.build === 'development' && + flags.enableDoubleInvokingEffects && + flags.dfsEffectsRefactor, + ); + } + it('should not double invoke effects in legacy mode', () => { function App({text}) { React.useEffect(() => { @@ -73,7 +79,7 @@ describe('ReactDoubleInvokeEvents', () => { }); }); - if (__DEV__ && __VARIANT__) { + if (supportsDoubleInvokeEffects()) { expect(Scheduler).toHaveYielded([ 'useLayoutEffect mount', 'useEffect mount', @@ -132,7 +138,7 @@ describe('ReactDoubleInvokeEvents', () => { }); }); - if (__DEV__ && __VARIANT__) { + if (supportsDoubleInvokeEffects()) { expect(Scheduler).toHaveYielded([ 'useEffect One mount', 'useEffect Two mount', @@ -193,7 +199,7 @@ describe('ReactDoubleInvokeEvents', () => { }); }); - if (__DEV__ && __VARIANT__) { + if (supportsDoubleInvokeEffects()) { expect(Scheduler).toHaveYielded([ 'useLayoutEffect One mount', 'useLayoutEffect Two mount', @@ -250,7 +256,7 @@ describe('ReactDoubleInvokeEvents', () => { }); }); - if (__DEV__ && __VARIANT__) { + if (supportsDoubleInvokeEffects()) { expect(Scheduler).toHaveYielded([ 'useLayoutEffect mount', 'useEffect mount', @@ -308,7 +314,7 @@ describe('ReactDoubleInvokeEvents', () => { ReactTestRenderer.create(, {unstable_isConcurrent: true}); }); - if (__DEV__ && __VARIANT__) { + if (supportsDoubleInvokeEffects()) { expect(Scheduler).toHaveYielded([ 'componentDidMount', 'componentWillUnmount', @@ -345,7 +351,7 @@ describe('ReactDoubleInvokeEvents', () => { }); }); - if (__DEV__ && __VARIANT__) { + if (supportsDoubleInvokeEffects()) { expect(Scheduler).toHaveYielded([ 'componentDidMount', 'componentWillUnmount', @@ -420,7 +426,7 @@ describe('ReactDoubleInvokeEvents', () => { }); }); - if (__DEV__ && __VARIANT__) { + if (supportsDoubleInvokeEffects()) { expect(Scheduler).toHaveYielded([ 'mount', 'useLayoutEffect mount', @@ -485,7 +491,7 @@ describe('ReactDoubleInvokeEvents', () => { ReactTestRenderer.create(, {unstable_isConcurrent: true}); }); - if (__DEV__ && __VARIANT__) { + if (supportsDoubleInvokeEffects()) { expect(Scheduler).toHaveYielded([ 'App useLayoutEffect mount', 'App useEffect mount', @@ -505,7 +511,7 @@ describe('ReactDoubleInvokeEvents', () => { _setShowChild(true); }); - if (__DEV__ && __VARIANT__) { + if (supportsDoubleInvokeEffects()) { expect(Scheduler).toHaveYielded([ 'App useLayoutEffect unmount', 'Child useLayoutEffect mount', @@ -573,7 +579,7 @@ describe('ReactDoubleInvokeEvents', () => { }); }); - if (__DEV__ && __VARIANT__) { + if (supportsDoubleInvokeEffects()) { expect(Scheduler).toHaveYielded([ 'componentDidMount', 'useLayoutEffect mount', diff --git a/packages/react/src/__tests__/ReactDOMTracing-test.internal.js b/packages/react/src/__tests__/ReactDOMTracing-test.internal.js index 7be6513a5737d..5026605672fcb 100644 --- a/packages/react/src/__tests__/ReactDOMTracing-test.internal.js +++ b/packages/react/src/__tests__/ReactDOMTracing-test.internal.js @@ -152,7 +152,7 @@ describe('ReactDOMTracing', () => { onInteractionScheduledWorkCompleted, ).toHaveBeenLastNotifiedOfInteraction(interaction); - if (gate(flags => flags.new)) { + if (gate(flags => flags.dfsEffectsRefactor)) { expect(onRender).toHaveBeenCalledTimes(3); } else { // TODO: This is 4 instead of 3 because this update was scheduled at @@ -310,7 +310,7 @@ describe('ReactDOMTracing', () => { expect( onInteractionScheduledWorkCompleted, ).toHaveBeenLastNotifiedOfInteraction(interaction); - if (gate(flags => flags.new)) { + if (gate(flags => flags.dfsEffectsRefactor)) { expect(onRender).toHaveBeenCalledTimes(3); } else { // TODO: This is 4 instead of 3 because this update was scheduled at diff --git a/packages/react/src/__tests__/ReactProfiler-test.internal.js b/packages/react/src/__tests__/ReactProfiler-test.internal.js index 831861941a24d..a87c83e9c180e 100644 --- a/packages/react/src/__tests__/ReactProfiler-test.internal.js +++ b/packages/react/src/__tests__/ReactProfiler-test.internal.js @@ -368,7 +368,7 @@ describe('Profiler', () => { renderer.update(); - if (gate(flags => flags.new)) { + if (gate(flags => flags.dfsEffectsRefactor)) { // 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(); @@ -4292,7 +4292,7 @@ describe('Profiler', () => { // 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)) { + if (gate(flags => flags.dfsEffectsRefactor)) { expect(Scheduler).toFlushAndYield([]); } else { expect(Scheduler).toFlushAndYield(['onPostCommit']); @@ -4783,7 +4783,8 @@ describe('Profiler', () => { }); if (__DEV__) { - // @gate new + // @gate dfsEffectsRefactor + // @gate enableDoubleInvokingEffects it('double invoking does not disconnect wrapped async work', () => { ReactFeatureFlags.enableDoubleInvokingEffects = true; diff --git a/scripts/jest/TestFlags.js b/scripts/jest/TestFlags.js index 6a2be742e2960..f869620f03290 100644 --- a/scripts/jest/TestFlags.js +++ b/scripts/jest/TestFlags.js @@ -44,6 +44,10 @@ const environmentFlags = { // Use this for tests that are known to be broken. FIXME: false, + + // Turn this flag back on (or delete) once the effect list is removed in favor + // of a depth-first traversal using `subtreeTags`. + dfsEffectsRefactor: false, }; function getTestFlags() {