Skip to content

Commit 7df32c4

Browse files
author
Brian Vaughn
authored
Flush useEffect clean up functions in the passive effects phase (#17925)
* Flush useEffect clean up functions in the passive effects phase This is a change in behavior that may cause broken product code, so it has been added behind a killswitch (deferPassiveEffectCleanupDuringUnmount) * Avoid scheduling unnecessary callbacks for cleanup effects Updated enqueuePendingPassiveEffectDestroyFn() to check rootDoesHavePassiveEffects before scheduling a new callback. This way we'll only schedule (at most) one. * Updated newly added test for added clarity. * Cleaned up hooks effect tags We previously used separate Mount* and Unmount* tags to track hooks work for each phase (snapshot, mutation, layout, and passive). This was somewhat complicated to trace through and there were man tag types we never even used (e.g. UnmountLayout, MountMutation, UnmountSnapshot). In addition to this, it left passive and layout hooks looking the same after renders without changed dependencies, which meant we were unable to reliably defer passive effect destroy functions until after the commit phase. This commit reduces the effect tag types to only include Layout and Passive and differentiates between work and no-work with an HasEffect flag. * Disabled deferred passive effects flushing in OSS builds for now * Split up unmount and mount effects list traversal
1 parent 9944bf2 commit 7df32c4

13 files changed

Lines changed: 248 additions & 87 deletions

‎packages/react-reconciler/src/ReactFiberCommitWork.js‎

Lines changed: 77 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ import type {ReactPriorityLevel} from './SchedulerWithReactIntegration';
2626

2727
import{unstable_wrapasSchedule_tracing_wrap}from'scheduler/tracing';
2828
import{
29+
deferPassiveEffectCleanupDuringUnmount,
2930
enableSchedulerTracing,
3031
enableProfilerTimer,
3132
enableSuspenseServerRenderer,
@@ -109,16 +110,13 @@ import {
109110
captureCommitPhaseError,
110111
resolveRetryThenable,
111112
markCommitTimeOfFallback,
113+
enqueuePendingPassiveEffectDestroyFn,
112114
}from'./ReactFiberWorkLoop';
113115
import{
114116
NoEffectasNoHookEffect,
115-
UnmountSnapshot,
116-
UnmountMutation,
117-
MountMutation,
118-
UnmountLayout,
119-
MountLayout,
120-
UnmountPassive,
121-
MountPassive,
117+
HasEffectasHookHasEffect,
118+
LayoutasHookLayout,
119+
PassiveasHookPassive,
122120
}from'./ReactHookEffectTags';
123121
import{didWarnAboutReassigningProps}from'./ReactFiberBeginWork';
124122
import{runWithPriority,NormalPriority}from'./SchedulerWithReactIntegration';
@@ -250,7 +248,6 @@ function commitBeforeMutationLifeCycles(
250248
caseForwardRef:
251249
caseSimpleMemoComponent:
252250
caseChunk: {
253-
commitHookEffectList(UnmountSnapshot,NoHookEffect,finishedWork);
254251
return;
255252
}
256253
caseClassComponent: {
@@ -328,26 +325,34 @@ function commitBeforeMutationLifeCycles(
328325
);
329326
}
330327

331-
functioncommitHookEffectList(
332-
unmountTag: number,
333-
mountTag: number,
334-
finishedWork: Fiber,
335-
){
328+
functioncommitHookEffectListUnmount(tag: number,finishedWork: Fiber){
336329
constupdateQueue: FunctionComponentUpdateQueue|null=(finishedWork.updateQueue: any);
337330
letlastEffect=updateQueue!==null ? updateQueue.lastEffect : null;
338331
if(lastEffect!==null){
339332
constfirstEffect=lastEffect.next;
340333
leteffect=firstEffect;
341334
do{
342-
if((effect.tag&unmountTag)!==NoHookEffect){
335+
if((effect.tag&tag)===tag){
343336
// Unmount
344337
constdestroy=effect.destroy;
345338
effect.destroy=undefined;
346339
if(destroy!==undefined){
347340
destroy();
348341
}
349342
}
350-
if((effect.tag&mountTag)!==NoHookEffect){
343+
effect=effect.next;
344+
}while(effect!==firstEffect);
345+
}
346+
}
347+
348+
functioncommitHookEffectListMount(tag: number,finishedWork: Fiber){
349+
constupdateQueue: FunctionComponentUpdateQueue|null=(finishedWork.updateQueue: any);
350+
letlastEffect=updateQueue!==null ? updateQueue.lastEffect : null;
351+
if(lastEffect!==null){
352+
constfirstEffect=lastEffect.next;
353+
leteffect=firstEffect;
354+
do{
355+
if((effect.tag&tag)===tag){
351356
// Mount
352357
constcreate=effect.create;
353358
effect.destroy=create();
@@ -398,8 +403,11 @@ export function commitPassiveHookEffects(finishedWork: Fiber): void {
398403
caseForwardRef:
399404
caseSimpleMemoComponent:
400405
caseChunk: {
401-
commitHookEffectList(UnmountPassive,NoHookEffect,finishedWork);
402-
commitHookEffectList(NoHookEffect,MountPassive,finishedWork);
406+
// TODO (#17945) We should call all passive destroy functions (for all fibers)
407+
// before calling any create functions. The current approach only serializes
408+
// these for a single fiber.
409+
commitHookEffectListUnmount(HookPassive|HookHasEffect,finishedWork);
410+
commitHookEffectListMount(HookPassive|HookHasEffect,finishedWork);
403411
break;
404412
}
405413
default:
@@ -419,7 +427,11 @@ function commitLifeCycles(
419427
caseForwardRef:
420428
caseSimpleMemoComponent:
421429
caseChunk: {
422-
commitHookEffectList(UnmountLayout,MountLayout,finishedWork);
430+
// At this point layout effects have already been destroyed (during mutation phase).
431+
// This is done to prevent sibling component effects from interfering with each other,
432+
// e.g. a destroy function in one component should never override a ref set
433+
// by a create function in another component during the same commit.
434+
commitHookEffectListMount(HookLayout|HookHasEffect,finishedWork);
423435
return;
424436
}
425437
caseClassComponent: {
@@ -756,32 +768,47 @@ function commitUnmount(
756768
if(lastEffect!==null){
757769
constfirstEffect=lastEffect.next;
758770

759-
// When the owner fiber is deleted, the destroy function of a passive
760-
// effect hook is called during the synchronous commit phase. This is
761-
// a concession to implementation complexity. Calling it in the
762-
// passive effect phase (like they usually are, when dependencies
763-
// change during an update) would require either traversing the
764-
// children of the deleted fiber again, or including unmount effects
765-
// as part of the fiber effect list.
766-
//
767-
// Because this is during the sync commit phase, we need to change
768-
// the priority.
769-
//
770-
// TODO: Reconsider this implementation trade off.
771-
constpriorityLevel=
772-
renderPriorityLevel>NormalPriority
773-
? NormalPriority
774-
: renderPriorityLevel;
775-
runWithPriority(priorityLevel,()=>{
771+
if(deferPassiveEffectCleanupDuringUnmount){
776772
leteffect=firstEffect;
777773
do{
778-
constdestroy=effect.destroy;
774+
const{destroy, tag}=effect;
779775
if(destroy!==undefined){
780-
safelyCallDestroy(current,destroy);
776+
if((tag&HookPassive)!==NoHookEffect){
777+
enqueuePendingPassiveEffectDestroyFn(destroy);
778+
}else{
779+
safelyCallDestroy(current,destroy);
780+
}
781781
}
782782
effect=effect.next;
783783
}while(effect!==firstEffect);
784-
});
784+
}else{
785+
// When the owner fiber is deleted, the destroy function of a passive
786+
// effect hook is called during the synchronous commit phase. This is
787+
// a concession to implementation complexity. Calling it in the
788+
// passive effect phase (like they usually are, when dependencies
789+
// change during an update) would require either traversing the
790+
// children of the deleted fiber again, or including unmount effects
791+
// as part of the fiber effect list.
792+
//
793+
// Because this is during the sync commit phase, we need to change
794+
// the priority.
795+
//
796+
// TODO: Reconsider this implementation trade off.
797+
constpriorityLevel=
798+
renderPriorityLevel>NormalPriority
799+
? NormalPriority
800+
: renderPriorityLevel;
801+
runWithPriority(priorityLevel,()=>{
802+
leteffect=firstEffect;
803+
do{
804+
constdestroy=effect.destroy;
805+
if(destroy!==undefined){
806+
safelyCallDestroy(current,destroy);
807+
}
808+
effect=effect.next;
809+
}while(effect!==firstEffect);
810+
});
811+
}
785812
}
786813
}
787814
return;
@@ -1285,9 +1312,12 @@ function commitWork(current: Fiber | null, finishedWork: Fiber): void {
12851312
caseMemoComponent:
12861313
caseSimpleMemoComponent:
12871314
caseChunk: {
1288-
// Note: We currently never use MountMutation, but useLayout uses
1289-
// UnmountMutation.
1290-
commitHookEffectList(UnmountMutation,MountMutation,finishedWork);
1315+
// Layout effects are destroyed during the mutation phase so that all
1316+
// destroy functions for all fibers are called before any create functions.
1317+
// This prevents sibling component effects from interfering with each other,
1318+
// e.g. a destroy function in one component should never override a ref set
1319+
// by a create function in another component during the same commit.
1320+
commitHookEffectListUnmount(HookLayout|HookHasEffect,finishedWork);
12911321
return;
12921322
}
12931323
caseProfiler: {
@@ -1325,9 +1355,12 @@ function commitWork(current: Fiber | null, finishedWork: Fiber): void {
13251355
caseMemoComponent:
13261356
caseSimpleMemoComponent:
13271357
caseChunk: {
1328-
// Note: We currently never use MountMutation, but useLayout uses
1329-
// UnmountMutation.
1330-
commitHookEffectList(UnmountMutation,MountMutation,finishedWork);
1358+
// Layout effects are destroyed during the mutation phase so that all
1359+
// destroy functions for all fibers are called before any create functions.
1360+
// This prevents sibling component effects from interfering with each other,
1361+
// e.g. a destroy function in one component should never override a ref set
1362+
// by a create function in another component during the same commit.
1363+
commitHookEffectListUnmount(HookLayout|HookHasEffect,finishedWork);
13311364
return;
13321365
}
13331366
caseClassComponent: {

‎packages/react-reconciler/src/ReactFiberHooks.js‎

Lines changed: 22 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -28,11 +28,9 @@ import {
2828
PassiveasPassiveEffect,
2929
}from'shared/ReactSideEffectTags';
3030
import{
31-
NoEffectasNoHookEffect,
32-
UnmountMutation,
33-
MountLayout,
34-
UnmountPassive,
35-
MountPassive,
31+
HasEffectasHookHasEffect,
32+
LayoutasHookLayout,
33+
PassiveasHookPassive,
3634
}from'./ReactHookEffectTags';
3735
import{
3836
scheduleWork,
@@ -923,7 +921,12 @@ function mountEffectImpl(fiberEffectTag, hookEffectTag, create, deps): void {
923921
consthook=mountWorkInProgressHook();
924922
constnextDeps=deps===undefined ? null : deps;
925923
currentlyRenderingFiber.effectTag|=fiberEffectTag;
926-
hook.memoizedState=pushEffect(hookEffectTag,create,undefined,nextDeps);
924+
hook.memoizedState=pushEffect(
925+
HookHasEffect|hookEffectTag,
926+
create,
927+
undefined,
928+
nextDeps,
929+
);
927930
}
928931

929932
functionupdateEffectImpl(fiberEffectTag,hookEffectTag,create,deps): void{
@@ -937,15 +940,20 @@ function updateEffectImpl(fiberEffectTag, hookEffectTag, create, deps): void {
937940
if(nextDeps!==null){
938941
constprevDeps=prevEffect.deps;
939942
if(areHookInputsEqual(nextDeps,prevDeps)){
940-
pushEffect(NoHookEffect,create,destroy,nextDeps);
943+
pushEffect(hookEffectTag,create,destroy,nextDeps);
941944
return;
942945
}
943946
}
944947
}
945948

946949
currentlyRenderingFiber.effectTag|=fiberEffectTag;
947950

948-
hook.memoizedState=pushEffect(hookEffectTag,create,destroy,nextDeps);
951+
hook.memoizedState=pushEffect(
952+
HookHasEffect|hookEffectTag,
953+
create,
954+
destroy,
955+
nextDeps,
956+
);
949957
}
950958

951959
functionmountEffect(
@@ -960,7 +968,7 @@ function mountEffect(
960968
}
961969
returnmountEffectImpl(
962970
UpdateEffect|PassiveEffect,
963-
UnmountPassive|MountPassive,
971+
HookPassive,
964972
create,
965973
deps,
966974
);
@@ -978,7 +986,7 @@ function updateEffect(
978986
}
979987
returnupdateEffectImpl(
980988
UpdateEffect|PassiveEffect,
981-
UnmountPassive|MountPassive,
989+
HookPassive,
982990
create,
983991
deps,
984992
);
@@ -988,24 +996,14 @@ function mountLayoutEffect(
988996
create: ()=>(()=>void)|void,
989997
deps: Array<mixed>|void|null,
990998
): void{
991-
returnmountEffectImpl(
992-
UpdateEffect,
993-
UnmountMutation|MountLayout,
994-
create,
995-
deps,
996-
);
999+
returnmountEffectImpl(UpdateEffect,HookLayout,create,deps);
9971000
}
9981001

9991002
functionupdateLayoutEffect(
10001003
create: ()=>(()=>void)|void,
10011004
deps: Array<mixed>|void|null,
10021005
): void{
1003-
returnupdateEffectImpl(
1004-
UpdateEffect,
1005-
UnmountMutation|MountLayout,
1006-
create,
1007-
deps,
1008-
);
1006+
returnupdateEffectImpl(UpdateEffect,HookLayout,create,deps);
10091007
}
10101008

10111009
functionimperativeHandleEffect<T>(
@@ -1059,7 +1057,7 @@ function mountImperativeHandle<T>(
10591057

10601058
returnmountEffectImpl(
10611059
UpdateEffect,
1062-
UnmountMutation|MountLayout,
1060+
HookLayout,
10631061
imperativeHandleEffect.bind(null,create,ref),
10641062
effectDeps,
10651063
);
@@ -1086,7 +1084,7 @@ function updateImperativeHandle<T>(
10861084

10871085
returnupdateEffectImpl(
10881086
UpdateEffect,
1089-
UnmountMutation|MountLayout,
1087+
HookLayout,
10901088
imperativeHandleEffect.bind(null,create,ref),
10911089
effectDeps,
10921090
);

‎packages/react-reconciler/src/ReactFiberWorkLoop.js‎

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ import type {Hook} from './ReactFiberHooks';
1818

1919
import{
2020
warnAboutDeprecatedLifecycles,
21+
deferPassiveEffectCleanupDuringUnmount,
2122
enableUserTimingAPI,
2223
enableSuspenseServerRenderer,
2324
replayFailedUnitOfWorkWithInvokeGuardedCallback,
@@ -257,6 +258,7 @@ let rootDoesHavePassiveEffects: boolean = false;
257258
letrootWithPendingPassiveEffects: FiberRoot|null=null;
258259
let pendingPassiveEffectsRenderPriority: ReactPriorityLevel=NoPriority;
259260
let pendingPassiveEffectsExpirationTime: ExpirationTime=NoWork;
261+
let pendingUnmountedPassiveEffectDestroyFunctions: Array<()=>void>=[];
260262

261263
let rootsWithPendingDiscreteUpdates: Map<
262264
FiberRoot,
@@ -2176,6 +2178,21 @@ export function flushPassiveEffects() {
21762178
}
21772179
}
21782180

2181+
exportfunctionenqueuePendingPassiveEffectDestroyFn(
2182+
destroy: ()=>void,
2183+
): void{
2184+
if(deferPassiveEffectCleanupDuringUnmount){
2185+
pendingUnmountedPassiveEffectDestroyFunctions.push(destroy);
2186+
if(!rootDoesHavePassiveEffects){
2187+
rootDoesHavePassiveEffects=true;
2188+
scheduleCallback(NormalPriority,()=>{
2189+
flushPassiveEffects();
2190+
returnnull;
2191+
});
2192+
}
2193+
}
2194+
}
2195+
21792196
functionflushPassiveEffectsImpl(){
21802197
if(rootWithPendingPassiveEffects===null){
21812198
returnfalse;
@@ -2193,6 +2210,20 @@ function flushPassiveEffectsImpl() {
21932210
executionContext|=CommitContext;
21942211
constprevInteractions=pushInteractions(root);
21952212

2213+
if(deferPassiveEffectCleanupDuringUnmount){
2214+
// Flush any pending passive effect destroy functions that belong to
2215+
// components that were unmounted during the most recent commit.
2216+
for(
2217+
leti=0;
2218+
i<pendingUnmountedPassiveEffectDestroyFunctions.length;
2219+
i++
2220+
){
2221+
constdestroy=pendingUnmountedPassiveEffectDestroyFunctions[i];
2222+
invokeGuardedCallback(null,destroy,null);
2223+
}
2224+
pendingUnmountedPassiveEffectDestroyFunctions.length=0;
2225+
}
2226+
21962227
// Note: This currently assumes there are no passive effects on the root
21972228
// fiber, because the root is not part of its own effect list. This could
21982229
// change in the future.

‎packages/react-reconciler/src/ReactHookEffectTags.js‎

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -9,11 +9,11 @@
99

1010
exporttypeHookEffectTag=number;
1111

12-
exportconstNoEffect=/* */0b00000000;
13-
exportconstUnmountSnapshot=/* */0b00000010;
14-
exportconstUnmountMutation=/* */0b00000100;
15-
exportconstMountMutation=/* */0b00001000;
16-
exportconstUnmountLayout=/* */0b00010000;
17-
exportconstMountLayout=/* */0b00100000;
18-
exportconstMountPassive=/* */0b01000000;
19-
exportconstUnmountPassive=/* */0b10000000;
12+
exportconstNoEffect=/* */0b000;
13+
14+
// Represents whether effect should fire.
15+
exportconstHasEffect=/* */0b001;
16+
17+
// Represents the phase in which the effect (not the clean-up) fires.
18+
exportconstLayout=/* */0b010;
19+
exportconstPassive=/* */0b100;

0 commit comments

Comments
 (0)