diff --git a/packages/react-reconciler/src/ReactFiberCommitWork.new.js b/packages/react-reconciler/src/ReactFiberCommitWork.new.js index 35115140041c7..767ee05b39b0c 100644 --- a/packages/react-reconciler/src/ReactFiberCommitWork.new.js +++ b/packages/react-reconciler/src/ReactFiberCommitWork.new.js @@ -75,9 +75,10 @@ import { Hydrating, HydratingAndUpdate, Passive, + BeforeMutationMask, MutationMask, - PassiveMask, LayoutMask, + PassiveMask, PassiveUnmountPendingDev, } from './ReactFiberFlags'; import getComponentName from 'shared/getComponentName'; @@ -128,6 +129,8 @@ import { commitHydratedSuspenseInstance, clearContainer, prepareScopeUpdate, + prepareForCommit, + beforeActiveInstanceBlur, } from './ReactFiberHostConfig'; import { captureCommitPhaseError, @@ -144,6 +147,7 @@ import { Passive as HookPassive, } from './ReactHookEffectTags'; import {didWarnAboutReassigningProps} from './ReactFiberBeginWork.new'; +import {doesFiberContain} from './ReactFiberTreeReflection'; let didWarnAboutUndefinedSnapshotBeforeUpdate: Set | null = null; if (__DEV__) { @@ -259,18 +263,114 @@ function safelyCallDestroy(current: Fiber, destroy: () => void) { } } -function commitBeforeMutationLifeCycles( - current: Fiber | null, - finishedWork: Fiber, -): void { - switch (finishedWork.tag) { - case FunctionComponent: - case ForwardRef: - case SimpleMemoComponent: { +let focusedInstanceHandle: null | Fiber = null; +let shouldFireAfterActiveInstanceBlur: boolean = false; + +export function commitBeforeMutationEffects( + root: FiberRoot, + firstChild: Fiber, +) { + focusedInstanceHandle = prepareForCommit(root.containerInfo); + + nextEffect = firstChild; + commitBeforeMutationEffects_begin(); + + // We no longer need to track the active instance fiber + const shouldFire = shouldFireAfterActiveInstanceBlur; + shouldFireAfterActiveInstanceBlur = false; + focusedInstanceHandle = null; + + return shouldFire; +} + +function commitBeforeMutationEffects_begin() { + while (nextEffect !== null) { + const fiber = nextEffect; + + // TODO: Should wrap this in flags check, too, as optimization + const deletions = fiber.deletions; + if (deletions !== null) { + for (let i = 0; i < deletions.length; i++) { + const deletion = deletions[i]; + commitBeforeMutationEffectsDeletion(deletion); + } + } + + const child = fiber.child; + if ( + (fiber.subtreeFlags & BeforeMutationMask) !== NoFlags && + child !== null + ) { + ensureCorrectReturnPointer(child, fiber); + nextEffect = child; + } else { + commitBeforeMutationEffects_complete(); + } + } +} + +function commitBeforeMutationEffects_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); + } + } + + const sibling = fiber.sibling; + if (sibling !== null) { + ensureCorrectReturnPointer(sibling, fiber.return); + nextEffect = sibling; return; } - case ClassComponent: { - if (finishedWork.flags & Snapshot) { + + 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. + // TODO: Move this out of the hot path using a dedicated effect tag. + if ( + 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 (current !== null) { const prevProps = current.memoizedProps; const prevState = current.memoizedState; @@ -324,30 +424,43 @@ function commitBeforeMutationLifeCycles( } instance.__reactInternalSnapshotBeforeUpdate = snapshot; } + break; } - return; - } - case HostRoot: { - if (supportsMutation) { - if (finishedWork.flags & Snapshot) { + case HostRoot: { + if (supportsMutation) { const root = finishedWork.stateNode; clearContainer(root.containerInfo); } + break; + } + 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.', + ); } - return; } - case HostComponent: - case HostText: - case HostPortal: - case IncompleteClassComponent: - // Nothing to do for these component types - return; + + resetCurrentDebugFiberInDEV(); + } +} + +function commitBeforeMutationEffectsDeletion(deletion: Fiber) { + // TODO (effects) It would be nice to avoid calling doesFiberContain() + // Maybe we can repurpose one of the subtreeFlags positions for this instead? + // Use it to store which part of the tree the focused instance is in? + // This assumes we can safely determine that instance during the "render" phase. + if (doesFiberContain(deletion, ((focusedInstanceHandle: any): Fiber))) { + shouldFireAfterActiveInstanceBlur = true; + beforeActiveInstanceBlur(deletion); } - 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(flags: HookFlags, finishedWork: Fiber) { @@ -2353,7 +2466,6 @@ function ensureCorrectReturnPointer(fiber, expectedReturnFiber) { } export { - commitBeforeMutationLifeCycles, commitResetTextContent, commitPlacement, commitDeletion, diff --git a/packages/react-reconciler/src/ReactFiberFlags.js b/packages/react-reconciler/src/ReactFiberFlags.js index 518abbee11050..f6778178d7544 100644 --- a/packages/react-reconciler/src/ReactFiberFlags.js +++ b/packages/react-reconciler/src/ReactFiberFlags.js @@ -60,6 +60,9 @@ export const MountPassiveDev = /* */ 0b10000000000000000000; // don't contain effects, by checking subtreeFlags. export const BeforeMutationMask = + // TODO: Remove Update flag from before mutation phase by re-landing Visiblity + // flag logic (see #20043) + Update | Snapshot | (enableCreateEventHandleAPI ? // createEventHandle needs to visit deleted and hidden trees to diff --git a/packages/react-reconciler/src/ReactFiberWorkLoop.new.js b/packages/react-reconciler/src/ReactFiberWorkLoop.new.js index a04b522e57ba0..e51542c9444d3 100644 --- a/packages/react-reconciler/src/ReactFiberWorkLoop.new.js +++ b/packages/react-reconciler/src/ReactFiberWorkLoop.new.js @@ -83,13 +83,11 @@ import * as Scheduler from 'scheduler'; import {__interactionsRef, __subscriberRef} from 'scheduler/tracing'; import { - prepareForCommit, resetAfterCommit, scheduleTimeout, cancelTimeout, noTimeout, warnsIfNotActing, - beforeActiveInstanceBlur, afterActiveInstanceBlur, clearContainer, } from './ReactFiberHostConfig'; @@ -122,9 +120,7 @@ import { NoFlags, PerformedWork, Placement, - Deletion, ChildDeletion, - Snapshot, Passive, PassiveStatic, Incomplete, @@ -181,11 +177,10 @@ import { createClassErrorUpdate, } from './ReactFiberThrow.new'; import { - commitBeforeMutationLifeCycles as commitBeforeMutationEffectOnFiber, + commitBeforeMutationEffects, commitLayoutEffects, commitMutationEffects, commitPassiveEffectDurations, - isSuspenseBoundaryBeingHidden, commitPassiveMountEffects, commitPassiveUnmountEffects, detachFiberAfterEffects, @@ -232,7 +227,6 @@ import {onCommitRoot as onCommitRootTestSelector} from './ReactTestSelectors'; // Used by `act` import enqueueTask from 'shared/enqueueTask'; -import {doesFiberContain} from './ReactFiberTreeReflection'; const ceil = Math.ceil; @@ -363,9 +357,6 @@ let currentEventPendingLanes: Lanes = NoLanes; // We warn about state updates for unmounted components differently in this case. let isFlushingPassiveEffects = false; -let focusedInstanceHandle: null | Fiber = null; -let shouldFireAfterActiveInstanceBlur: boolean = false; - export function getWorkInProgressRoot(): FiberRoot | null { return workInProgressRoot; } @@ -1941,6 +1932,24 @@ function commitRootImpl(root, renderPriorityLevel) { firstEffect = finishedWork.firstEffect; } + // If there are pending passive effects, schedule a callback to process them. + // Do this as early as possible, so it is queued before anything else that + // might get scheduled in the commit phase. (See #16714.) + // TODO: Delete all other places that schedule the passive effect callback + // They're redundant. + if ( + (finishedWork.subtreeFlags & Passive) !== NoFlags || + (finishedWork.flags & Passive) !== NoFlags + ) { + if (!rootDoesHavePassiveEffects) { + rootDoesHavePassiveEffects = true; + scheduleCallback(NormalSchedulerPriority, () => { + flushPassiveEffects(); + return null; + }); + } + } + if (firstEffect !== null) { let previousLanePriority; if (decoupleUpdatePriorityFromScheduler) { @@ -1962,32 +1971,10 @@ function commitRootImpl(root, renderPriorityLevel) { // The first phase a "before mutation" phase. We use this phase to read the // state of the host tree right before we mutate it. This is where // getSnapshotBeforeUpdate is called. - focusedInstanceHandle = prepareForCommit(root.containerInfo); - shouldFireAfterActiveInstanceBlur = false; - - nextEffect = firstEffect; - do { - if (__DEV__) { - invokeGuardedCallback(null, commitBeforeMutationEffects, null); - if (hasCaughtError()) { - invariant(nextEffect !== null, 'Should be working on an effect.'); - const error = clearCaughtError(); - captureCommitPhaseError(nextEffect, error); - nextEffect = nextEffect.nextEffect; - } - } else { - try { - commitBeforeMutationEffects(); - } catch (error) { - invariant(nextEffect !== null, 'Should be working on an effect.'); - captureCommitPhaseError(nextEffect, error); - nextEffect = nextEffect.nextEffect; - } - } - } while (nextEffect !== null); - - // We no longer need to track the active instance fiber - focusedInstanceHandle = null; + const shouldFireAfterActiveInstanceBlur = commitBeforeMutationEffects( + root, + finishedWork, + ); if (enableProfilerTimer) { // Mark the current commit time to be shared by all Profilers in this @@ -2204,52 +2191,6 @@ function commitRootImpl(root, renderPriorityLevel) { return null; } -function commitBeforeMutationEffects() { - while (nextEffect !== null) { - const current = nextEffect.alternate; - - if (!shouldFireAfterActiveInstanceBlur && focusedInstanceHandle !== null) { - if ((nextEffect.flags & Deletion) !== NoFlags) { - if (doesFiberContain(nextEffect, focusedInstanceHandle)) { - shouldFireAfterActiveInstanceBlur = true; - beforeActiveInstanceBlur(nextEffect); - } - } else { - // TODO: Move this out of the hot path using a dedicated effect tag. - if ( - nextEffect.tag === SuspenseComponent && - isSuspenseBoundaryBeingHidden(current, nextEffect) && - doesFiberContain(nextEffect, focusedInstanceHandle) - ) { - shouldFireAfterActiveInstanceBlur = true; - beforeActiveInstanceBlur(nextEffect); - } - } - } - - const flags = nextEffect.flags; - if ((flags & Snapshot) !== NoFlags) { - setCurrentDebugFiberInDEV(nextEffect); - - commitBeforeMutationEffectOnFiber(current, nextEffect); - - resetCurrentDebugFiberInDEV(); - } - if ((flags & Passive) !== NoFlags) { - // If there are passive effects, schedule a callback to flush at - // the earliest opportunity. - if (!rootDoesHavePassiveEffects) { - rootDoesHavePassiveEffects = true; - scheduleCallback(NormalSchedulerPriority, () => { - flushPassiveEffects(); - return null; - }); - } - } - nextEffect = nextEffect.nextEffect; - } -} - export function flushPassiveEffects(): boolean { // Returns whether passive effects were flushed. if (pendingPassiveEffectsRenderPriority !== NoSchedulerPriority) {