Skip to content

Commit 9a30e80

Browse files
sammy-SCrbalicki2
andcommitted
Only double invoke newly added components
We need to keep track of fibers that have passed double invocation. To achieve this, this commit repurposes flag PlacementDEV to `NeedsDoubleInvokedEffectsDEV`. Previous approach with Offscreen was not enough. When previously visible Offscreen is hidden, it can have descendants added. Once Offscreen becomes visible, only newly added components have to have their effects double invoked. Not the previously existing components. Co-authored-by: Robert Balicki <[email protected]>
1 parent cb5084d commit 9a30e80

File tree

8 files changed

+155
-83
lines changed

8 files changed

+155
-83
lines changed

packages/react-reconciler/src/ReactChildFiber.new.js

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ import {
1717
Placement,
1818
ChildDeletion,
1919
Forked,
20-
PlacementDEV,
20+
NeedsDoubleInvokedEffectsDEV,
2121
} from './ReactFiberFlags';
2222
import {
2323
getIteratorFn,
@@ -349,15 +349,15 @@ function ChildReconciler(shouldTrackSideEffects) {
349349
const oldIndex = current.index;
350350
if (oldIndex < lastPlacedIndex) {
351351
// This is a move.
352-
newFiber.flags |= Placement | PlacementDEV;
352+
newFiber.flags |= Placement | NeedsDoubleInvokedEffectsDEV;
353353
return lastPlacedIndex;
354354
} else {
355355
// This item can stay in place.
356356
return oldIndex;
357357
}
358358
} else {
359359
// This is an insertion.
360-
newFiber.flags |= Placement | PlacementDEV;
360+
newFiber.flags |= Placement | NeedsDoubleInvokedEffectsDEV;
361361
return lastPlacedIndex;
362362
}
363363
}
@@ -366,7 +366,7 @@ function ChildReconciler(shouldTrackSideEffects) {
366366
// This is simpler for the single child case. We only need to do a
367367
// placement for inserting new children.
368368
if (shouldTrackSideEffects && newFiber.alternate === null) {
369-
newFiber.flags |= Placement | PlacementDEV;
369+
newFiber.flags |= Placement | NeedsDoubleInvokedEffectsDEV;
370370
}
371371
return newFiber;
372372
}

packages/react-reconciler/src/ReactChildFiber.old.js

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ import {
1717
Placement,
1818
ChildDeletion,
1919
Forked,
20-
PlacementDEV,
20+
NeedsDoubleInvokedEffectsDEV,
2121
} from './ReactFiberFlags';
2222
import {
2323
getIteratorFn,
@@ -349,15 +349,15 @@ function ChildReconciler(shouldTrackSideEffects) {
349349
const oldIndex = current.index;
350350
if (oldIndex < lastPlacedIndex) {
351351
// This is a move.
352-
newFiber.flags |= Placement | PlacementDEV;
352+
newFiber.flags |= Placement | NeedsDoubleInvokedEffectsDEV;
353353
return lastPlacedIndex;
354354
} else {
355355
// This item can stay in place.
356356
return oldIndex;
357357
}
358358
} else {
359359
// This is an insertion.
360-
newFiber.flags |= Placement | PlacementDEV;
360+
newFiber.flags |= Placement | NeedsDoubleInvokedEffectsDEV;
361361
return lastPlacedIndex;
362362
}
363363
}
@@ -366,7 +366,7 @@ function ChildReconciler(shouldTrackSideEffects) {
366366
// This is simpler for the single child case. We only need to do a
367367
// placement for inserting new children.
368368
if (shouldTrackSideEffects && newFiber.alternate === null) {
369-
newFiber.flags |= Placement | PlacementDEV;
369+
newFiber.flags |= Placement | NeedsDoubleInvokedEffectsDEV;
370370
}
371371
return newFiber;
372372
}

packages/react-reconciler/src/ReactFiber.new.js

Lines changed: 14 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,12 @@ import {
3333
enableTransitionTracing,
3434
enableDebugTracing,
3535
} from 'shared/ReactFeatureFlags';
36-
import {NoFlags, Placement, StaticMask} from './ReactFiberFlags';
36+
import {
37+
NoFlags,
38+
Placement,
39+
StaticMask,
40+
NeedsDoubleInvokedEffectsDEV,
41+
} from './ReactFiberFlags';
3742
import {ConcurrentRoot} from './ReactRootTags';
3843
import {
3944
IndeterminateComponent,
@@ -300,9 +305,12 @@ export function createWorkInProgress(current: Fiber, pendingProps: any): Fiber {
300305
}
301306
}
302307

