diff --git a/packages/react-reconciler/src/ReactChildFiber.new.js b/packages/react-reconciler/src/ReactChildFiber.new.js index 8dd26b660c339..d145a2e57e278 100644 --- a/packages/react-reconciler/src/ReactChildFiber.new.js +++ b/packages/react-reconciler/src/ReactChildFiber.new.js @@ -13,7 +13,12 @@ import type {Fiber} from './ReactInternalTypes'; import type {Lanes} from './ReactFiberLane'; import getComponentName from 'shared/getComponentName'; -import {Deletion, ChildDeletion, Placement} from './ReactFiberFlags'; +import { + Deletion, + ChildDeletion, + Placement, + StaticMask, +} from './ReactFiberFlags'; import { getIteratorFn, REACT_ELEMENT_TYPE, @@ -269,7 +274,7 @@ function ChildReconciler(shouldTrackSideEffects) { returnFiber.firstEffect = returnFiber.lastEffect = childToDelete; } childToDelete.nextEffect = null; - childToDelete.flags = Deletion; + childToDelete.flags = (childToDelete.flags & StaticMask) | Deletion; let deletions = returnFiber.deletions; if (deletions === null) { @@ -353,7 +358,7 @@ function ChildReconciler(shouldTrackSideEffects) { const oldIndex = current.index; if (oldIndex < lastPlacedIndex) { // This is a move. - newFiber.flags = Placement; + newFiber.flags |= Placement; return lastPlacedIndex; } else { // This item can stay in place. @@ -361,7 +366,7 @@ function ChildReconciler(shouldTrackSideEffects) { } } else { // This is an insertion. - newFiber.flags = Placement; + newFiber.flags |= Placement; return lastPlacedIndex; } } @@ -370,7 +375,7 @@ function ChildReconciler(shouldTrackSideEffects) { // This is simpler for the single child case. We only need to do a // placement for inserting new children. if (shouldTrackSideEffects && newFiber.alternate === null) { - newFiber.flags = Placement; + newFiber.flags |= Placement; } return newFiber; } diff --git a/packages/react-reconciler/src/ReactFiberBeginWork.new.js b/packages/react-reconciler/src/ReactFiberBeginWork.new.js index 3a1139b9c19de..9c120b52eccb5 100644 --- a/packages/react-reconciler/src/ReactFiberBeginWork.new.js +++ b/packages/react-reconciler/src/ReactFiberBeginWork.new.js @@ -2007,7 +2007,8 @@ function updateSuspensePrimaryChildren( if (currentFallbackChildFragment !== null) { // Delete the fallback child fragment currentFallbackChildFragment.nextEffect = null; - currentFallbackChildFragment.flags = Deletion; + currentFallbackChildFragment.flags = + (currentFallbackChildFragment.flags & StaticMask) | Deletion; workInProgress.firstEffect = workInProgress.lastEffect = currentFallbackChildFragment; let deletions = workInProgress.deletions; if (deletions === null) { @@ -2998,7 +2999,7 @@ function remountFiber( returnFiber.firstEffect = returnFiber.lastEffect = current; } current.nextEffect = null; - current.flags = Deletion; + current.flags = (current.flags & StaticMask) | Deletion; let deletions = returnFiber.deletions; if (deletions === null) { diff --git a/packages/react-reconciler/src/ReactFiberCommitWork.new.js b/packages/react-reconciler/src/ReactFiberCommitWork.new.js index f16fb99435519..868f523d8c282 100644 --- a/packages/react-reconciler/src/ReactFiberCommitWork.new.js +++ b/packages/react-reconciler/src/ReactFiberCommitWork.new.js @@ -24,6 +24,7 @@ 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 {unstable_wrap as Schedule_tracing_wrap} from 'scheduler/tracing'; import { @@ -65,11 +66,20 @@ import { NoFlags, ContentReset, Placement, + ChildDeletion, Snapshot, Update, + Passive, + PassiveStatic, + PassiveMask, + PassiveUnmountPendingDev, } from './ReactFiberFlags'; import getComponentName from 'shared/getComponentName'; import invariant from 'shared/invariant'; +import { + resetCurrentFiber as resetCurrentDebugFiberInDEV, + setCurrentFiber as setCurrentDebugFiberInDEV, +} from './ReactCurrentFiber'; import {onCommitUnmount} from './ReactFiberDevToolsHook.new'; import {resolveDefaultProps} from './ReactFiberLazyComponent.new'; @@ -78,6 +88,8 @@ import { getCommitTime, recordLayoutEffectDuration, startLayoutEffectTimer, + recordPassiveEffectDuration, + startPassiveEffectTimer, } from './ReactProfilerTimer.new'; import {ProfileMode} from './ReactTypeOfMode'; import {commitUpdateQueue} from './ReactUpdateQueue.new'; @@ -134,6 +146,8 @@ if (__DEV__) { const PossiblyWeakSet = typeof WeakSet === 'function' ? WeakSet : Set; +let nextEffect: Fiber | null = null; + const callComponentWillUnmountWithTimer = function(current, instance) { instance.props = current.memoizedProps; instance.state = current.memoizedState; @@ -330,19 +344,19 @@ function commitBeforeMutationLifeCycles( ); } -function commitHookEffectListUnmount(tag: number, finishedWork: Fiber) { +function commitHookEffectListUnmount(flags: HookFlags, finishedWork: Fiber) { const updateQueue: FunctionComponentUpdateQueue | null = (finishedWork.updateQueue: any); const lastEffect = updateQueue !== null ? updateQueue.lastEffect : null; if (lastEffect !== null) { const firstEffect = lastEffect.next; let effect = firstEffect; do { - if ((effect.tag & tag) === tag) { + if ((effect.tag & flags) === flags) { // Unmount const destroy = effect.destroy; effect.destroy = undefined; if (destroy !== undefined) { - destroy(); + safelyCallDestroy(finishedWork, destroy); } } effect = effect.next; @@ -1059,29 +1073,42 @@ function commitNestedUnmounts( } function detachFiberMutation(fiber: Fiber) { - // Cut off the return pointers to disconnect it from the tree. Ideally, we - // should clear the child pointer of the parent alternate to let this + // Cut off the return pointer to disconnect it from the tree. + // This enables us to detect and warn against state updates on an unmounted component. + // It also prevents events from bubbling from within disconnected components. + // + // Ideally, we should also clear the child pointer of the parent alternate to let this // get GC:ed but we don't know which for sure which parent is the current - // one so we'll settle for GC:ing the subtree of this child. This child - // itself will be GC:ed when the parent updates the next time. - // Note: we cannot null out sibling here, otherwise it can cause issues - // with findDOMNode and how it requires the sibling field to carry out - // traversal in a later effect. See PR #16820. We now clear the sibling - // field after effects, see: detachFiberAfterEffects. + // one so we'll settle for GC:ing the subtree of this child. + // This child itself will be GC:ed when the parent updates the next time. // - // Don't disconnect stateNode now; it will be detached in detachFiberAfterEffects. - // It may be required if the current component is an error boundary, - // and one of its descendants throws while unmounting a passive effect. - fiber.alternate = null; + // Note that we can't clear child or sibling pointers yet. + // They're needed for passive effects and for findDOMNode. + // We defer those fields, and all other cleanup, to the passive phase (see detachFiberAfterEffects). + const alternate = fiber.alternate; + if (alternate !== null) { + alternate.return = null; + fiber.alternate = null; + } + fiber.return = null; +} + +export 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.firstEffect = null; - fiber.lastEffect = null; fiber.memoizedProps = null; fiber.memoizedState = null; fiber.pendingProps = null; - fiber.return = null; + fiber.sibling = null; + fiber.stateNode = null; fiber.updateQueue = null; + fiber.nextEffect = null; + fiber.firstEffect = null; + fiber.lastEffect = null; + if (__DEV__) { fiber._debugOwner = null; } @@ -1797,6 +1824,265 @@ function commitResetTextContent(current: Fiber) { resetTextContent(current.stateNode); } +export function commitPassiveMountEffects( + root: FiberRoot, + firstChild: Fiber, +): void { + nextEffect = firstChild; + commitPassiveMountEffects_begin(firstChild, root); +} + +function commitPassiveMountEffects_begin(subtreeRoot: Fiber, root: FiberRoot) { + while (nextEffect !== null) { + const fiber = nextEffect; + const firstChild = fiber.child; + if ((fiber.subtreeFlags & PassiveMask) !== NoFlags && firstChild !== null) { + ensureCorrectReturnPointer(firstChild, fiber); + nextEffect = firstChild; + } else { + commitPassiveMountEffects_complete(subtreeRoot, root); + } + } +} + +function commitPassiveMountEffects_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) { + ensureCorrectReturnPointer(sibling, fiber.return); + nextEffect = sibling; + return; + } + + nextEffect = fiber.return; + } +} + +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; + } + } +} + +export function commitPassiveUnmountEffects(firstChild: Fiber): void { + nextEffect = firstChild; + commitPassiveUnmountEffects_begin(); +} + +function commitPassiveUnmountEffects_begin() { + while (nextEffect !== null) { + const fiber = nextEffect; + const child = fiber.child; + + if ((nextEffect.flags & ChildDeletion) !== NoFlags) { + const deletions = fiber.deletions; + if (deletions !== null) { + for (let i = 0; i < deletions.length; i++) { + const fiberToDelete = deletions[i]; + nextEffect = fiberToDelete; + commitPassiveUnmountEffectsInsideOfDeletedTree_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) { + ensureCorrectReturnPointer(child, fiber); + nextEffect = child; + } else { + commitPassiveUnmountEffects_complete(); + } + } +} + +function commitPassiveUnmountEffects_complete() { + while (nextEffect !== null) { + const fiber = nextEffect; + if ((fiber.flags & Passive) !== NoFlags) { + setCurrentDebugFiberInDEV(fiber); + commitPassiveUnmountOnFiber(fiber); + resetCurrentDebugFiberInDEV(); + } + + const sibling = fiber.sibling; + if (sibling !== null) { + ensureCorrectReturnPointer(sibling, fiber.return); + nextEffect = sibling; + return; + } + + nextEffect = fiber.return; + } +} + +function commitPassiveUnmountOnFiber(finishedWork: Fiber): void { + if (__DEV__) { + finishedWork.flags &= ~PassiveUnmountPendingDev; + const alternate = finishedWork.alternate; + if (alternate !== null) { + alternate.flags &= ~PassiveUnmountPendingDev; + } + } + + 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 commitPassiveUnmountEffectsInsideOfDeletedTree_begin( + deletedSubtreeRoot: Fiber, +) { + while (nextEffect !== null) { + const fiber = nextEffect; + const child = fiber.child; + if ((fiber.subtreeFlags & PassiveStatic) !== NoFlags && child !== null) { + ensureCorrectReturnPointer(child, fiber); + nextEffect = child; + } else { + commitPassiveUnmountEffectsInsideOfDeletedTree_complete( + deletedSubtreeRoot, + ); + } + } +} + +function commitPassiveUnmountEffectsInsideOfDeletedTree_complete( + deletedSubtreeRoot: Fiber, +) { + while (nextEffect !== null) { + const fiber = nextEffect; + if ((fiber.flags & PassiveStatic) !== NoFlags) { + setCurrentDebugFiberInDEV(fiber); + commitPassiveUnmountInsideDeletedTreeOnFiber(fiber); + resetCurrentDebugFiberInDEV(); + } + + if (fiber === deletedSubtreeRoot) { + nextEffect = null; + return; + } + + const sibling = fiber.sibling; + if (sibling !== null) { + ensureCorrectReturnPointer(sibling, fiber.return); + nextEffect = sibling; + return; + } + + nextEffect = fiber.return; + } +} + +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; + } + } +} + +let didWarnWrongReturnPointer = false; +function ensureCorrectReturnPointer(fiber, expectedReturnFiber) { + if (__DEV__) { + if (!didWarnWrongReturnPointer && fiber.return !== expectedReturnFiber) { + didWarnWrongReturnPointer = true; + console.error( + 'Internal React error: Return pointer is inconsistent ' + + 'with parent.', + ); + } + } + + // TODO: Remove this assignment once we're confident that it won't break + // anything, by checking the warning logs for the above invariant + fiber.return = expectedReturnFiber; +} + export { commitBeforeMutationLifeCycles, commitResetTextContent, diff --git a/packages/react-reconciler/src/ReactFiberHooks.new.js b/packages/react-reconciler/src/ReactFiberHooks.new.js index 6f2cb9ca400c1..076067f94f0f0 100644 --- a/packages/react-reconciler/src/ReactFiberHooks.new.js +++ b/packages/react-reconciler/src/ReactFiberHooks.new.js @@ -48,6 +48,7 @@ import {readContext} from './ReactFiberNewContext.new'; import { Update as UpdateEffect, Passive as PassiveEffect, + PassiveStatic as PassiveStaticEffect, } from './ReactFiberFlags'; import { HasEffect as HookHasEffect, @@ -1303,7 +1304,7 @@ function mountEffect( } } return mountEffectImpl( - UpdateEffect | PassiveEffect, + UpdateEffect | PassiveEffect | PassiveStaticEffect, HookPassive, create, deps, diff --git a/packages/react-reconciler/src/ReactFiberHydrationContext.new.js b/packages/react-reconciler/src/ReactFiberHydrationContext.new.js index d609204b16c85..5369c62e54e80 100644 --- a/packages/react-reconciler/src/ReactFiberHydrationContext.new.js +++ b/packages/react-reconciler/src/ReactFiberHydrationContext.new.js @@ -24,7 +24,13 @@ import { HostRoot, SuspenseComponent, } from './ReactWorkTags'; -import {Deletion, ChildDeletion, Placement, Hydrating} from './ReactFiberFlags'; +import { + Deletion, + ChildDeletion, + Placement, + Hydrating, + StaticMask, +} from './ReactFiberFlags'; import invariant from 'shared/invariant'; import { @@ -124,7 +130,7 @@ function deleteHydratableInstance( const childToDelete = createFiberFromHostInstanceForDeletion(); childToDelete.stateNode = instance; childToDelete.return = returnFiber; - childToDelete.flags = Deletion; + childToDelete.flags = (childToDelete.flags & StaticMask) | Deletion; // This might seem like it belongs on progressedFirstDeletion. However, // these children are not part of the reconciliation list of children. diff --git a/packages/react-reconciler/src/ReactFiberWorkLoop.new.js b/packages/react-reconciler/src/ReactFiberWorkLoop.new.js index d0bff91934359..077e617e8c165 100644 --- a/packages/react-reconciler/src/ReactFiberWorkLoop.new.js +++ b/packages/react-reconciler/src/ReactFiberWorkLoop.new.js @@ -15,6 +15,7 @@ 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, @@ -50,6 +51,10 @@ import { flushSyncCallbackQueue, scheduleSyncCallback, } from './SchedulerWithReactIntegration.new'; +import { + NoFlags as NoHookEffect, + Passive as HookPassive, +} from './ReactHookEffectTags'; import { logCommitStarted, logCommitStopped, @@ -121,16 +126,18 @@ import { Update, PlacementAndUpdate, Deletion, + ChildDeletion, Ref, ContentReset, Snapshot, Callback, Passive, - PassiveUnmountPendingDev, + PassiveStatic, Incomplete, HostEffectMask, Hydrating, HydratingAndUpdate, + StaticMask, } from './ReactFiberFlags'; import { NoLanePriority, @@ -191,6 +198,9 @@ import { commitPassiveEffectDurations, commitResetTextContent, isSuspenseBoundaryBeingHidden, + commitPassiveMountEffects, + commitPassiveUnmountEffects, + detachFiberAfterEffects, } from './ReactFiberCommitWork.new'; import {enqueueUpdate} from './ReactUpdateQueue.new'; import {resetContextDependencies} from './ReactFiberNewContext.new'; @@ -209,9 +219,7 @@ import { import { markNestedUpdateScheduled, recordCommitTime, - recordPassiveEffectDuration, resetNestedUpdateFlag, - startPassiveEffectTimer, startProfilerTimer, stopProfilerTimerIfRunningAndRecordDelta, syncNestedUpdateFlag, @@ -337,8 +345,6 @@ 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; @@ -1768,7 +1774,7 @@ function completeUnitOfWork(unitOfWork: Fiber): void { // 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 ((flags & ~StaticMask) > PerformedWork) { if (returnFiber.lastEffect !== null) { returnFiber.lastEffect.nextEffect = completedWork; } else { @@ -2115,13 +2121,21 @@ function commitRootImpl(root, 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. + // clear this in flushPassiveEffects + // TODO: We should always do this in the passive phase, by scheduling + // a passive callback for every deletion. nextEffect = firstEffect; while (nextEffect !== null) { const nextNextEffect = nextEffect.nextEffect; nextEffect.nextEffect = null; - if (nextEffect.flags & Deletion) { - detachFiberAfterEffects(nextEffect); + if (nextEffect.flags & ChildDeletion) { + const deletions = nextEffect.deletions; + if (deletions !== null) { + for (let i = 0; i < deletions.length; i++) { + const deletion = deletions[i]; + detachFiberAfterEffects(deletion); + } + } } nextEffect = nextNextEffect; } @@ -2475,7 +2489,6 @@ export function enqueuePendingPassiveHookEffectMount( fiber: Fiber, effect: HookEffect, ): void { - pendingPassiveHookEffectsMount.push(effect, fiber); if (!rootDoesHavePassiveEffects) { rootDoesHavePassiveEffects = true; scheduleCallback(NormalSchedulerPriority, () => { @@ -2489,14 +2502,6 @@ 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, () => { @@ -2506,11 +2511,6 @@ export function enqueuePendingPassiveHookEffectUnmount( } } -function invokePassiveEffectCreate(effect: HookEffect): void { - const create = effect.create; - effect.destroy = create(); -} - function flushPassiveEffectsImpl() { if (rootWithPendingPassiveEffects === null) { return false; @@ -2544,136 +2544,10 @@ function flushPassiveEffectsImpl() { executionContext |= CommitContext; const prevInteractions = pushInteractions(root); - // It's important that ALL pending passive effect destroy functions are called - // before ANY passive effect create functions are called. - // Otherwise effects in sibling components might interfere with each other. - // 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. - - // First pass: Destroy stale passive effects. - const unmountEffects = pendingPassiveHookEffectsUnmount; - pendingPassiveHookEffectsUnmount = []; - for (let i = 0; i < unmountEffects.length; i += 2) { - const effect = ((unmountEffects[i]: any): HookEffect); - const fiber = ((unmountEffects[i + 1]: any): Fiber); - const destroy = effect.destroy; - effect.destroy = undefined; - - if (__DEV__) { - fiber.flags &= ~PassiveUnmountPendingDev; - const alternate = fiber.alternate; - if (alternate !== null) { - alternate.flags &= ~PassiveUnmountPendingDev; - } - } - - if (typeof destroy === 'function') { - if (__DEV__) { - setCurrentDebugFiberInDEV(fiber); - if ( - enableProfilerTimer && - enableProfilerCommitHooks && - fiber.mode & ProfileMode - ) { - startPassiveEffectTimer(); - invokeGuardedCallback(null, destroy, null); - recordPassiveEffectDuration(fiber); - } else { - invokeGuardedCallback(null, destroy, null); - } - if (hasCaughtError()) { - invariant(fiber !== null, 'Should be working on an effect.'); - const error = clearCaughtError(); - captureCommitPhaseError(fiber, error); - } - resetCurrentDebugFiberInDEV(); - } else { - try { - if ( - enableProfilerTimer && - enableProfilerCommitHooks && - fiber.mode & ProfileMode - ) { - try { - startPassiveEffectTimer(); - destroy(); - } finally { - recordPassiveEffectDuration(fiber); - } - } else { - destroy(); - } - } catch (error) { - invariant(fiber !== null, 'Should be working on an effect.'); - captureCommitPhaseError(fiber, error); - } - } - } - } - // Second pass: Create new passive effects. - const mountEffects = pendingPassiveHookEffectsMount; - pendingPassiveHookEffectsMount = []; - for (let i = 0; i < mountEffects.length; i += 2) { - const effect = ((mountEffects[i]: any): HookEffect); - const fiber = ((mountEffects[i + 1]: any): Fiber); - if (__DEV__) { - setCurrentDebugFiberInDEV(fiber); - if ( - enableProfilerTimer && - enableProfilerCommitHooks && - fiber.mode & ProfileMode - ) { - startPassiveEffectTimer(); - invokeGuardedCallback(null, invokePassiveEffectCreate, null, effect); - recordPassiveEffectDuration(fiber); - } else { - invokeGuardedCallback(null, invokePassiveEffectCreate, null, effect); - } - if (hasCaughtError()) { - invariant(fiber !== null, 'Should be working on an effect.'); - const error = clearCaughtError(); - captureCommitPhaseError(fiber, error); - } - resetCurrentDebugFiberInDEV(); - } else { - try { - const create = effect.create; - if ( - enableProfilerTimer && - enableProfilerCommitHooks && - fiber.mode & ProfileMode - ) { - try { - startPassiveEffectTimer(); - effect.destroy = create(); - } finally { - recordPassiveEffectDuration(fiber); - } - } else { - effect.destroy = create(); - } - } catch (error) { - invariant(fiber !== null, 'Should be working on an effect.'); - captureCommitPhaseError(fiber, error); - } - } - } - - // Note: This currently assumes there are no passive effects on the root fiber - // because the root is not part of its own effect list. - // This could change in the future. - let effect = root.current.firstEffect; - while (effect !== null) { - const nextNextEffect = effect.nextEffect; - // Remove nextEffect pointer to assist GC - effect.nextEffect = null; - if (effect.flags & Deletion) { - detachFiberAfterEffects(effect); - } - effect = nextNextEffect; - } + commitPassiveUnmountEffects(root.current); + commitPassiveMountEffects(root, root.current); + // TODO: Move to commitPassiveMountEffects if (enableProfilerTimer && enableProfilerCommitHooks) { const profilerEffects = pendingPassiveProfilerEffects; pendingPassiveProfilerEffects = []; @@ -3058,12 +2932,25 @@ function warnAboutUpdateOnUnmountedFiberInDEV(fiber) { return; } - // If there are pending passive effects unmounts for this Fiber, - // we can assume that they would have prevented this update. - if ((fiber.flags & PassiveUnmountPendingDev) !== NoFlags) { - return; - } + if ((fiber.flags & PassiveStatic) !== NoFlags) { + const updateQueue: FunctionComponentUpdateQueue | null = (fiber.updateQueue: any); + if (updateQueue !== null) { + const lastEffect = updateQueue.lastEffect; + if (lastEffect !== null) { + const firstEffect = lastEffect.next; + let effect = firstEffect; + do { + if (effect.destroy !== undefined) { + if ((effect.tag & HookPassive) !== NoHookEffect) { + return; + } + } + effect = effect.next; + } while (effect !== firstEffect); + } + } + } // We show the whole stack but dedupe on the top component's name because // the problematic code almost always lies inside that component. const componentName = getComponentName(fiber.type) || 'ReactComponent'; @@ -3751,8 +3638,3 @@ export function act(callback: () => Thenable): Thenable { }; } } - -function detachFiberAfterEffects(fiber: Fiber): void { - fiber.sibling = null; - fiber.stateNode = null; -}