-
Notifications
You must be signed in to change notification settings - Fork 46.9k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Flush useEffect
clean up functions in the passive effects phase
#17925
Changes from all commits
e1f8379
cf44b09
5f3a3b9
e8c7da1
d93a76f
606af56
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -26,6 +26,7 @@ import type {ReactPriorityLevel} from './SchedulerWithReactIntegration'; | |
|
||
import {unstable_wrap as Schedule_tracing_wrap} from 'scheduler/tracing'; | ||
import { | ||
deferPassiveEffectCleanupDuringUnmount, | ||
enableSchedulerTracing, | ||
enableProfilerTimer, | ||
enableSuspenseServerRenderer, | ||
|
@@ -109,16 +110,13 @@ import { | |
captureCommitPhaseError, | ||
resolveRetryThenable, | ||
markCommitTimeOfFallback, | ||
enqueuePendingPassiveEffectDestroyFn, | ||
} from './ReactFiberWorkLoop'; | ||
import { | ||
NoEffect as NoHookEffect, | ||
UnmountSnapshot, | ||
UnmountMutation, | ||
MountMutation, | ||
UnmountLayout, | ||
MountLayout, | ||
UnmountPassive, | ||
MountPassive, | ||
HasEffect as HookHasEffect, | ||
Layout as HookLayout, | ||
Passive as HookPassive, | ||
} from './ReactHookEffectTags'; | ||
import {didWarnAboutReassigningProps} from './ReactFiberBeginWork'; | ||
import {runWithPriority, NormalPriority} from './SchedulerWithReactIntegration'; | ||
|
@@ -250,7 +248,6 @@ function commitBeforeMutationLifeCycles( | |
case ForwardRef: | ||
case SimpleMemoComponent: | ||
case Chunk: { | ||
commitHookEffectList(UnmountSnapshot, NoHookEffect, finishedWork); | ||
return; | ||
} | ||
case ClassComponent: { | ||
|
@@ -328,26 +325,34 @@ function commitBeforeMutationLifeCycles( | |
); | ||
} | ||
|
||
function commitHookEffectList( | ||
unmountTag: number, | ||
mountTag: number, | ||
finishedWork: Fiber, | ||
) { | ||
function commitHookEffectListUnmount(tag: number, finishedWork: Fiber) { | ||
const updateQueue: FunctionComponentUpdateQueue | null = (finishedWork.updateQueue: any); | ||
let lastEffect = updateQueue !== null ? updateQueue.lastEffect : null; | ||
if (lastEffect !== null) { | ||
const firstEffect = lastEffect.next; | ||
let effect = firstEffect; | ||
do { | ||
if ((effect.tag & unmountTag) !== NoHookEffect) { | ||
if ((effect.tag & tag) === tag) { | ||
// Unmount | ||
const destroy = effect.destroy; | ||
effect.destroy = undefined; | ||
if (destroy !== undefined) { | ||
destroy(); | ||
} | ||
} | ||
if ((effect.tag & mountTag) !== NoHookEffect) { | ||
effect = effect.next; | ||
} while (effect !== firstEffect); | ||
} | ||
} | ||
|
||
function commitHookEffectListMount(tag: number, finishedWork: Fiber) { | ||
const updateQueue: FunctionComponentUpdateQueue | null = (finishedWork.updateQueue: any); | ||
let lastEffect = updateQueue !== null ? updateQueue.lastEffect : null; | ||
if (lastEffect !== null) { | ||
const firstEffect = lastEffect.next; | ||
let effect = firstEffect; | ||
do { | ||
if ((effect.tag & tag) === tag) { | ||
// Mount | ||
const create = effect.create; | ||
effect.destroy = create(); | ||
|
@@ -398,8 +403,11 @@ export function commitPassiveHookEffects(finishedWork: Fiber): void { | |
case ForwardRef: | ||
case SimpleMemoComponent: | ||
case Chunk: { | ||
commitHookEffectList(UnmountPassive, NoHookEffect, finishedWork); | ||
commitHookEffectList(NoHookEffect, MountPassive, finishedWork); | ||
// TODO (#17945) We should call all passive destroy functions (for all fibers) | ||
// before calling any create functions. The current approach only serializes | ||
// these for a single fiber. | ||
commitHookEffectListUnmount(HookPassive | HookHasEffect, finishedWork); | ||
commitHookEffectListMount(HookPassive | HookHasEffect, finishedWork); | ||
break; | ||
} | ||
default: | ||
|
@@ -419,7 +427,11 @@ function commitLifeCycles( | |
case ForwardRef: | ||
case SimpleMemoComponent: | ||
case Chunk: { | ||
commitHookEffectList(UnmountLayout, MountLayout, finishedWork); | ||
// 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. | ||
commitHookEffectListMount(HookLayout | HookHasEffect, finishedWork); | ||
return; | ||
} | ||
case ClassComponent: { | ||
|
@@ -756,32 +768,47 @@ function commitUnmount( | |
if (lastEffect !== null) { | ||
const firstEffect = lastEffect.next; | ||
|
||
// When the owner fiber is deleted, the destroy function of a passive | ||
// effect hook is called during the synchronous commit phase. This is | ||
// a concession to implementation complexity. Calling it in the | ||
// passive effect phase (like they usually are, when dependencies | ||
// change during an update) would require either traversing the | ||
// children of the deleted fiber again, or including unmount effects | ||
// as part of the fiber effect list. | ||
// | ||
// Because this is during the sync commit phase, we need to change | ||
// the priority. | ||
// | ||
// TODO: Reconsider this implementation trade off. | ||
const priorityLevel = | ||
renderPriorityLevel > NormalPriority | ||
? NormalPriority | ||
: renderPriorityLevel; | ||
runWithPriority(priorityLevel, () => { | ||
if (deferPassiveEffectCleanupDuringUnmount) { | ||
let effect = firstEffect; | ||
do { | ||
const destroy = effect.destroy; | ||
const {destroy, tag} = effect; | ||
if (destroy !== undefined) { | ||
safelyCallDestroy(current, destroy); | ||
if ((tag & HookPassive) !== NoHookEffect) { | ||
enqueuePendingPassiveEffectDestroyFn(destroy); | ||
} else { | ||
safelyCallDestroy(current, destroy); | ||
} | ||
} | ||
effect = effect.next; | ||
} while (effect !== firstEffect); | ||
}); | ||
} else { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This is the old code path, in case we enable the kill switch. |
||
// When the owner fiber is deleted, the destroy function of a passive | ||
// effect hook is called during the synchronous commit phase. This is | ||
// a concession to implementation complexity. Calling it in the | ||
// passive effect phase (like they usually are, when dependencies | ||
// change during an update) would require either traversing the | ||
// children of the deleted fiber again, or including unmount effects | ||
// as part of the fiber effect list. | ||
// | ||
// Because this is during the sync commit phase, we need to change | ||
// the priority. | ||
// | ||
// TODO: Reconsider this implementation trade off. | ||
const priorityLevel = | ||
renderPriorityLevel > NormalPriority | ||
? NormalPriority | ||
: renderPriorityLevel; | ||
runWithPriority(priorityLevel, () => { | ||
let effect = firstEffect; | ||
do { | ||
const destroy = effect.destroy; | ||
if (destroy !== undefined) { | ||
safelyCallDestroy(current, destroy); | ||
} | ||
effect = effect.next; | ||
} while (effect !== firstEffect); | ||
}); | ||
} | ||
} | ||
} | ||
return; | ||
|
@@ -1285,9 +1312,12 @@ function commitWork(current: Fiber | null, finishedWork: Fiber): void { | |
case MemoComponent: | ||
case SimpleMemoComponent: | ||
case Chunk: { | ||
// Note: We currently never use MountMutation, but useLayout uses | ||
// UnmountMutation. | ||
commitHookEffectList(UnmountMutation, MountMutation, finishedWork); | ||
// Layout effects are destroyed during the mutation phase so that all | ||
// destroy functions for all fibers are called before any create functions. | ||
// This prevents sibling component effects from interfering with each other, | ||
// e.g. a destroy function in one component should never override a ref set | ||
// by a create function in another component during the same commit. | ||
commitHookEffectListUnmount(HookLayout | HookHasEffect, finishedWork); | ||
return; | ||
} | ||
case Profiler: { | ||
|
@@ -1325,9 +1355,12 @@ function commitWork(current: Fiber | null, finishedWork: Fiber): void { | |
case MemoComponent: | ||
case SimpleMemoComponent: | ||
case Chunk: { | ||
// Note: We currently never use MountMutation, but useLayout uses | ||
// UnmountMutation. | ||
commitHookEffectList(UnmountMutation, MountMutation, finishedWork); | ||
// Layout effects are destroyed during the mutation phase so that all | ||
// destroy functions for all fibers are called before any create functions. | ||
// This prevents sibling component effects from interfering with each other, | ||
// e.g. a destroy function in one component should never override a ref set | ||
// by a create function in another component during the same commit. | ||
commitHookEffectListUnmount(HookLayout | HookHasEffect, finishedWork); | ||
return; | ||
} | ||
case ClassComponent: { | ||
|
Original file line number | Diff line number | Diff line change | ||
---|---|---|---|---|
|
@@ -18,6 +18,7 @@ import type {Hook} from './ReactFiberHooks'; | |||
|
||||
import { | ||||
warnAboutDeprecatedLifecycles, | ||||
deferPassiveEffectCleanupDuringUnmount, | ||||
enableUserTimingAPI, | ||||
enableSuspenseServerRenderer, | ||||
replayFailedUnitOfWorkWithInvokeGuardedCallback, | ||||
|
@@ -257,6 +258,7 @@ let rootDoesHavePassiveEffects: boolean = false; | |||
let rootWithPendingPassiveEffects: FiberRoot | null = null; | ||||
let pendingPassiveEffectsRenderPriority: ReactPriorityLevel = NoPriority; | ||||
let pendingPassiveEffectsExpirationTime: ExpirationTime = NoWork; | ||||
let pendingUnmountedPassiveEffectDestroyFunctions: Array<() => void> = []; | ||||
|
||||
let rootsWithPendingDiscreteUpdates: Map< | ||||
FiberRoot, | ||||
|
@@ -2176,6 +2178,21 @@ export function flushPassiveEffects() { | |||
} | ||||
} | ||||
|
||||
export function enqueuePendingPassiveEffectDestroyFn( | ||||
destroy: () => void, | ||||
): void { | ||||
if (deferPassiveEffectCleanupDuringUnmount) { | ||||
pendingUnmountedPassiveEffectDestroyFunctions.push(destroy); | ||||
if (!rootDoesHavePassiveEffects) { | ||||
rootDoesHavePassiveEffects = true; | ||||
scheduleCallback(NormalPriority, () => { | ||||
flushPassiveEffects(); | ||||
return null; | ||||
}); | ||||
} | ||||
} | ||||
} | ||||
|
||||
function flushPassiveEffectsImpl() { | ||||
if (rootWithPendingPassiveEffects === null) { | ||||
return false; | ||||
|
@@ -2193,6 +2210,20 @@ function flushPassiveEffectsImpl() { | |||
executionContext |= CommitContext; | ||||
const prevInteractions = pushInteractions(root); | ||||
|
||||
if (deferPassiveEffectCleanupDuringUnmount) { | ||||
// Flush any pending passive effect destroy functions that belong to | ||||
// components that were unmounted during the most recent commit. | ||||
for ( | ||||
let i = 0; | ||||
i < pendingUnmountedPassiveEffectDestroyFunctions.length; | ||||
i++ | ||||
) { | ||||
const destroy = pendingUnmountedPassiveEffectDestroyFunctions[i]; | ||||
invokeGuardedCallback(null, destroy, null); | ||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Did you confirm that with a test? I would expect the current behavior to also unmount all the destroy functions on a fiber, even if one throws, because if one of them throws, then the nearest error boundary fiber gets deleted, which triggers
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Ah, yes I think you are right. Disregard! |
||||
} | ||||
pendingUnmountedPassiveEffectDestroyFunctions.length = 0; | ||||
} | ||||
|
||||
// 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. | ||||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We don't have a snapshot phase hook (yet) so this call was walking the effect list for no reason. I've removed it.