303-
// Reset all effects except static ones.
308+
// Reset all effects except static ones NeedsDoubleInvokedEffectsDEV.
304309
// Static effects are not specific to a render.
305-
workInProgress.flags = current.flags & StaticMask;
310+
// NeedsDoubleInvokedEffectsDEV is cleared when component is double invoked
311+
// in debugging. It is only used with StrictMode enabled.
312+
workInProgress.flags =
313+
current.flags & (StaticMask | NeedsDoubleInvokedEffectsDEV);
306314
workInProgress.childLanes = current.childLanes;
307315
workInProgress.lanes = current.lanes;
308316

@@ -369,7 +377,9 @@ export function resetWorkInProgress(
369377

370378
// Reset the effect flags but keep any Placement tags, since that's something
371379
// that child fiber is setting, not the reconciliation.
372-
workInProgress.flags &= StaticMask | Placement;
380+
// NeedsDoubleInvokedEffectsDEV is cleared when component is double invoked
381+
// in debugging. It is only used with StrictMode enabled.
382+
workInProgress.flags &= StaticMask | Placement | NeedsDoubleInvokedEffectsDEV;
373383

374384
// The effects are no longer valid.
375385

packages/react-reconciler/src/ReactFiber.old.js

Lines changed: 14 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,12 @@ import {
3333
enableTransitionTracing,
3434
enableDebugTracing,
3535
} from 'shared/ReactFeatureFlags';
36-
import {NoFlags, Placement, StaticMask} from './ReactFiberFlags';
36+
import {
37+
NoFlags,
38+
Placement,
39+
StaticMask,
40+
NeedsDoubleInvokedEffectsDEV,
41+
} from './ReactFiberFlags';
3742
import {ConcurrentRoot} from './ReactRootTags';
3843
import {
3944
IndeterminateComponent,
@@ -300,9 +305,12 @@ export function createWorkInProgress(current: Fiber, pendingProps: any): Fiber {
300305
}
301306
}
302307

303-
// Reset all effects except static ones.
308+
// Reset all effects except static ones NeedsDoubleInvokedEffectsDEV.
304309
// Static effects are not specific to a render.
305-
workInProgress.flags = current.flags & StaticMask;
310+
// NeedsDoubleInvokedEffectsDEV is cleared when component is double invoked
311+
// in debugging. It is only used with StrictMode enabled.
312+
workInProgress.flags =
313+
current.flags & (StaticMask | NeedsDoubleInvokedEffectsDEV);
306314
workInProgress.childLanes = current.childLanes;
307315
workInProgress.lanes = current.lanes;
308316

@@ -369,7 +377,9 @@ export function resetWorkInProgress(
369377

370378
// Reset the effect flags but keep any Placement tags, since that's something
371379
// that child fiber is setting, not the reconciliation.
372-
workInProgress.flags &= StaticMask | Placement;
380+
// NeedsDoubleInvokedEffectsDEV is cleared when component is double invoked
381+
// in debugging. It is only used with StrictMode enabled.
382+
workInProgress.flags &= StaticMask | Placement | NeedsDoubleInvokedEffectsDEV;
373383

374384
// The effects are no longer valid.
375385

packages/react-reconciler/src/ReactFiberFlags.js

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -53,8 +53,8 @@ export const RefStatic = /* */ 0b000100000000000000000000;
5353
export const LayoutStatic = /* */ 0b001000000000000000000000;
5454
export const PassiveStatic = /* */ 0b010000000000000000000000;
5555

56-
// Flag used to identify newly inserted fibers. It isn't reset after commit unlike `Placement`.
57-
export const PlacementDEV = /* */ 0b100000000000000000000000;
56+
// Flag used to identify fibers that need to have effects double invoked.
57+
export const NeedsDoubleInvokedEffectsDEV = /* */ 0b100000000000000000000000;
5858

5959
// Groups of flags that are used in the commit phase to skip over trees that
6060
// don't contain effects, by checking subtreeFlags.

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

Lines changed: 25 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -123,7 +123,7 @@ import {
123123
MutationMask,
124124
LayoutMask,
125125
PassiveMask,
126-
PlacementDEV,
126+
NeedsDoubleInvokedEffectsDEV,
127127
Visibility,
128128
} from './ReactFiberFlags';
129129
import {
@@ -3239,24 +3239,33 @@ function recursivelyTraverseAndDoubleInvokeEffectsInDEV(
32393239
parentFiber: Fiber,
32403240
isInStrictMode: boolean,
32413241
) {
3242-
if ((parentFiber.subtreeFlags & (PlacementDEV | Visibility)) === NoFlags) {
3242+
if (
3243+
(parentFiber.subtreeFlags & (NeedsDoubleInvokedEffectsDEV | Visibility)) ===
3244+
NoFlags
3245+
) {
32433246
// Parent's descendants have already had effects double invoked.
32443247
// Early exit to avoid unnecessary tree traversal.
32453248
return;
32463249
}
3250+
32473251
let child = parentFiber.child;
32483252
while (child !== null) {
32493253
doubleInvokeEffectsInDEVIfNecessary(root, child, isInStrictMode);
32503254
child = child.sibling;
32513255
}
32523256
}
32533257

3254-
// Unconditionally disconnects and connects passive and layout effects.
3255-
function doubleInvokeEffectsOnFiber(root: FiberRoot, fiber: Fiber) {
3256-
disappearLayoutEffects(fiber);
3257-
disconnectPassiveEffect(fiber);
3258-
reappearLayoutEffects(root, fiber.alternate, fiber, false);
3259-
reconnectPassiveEffects(root, fiber, NoLanes, null, false);
3258+
// NeedsDoubleInvokedEffectsDEV needs to be unset once effects are double invoked.
3259+
// This is to keep track of fibers which need to have their effects double invoked.
3260+
function recursivelyUnsetNeedsDoubleInvokedEffectsDEV(parentFiber: Fiber) {
3261+
if (parentFiber.subtreeFlags & NeedsDoubleInvokedEffectsDEV) {
3262+
parentFiber.flags &= ~NeedsDoubleInvokedEffectsDEV;
3263+
let child = parentFiber.child;
3264+
while (child !== null) {
3265+
recursivelyUnsetNeedsDoubleInvokedEffectsDEV(child);
3266+
child = child.sibling;
3267+
}
3268+
}
32603269
}
32613270

32623271
function doubleInvokeEffectsInDEVIfNecessary(
@@ -3270,10 +3279,14 @@ function doubleInvokeEffectsInDEVIfNecessary(
32703279
// First case: the fiber **is not** of type OffscreenComponent. No
32713280
// special rules apply to double invoking effects.
32723281
if (fiber.tag !== OffscreenComponent) {
3273-
if (fiber.flags & PlacementDEV) {
3282+
if (fiber.flags & NeedsDoubleInvokedEffectsDEV) {
32743283
setCurrentDebugFiberInDEV(fiber);
32753284
if (isInStrictMode) {
3276-
doubleInvokeEffectsOnFiber(root, fiber);
3285+
disappearLayoutEffects(fiber);
3286+
disconnectPassiveEffect(fiber);
3287+
reappearLayoutEffects(root, fiber.alternate, fiber, false);
3288+
reconnectPassiveEffects(root, fiber, NoLanes, null, false);
3289+
recursivelyUnsetNeedsDoubleInvokedEffectsDEV(fiber);
32773290
}
32783291
resetCurrentDebugFiberInDEV();
32793292
} else {
@@ -3287,25 +3300,10 @@ function doubleInvokeEffectsInDEVIfNecessary(
32873300
}
32883301

32893302
// Second case: the fiber **is** of type OffscreenComponent.
3290-
// This branch contains cases specific to Offscreen.
32913303
if (fiber.memoizedState === null) {
3292-
// Only consider Offscreen that is visible.
3304+
// Only continue traversal if Offscreen is visible.
32933305
// TODO (Offscreen) Handle manual mode.
3294-
setCurrentDebugFiberInDEV(fiber);
3295-
if (isInStrictMode && fiber.flags & Visibility) {
3296-
// Double invoke effects on Offscreen's subtree only
3297-
// if it is visible and its visibility has changed.
3298-
doubleInvokeEffectsOnFiber(root, fiber);
3299-
} else if (fiber.subtreeFlags & PlacementDEV) {
3300-
// Something in the subtree could have been suspended.
3301-
// We need to continue traversal and find newly inserted fibers.
3302-
recursivelyTraverseAndDoubleInvokeEffectsInDEV(
3303-
root,
3304-
fiber,
3305-
isInStrictMode,
3306-
);
3307-
}
3308-
resetCurrentDebugFiberInDEV();
3306+
recursivelyTraverseAndDoubleInvokeEffectsInDEV(root, fiber, isInStrictMode);
33093307
}
33103308
}
33113309

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

Lines changed: 25 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -123,7 +123,7 @@ import {
123123
MutationMask,
124124
LayoutMask,
125125
PassiveMask,
126-
PlacementDEV,
126+
NeedsDoubleInvokedEffectsDEV,
127127
Visibility,
128128
} from './ReactFiberFlags';
129129
import {
@@ -3239,24 +3239,33 @@ function recursivelyTraverseAndDoubleInvokeEffectsInDEV(
32393239
parentFiber: Fiber,
32403240
isInStrictMode: boolean,
32413241
) {
3242-
if ((parentFiber.subtreeFlags & (PlacementDEV | Visibility)) === NoFlags) {
3242+
if (
3243+
(parentFiber.subtreeFlags & (NeedsDoubleInvokedEffectsDEV | Visibility)) ===
3244+
NoFlags
3245+
) {
32433246
// Parent's descendants have already had effects double invoked.
32443247
// Early exit to avoid unnecessary tree traversal.
32453248
return;
32463249
}
3250+
32473251
let child = parentFiber.child;
32483252
while (child !== null) {
32493253
doubleInvokeEffectsInDEVIfNecessary(root, child, isInStrictMode);
32503254
child = child.sibling;
32513255
}
32523256
}
32533257

3254-
// Unconditionally disconnects and connects passive and layout effects.
3255-
function doubleInvokeEffectsOnFiber(root: FiberRoot, fiber: Fiber) {
3256-
disappearLayoutEffects(fiber);
3257-
disconnectPassiveEffect(fiber);
3258-
reappearLayoutEffects(root, fiber.alternate, fiber, false);
3259-
reconnectPassiveEffects(root, fiber, NoLanes, null, false);
3258+
// NeedsDoubleInvokedEffectsDEV needs to be unset once effects are double invoked.
3259+
// This is to keep track of fibers which need to have their effects double invoked.
3260+
function recursivelyUnsetNeedsDoubleInvokedEffectsDEV(parentFiber: Fiber) {
3261+
if (parentFiber.subtreeFlags & NeedsDoubleInvokedEffectsDEV) {
3262+
parentFiber.flags &= ~NeedsDoubleInvokedEffectsDEV;
3263+
let child = parentFiber.child;
3264+
while (child !== null) {
3265+
recursivelyUnsetNeedsDoubleInvokedEffectsDEV(child);
3266+
child = child.sibling;
3267+
}
3268+
}
32603269
}
32613270

32623271
function doubleInvokeEffectsInDEVIfNecessary(
@@ -3270,10 +3279,14 @@ function doubleInvokeEffectsInDEVIfNecessary(
32703279
// First case: the fiber **is not** of type OffscreenComponent. No
32713280
// special rules apply to double invoking effects.
32723281
if (fiber.tag !== OffscreenComponent) {
3273-
if (fiber.flags & PlacementDEV) {
3282+
if (fiber.flags & NeedsDoubleInvokedEffectsDEV) {
32743283
setCurrentDebugFiberInDEV(fiber);
32753284
if (isInStrictMode) {
3276-
doubleInvokeEffectsOnFiber(root, fiber);
3285+
disappearLayoutEffects(fiber);
3286+
disconnectPassiveEffect(fiber);
3287+
reappearLayoutEffects(root, fiber.alternate, fiber, false);
3288+
reconnectPassiveEffects(root, fiber, NoLanes, null, false);
3289+
recursivelyUnsetNeedsDoubleInvokedEffectsDEV(fiber);
32773290
}
32783291
resetCurrentDebugFiberInDEV();
32793292
} else {
@@ -3287,25 +3300,10 @@ function doubleInvokeEffectsInDEVIfNecessary(
32873300
}
32883301

32893302
// Second case: the fiber **is** of type OffscreenComponent.
3290-
// This branch contains cases specific to Offscreen.
32913303
if (fiber.memoizedState === null) {
3292-
// Only consider Offscreen that is visible.
3304+
// Only continue traversal if Offscreen is visible.
32933305
// TODO (Offscreen) Handle manual mode.
3294-
setCurrentDebugFiberInDEV(fiber);
3295-
if (isInStrictMode && fiber.flags & Visibility) {
3296-
// Double invoke effects on Offscreen's subtree only
3297-
// if it is visible and its visibility has changed.
3298-
doubleInvokeEffectsOnFiber(root, fiber);
3299-
} else if (fiber.subtreeFlags & PlacementDEV) {
3300-
// Something in the subtree could have been suspended.
3301-
// We need to continue traversal and find newly inserted fibers.
3302-
recursivelyTraverseAndDoubleInvokeEffectsInDEV(
3303-
root,
3304-
fiber,
3305-
isInStrictMode,
3306-
);
3307-
}
3308-
resetCurrentDebugFiberInDEV();
3306+
recursivelyTraverseAndDoubleInvokeEffectsInDEV(root, fiber, isInStrictMode);
33093307
}
33103308
}
33113309

0 commit comments

Comments
 (0)