Skip to content

Commit cfb6cfa

Browse files
committed
Reused components commit with timing as new ones
When an Offscreen tree goes from hidden -> visible, the tree may include both reused components that were unmounted when the tree was hidden, and also brand new components that didn't exist in the hidden tree. Currently when this happens, we commit all the reused components' effects first, before committing the new ones, using two separate traversals of the tree. Instead, we should fire all the effects with the same timing as if it were a completely new tree. See the test I wrote for an example. This is also more efficient because we only need to traverse the tree once.
1 parent 679eea3 commit cfb6cfa

File tree

3 files changed

+489
-194
lines changed

3 files changed

+489
-194
lines changed

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

Lines changed: 159 additions & 97 deletions
Original file line numberDiff line numberDiff line change
@@ -225,18 +225,6 @@ const callComponentWillUnmountWithTimer = function(current, instance) {
225225
}
226226
};
227227

228-
// Capture errors so they don't interrupt mounting.
229-
function safelyCallCommitHookLayoutEffectListMount(
230-
current: Fiber,
231-
nearestMountedAncestor: Fiber | null,
232-
) {
233-
try {
234-
commitHookEffectListMount(HookLayout, current);
235-
} catch (error) {
236-
captureCommitPhaseError(current, nearestMountedAncestor, error);
237-
}
238-
}
239-
240228
// Capture errors so they don't interrupt unmounting.
241229
function safelyCallComponentWillUnmount(
242230
current: Fiber,
@@ -250,19 +238,6 @@ function safelyCallComponentWillUnmount(
250238
}
251239
}
252240

253-
// Capture errors so they don't interrupt mounting.
254-
function safelyCallComponentDidMount(
255-
current: Fiber,
256-
nearestMountedAncestor: Fiber | null,
257-
instance: any,
258-
) {
259-
try {
260-
instance.componentDidMount();
261-
} catch (error) {
262-
captureCommitPhaseError(current, nearestMountedAncestor, error);
263-
}
264-
}
265-
266241
// Capture errors so they don't interrupt mounting.
267242
function safelyAttachRef(current: Fiber, nearestMountedAncestor: Fiber | null) {
268243
try {
@@ -706,7 +681,7 @@ export function commitPassiveEffectDurations(
706681
}
707682
}
708683

709-
function commitHookLayoutEffects(finishedWork: Fiber) {
684+
function commitHookLayoutEffects(finishedWork: Fiber, hookFlags: HookFlags) {
710685
// At this point layout effects have already been destroyed (during mutation phase).
711686
// This is done to prevent sibling component effects from interfering with each other,
712687
// e.g. a destroy function in one component should never override a ref set
@@ -718,14 +693,14 @@ function commitHookLayoutEffects(finishedWork: Fiber) {
718693
) {
719694
try {
720695
startLayoutEffectTimer();
721-
commitHookEffectListMount(HookLayout | HookHasEffect, finishedWork);
696+
commitHookEffectListMount(hookFlags, finishedWork);
722697
} catch (error) {
723698
captureCommitPhaseError(finishedWork, finishedWork.return, error);
724699
}
725700
recordLayoutEffectDuration(finishedWork);
726701
} else {
727702
try {
728-
commitHookEffectListMount(HookLayout | HookHasEffect, finishedWork);
703+
commitHookEffectListMount(hookFlags, finishedWork);
729704
} catch (error) {
730705
captureCommitPhaseError(finishedWork, finishedWork.return, error);
731706
}
@@ -853,7 +828,7 @@ function commitClassLayoutLifecycles(
853828
}
854829
}
855830

856-
function commitClassCallbacks(finishedWork: Fiber, current: Fiber | null) {
831+
function commitClassCallbacks(finishedWork: Fiber) {
857832
// TODO: I think this is now always non-null by the time it reaches the
858833
// commit phase. Consider removing the type check.
859834
const updateQueue: UpdateQueue<*> | null = (finishedWork.updateQueue: any);
@@ -897,7 +872,7 @@ function commitClassCallbacks(finishedWork: Fiber, current: Fiber | null) {
897872
}
898873
}
899874

900-
function commitHostComponentMount(finishedWork: Fiber, current: Fiber | null) {
875+
function commitHostComponentMount(finishedWork: Fiber) {
901876
const type = finishedWork.type;
902877
const props = finishedWork.memoizedProps;
903878
const instance: Instance = finishedWork.stateNode;
@@ -978,6 +953,8 @@ function commitLayoutEffectOnFiber(
978953
finishedWork: Fiber,
979954
committedLanes: Lanes,
980955
): void {
956+
// When updating this function, also update reappearLayoutEffects, which does
957+
// most of the same things when an offscreen tree goes from hidden -> visible.
981958
const flags = finishedWork.flags;
982959
switch (finishedWork.tag) {
983960
case FunctionComponent:
@@ -989,9 +966,7 @@ function commitLayoutEffectOnFiber(
989966
committedLanes,
990967
);
991968
if (flags & Update) {
992-
if (!offscreenSubtreeWasHidden) {
993-
commitHookLayoutEffects(finishedWork);
994-
}
969+
commitHookLayoutEffects(finishedWork, HookLayout | HookHasEffect);
995970
}
996971
break;
997972
}
@@ -1002,19 +977,15 @@ function commitLayoutEffectOnFiber(
1002977
committedLanes,
1003978
);
1004979
if (flags & Update) {
1005-
if (!offscreenSubtreeWasHidden) {
1006-
commitClassLayoutLifecycles(finishedWork, current);
1007-
}
980+
commitClassLayoutLifecycles(finishedWork, current);
1008981
}
1009982

1010983
if (flags & Callback) {
1011-
commitClassCallbacks(finishedWork, current);
984+
commitClassCallbacks(finishedWork);
1012985
}
1013986

1014987
if (flags & Ref) {
1015-
if (!offscreenSubtreeWasHidden) {
1016-
safelyAttachRef(finishedWork, finishedWork.return);
1017-
}
988+
safelyAttachRef(finishedWork, finishedWork.return);
1018989
}
1019990
break;
1020991
}
@@ -1063,13 +1034,11 @@ function commitLayoutEffectOnFiber(
10631034
// These effects should only be committed when components are first mounted,
10641035
// aka when there is no current/alternate.
10651036
if (current === null && flags & Update) {
1066-
commitHostComponentMount(finishedWork, current);
1037+
commitHostComponentMount(finishedWork);
10671038
}
10681039

10691040
if (flags & Ref) {
1070-
if (!offscreenSubtreeWasHidden) {
1071-
safelyAttachRef(finishedWork, finishedWork.return);
1072-
}
1041+
safelyAttachRef(finishedWork, finishedWork.return);
10731042
}
10741043
break;
10751044
}
@@ -1117,19 +1086,25 @@ function commitLayoutEffectOnFiber(
11171086
offscreenSubtreeWasHidden = newOffscreenSubtreeWasHidden;
11181087

11191088
if (offscreenSubtreeWasHidden && !prevOffscreenSubtreeWasHidden) {
1120-
// This is the root of a reappearing boundary. Turn its layout
1121-
// effects back on.
1122-
recursivelyTraverseReappearLayoutEffects(finishedWork);
1089+
// This is the root of a reappearing boundary. As we continue
1090+
// traversing the layout effects, we must also re-mount layout
1091+
// effects that were unmounted when the Offscreen subtree was
1092+
// hidden. So this is a superset of the normal commitLayoutEffects.
1093+
const includeWorkInProgressEffects =
1094+
(finishedWork.subtreeFlags & LayoutMask) !== NoFlags;
1095+
recursivelyTraverseReappearLayoutEffects(
1096+
finishedRoot,
1097+
finishedWork,
1098+
committedLanes,
1099+
includeWorkInProgressEffects,
1100+
);
1101+
} else {
1102+
recursivelyTraverseLayoutEffects(
1103+
finishedRoot,
1104+
finishedWork,
1105+
committedLanes,
1106+
);
11231107
}
1124-
1125-
// TODO: We shouldn't traverse twice when reappearing layout effects.
1126-
// Move this into the else block of the above if statement, and modify
1127-
// reappearLayoutEffects to fire regular layout effects, too.
1128-
recursivelyTraverseLayoutEffects(
1129-
finishedRoot,
1130-
finishedWork,
1131-
committedLanes,
1132-
);
11331108
offscreenSubtreeIsHidden = prevOffscreenSubtreeIsHidden;
11341109
offscreenSubtreeWasHidden = prevOffscreenSubtreeWasHidden;
11351110
}
@@ -2723,90 +2698,177 @@ function recursivelyTraverseDisappearLayoutEffects(parentFiber: Fiber) {
27232698
}
27242699
}
27252700

2726-
function reappearLayoutEffects(finishedWork: Fiber) {
2701+
function reappearLayoutEffects(
2702+
finishedRoot: FiberRoot,
2703+
current: Fiber | null,
2704+
finishedWork: Fiber,
2705+
committedLanes: Lanes,
2706+
// This function visits both newly finished work and nodes that were re-used
2707+
// from a previously committed tree. We cannot check non-static flags if the
2708+
// node was reused.
2709+
includeWorkInProgressEffects: boolean,
2710+
) {
27272711
// Turn on layout effects in a tree that previously disappeared.
2728-
// TODO (Offscreen) Check: flags & LayoutStatic
2712+
const flags = finishedWork.flags;
27292713
switch (finishedWork.tag) {
27302714
case FunctionComponent:
27312715
case ForwardRef:
27322716
case SimpleMemoComponent: {
2733-
recursivelyTraverseReappearLayoutEffects(finishedWork);
2734-
2735-
// TODO: Check for LayoutStatic flag
2736-
if (
2737-
enableProfilerTimer &&
2738-
enableProfilerCommitHooks &&
2739-
finishedWork.mode & ProfileMode
2740-
) {
2741-
try {
2742-
startLayoutEffectTimer();
2743-
safelyCallCommitHookLayoutEffectListMount(
2744-
finishedWork,
2745-
finishedWork.return,
2746-
);
2747-
} finally {
2748-
recordLayoutEffectDuration(finishedWork);
2749-
}
2750-
} else {
2751-
safelyCallCommitHookLayoutEffectListMount(
2752-
finishedWork,
2753-
finishedWork.return,
2754-
);
2755-
}
2717+
recursivelyTraverseReappearLayoutEffects(
2718+
finishedRoot,
2719+
finishedWork,
2720+
committedLanes,
2721+
includeWorkInProgressEffects,
2722+
);
2723+
// TODO: Check flags & LayoutStatic
2724+
commitHookLayoutEffects(finishedWork, HookLayout);
27562725
break;
27572726
}
27582727
case ClassComponent: {
2759-
recursivelyTraverseReappearLayoutEffects(finishedWork);
2728+
recursivelyTraverseReappearLayoutEffects(
2729+
finishedRoot,
2730+
finishedWork,
2731+
committedLanes,
2732+
includeWorkInProgressEffects,
2733+
);
27602734

2761-
const instance = finishedWork.stateNode;
27622735
// TODO: Check for LayoutStatic flag
2736+
const instance = finishedWork.stateNode;
27632737
if (typeof instance.componentDidMount === 'function') {
2764-
safelyCallComponentDidMount(
2765-
finishedWork,
2766-
finishedWork.return,
2767-
instance,
2768-
);
2738+
try {
2739+
instance.componentDidMount();
2740+
} catch (error) {
2741+
captureCommitPhaseError(finishedWork, finishedWork.return, error);
2742+
}
27692743
}
2770-
// TODO: Check for RefStatic flag
2771-
safelyAttachRef(finishedWork, finishedWork.return);
2744+
2745+
// Commit any callbacks that would have fired while the component
2746+
// was hidden.
27722747
const updateQueue: UpdateQueue<
27732748
*,
27742749
> | null = (finishedWork.updateQueue: any);
27752750
if (updateQueue !== null) {
27762751
commitHiddenCallbacks(updateQueue, instance);
27772752
}
2753+
2754+
// If this is newly finished work, check for setState callbacks
2755+
if (includeWorkInProgressEffects && flags & Callback) {
2756+
commitClassCallbacks(finishedWork);
2757+
}
2758+
2759+
// TODO: Check flags & RefStatic
2760+
safelyAttachRef(finishedWork, finishedWork.return);
27782761
break;
27792762
}
2763+
// Unlike commitLayoutEffectsOnFiber, we don't need to handle HostRoot
2764+
// because this function only visits nodes that are inside an
2765+
// Offscreen fiber.
2766+
// case HostRoot: {
2767+
// ...
2768+
// }
27802769
case HostComponent: {
2781-
recursivelyTraverseReappearLayoutEffects(finishedWork);
2770+
recursivelyTraverseReappearLayoutEffects(
2771+
finishedRoot,
2772+
finishedWork,
2773+
committedLanes,
2774+
includeWorkInProgressEffects,
2775+
);
27822776

2783-
// TODO: Check for RefStatic flag
2777+
// Renderers may schedule work to be done after host components are mounted
2778+
// (eg DOM renderer may schedule auto-focus for inputs and form controls).
2779+
// These effects should only be committed when components are first mounted,
2780+
// aka when there is no current/alternate.
2781+
if (includeWorkInProgressEffects && current === null && flags & Update) {
2782+
commitHostComponentMount(finishedWork);
2783+
}
2784+
2785+
// TODO: Check flags & Ref
27842786
safelyAttachRef(finishedWork, finishedWork.return);
27852787
break;
27862788
}
2789+
case Profiler: {
2790+
recursivelyTraverseReappearLayoutEffects(
2791+
finishedRoot,
2792+
finishedWork,
2793+
committedLanes,
2794+
includeWorkInProgressEffects,
2795+
);
2796+
// TODO: Figure out how Profiler updates should work with Offscreen
2797+
if (includeWorkInProgressEffects && flags & Update) {
2798+
commitProfilerUpdate(finishedWork, current);
2799+
}
2800+
break;
2801+
}
2802+
case SuspenseComponent: {
2803+
recursivelyTraverseReappearLayoutEffects(
2804+
finishedRoot,
2805+
finishedWork,
2806+
committedLanes,
2807+
includeWorkInProgressEffects,
2808+
);
2809+
2810+
// TODO: Figure out how Suspense hydration callbacks should work
2811+
// with Offscreen.
2812+
if (includeWorkInProgressEffects && flags & Update) {
2813+
commitSuspenseHydrationCallbacks(finishedRoot, finishedWork);
2814+
}
2815+
break;
2816+
}
27872817
case OffscreenComponent: {
2788-
const isHidden = finishedWork.memoizedState !== null;
2818+
const offscreenState: OffscreenState = finishedWork.memoizedState;
2819+
const isHidden = offscreenState !== null;
27892820
if (isHidden) {
27902821
// Nested Offscreen tree is still hidden. Don't re-appear its effects.
27912822
} else {
2792-
recursivelyTraverseReappearLayoutEffects(finishedWork);
2823+
recursivelyTraverseReappearLayoutEffects(
2824+
finishedRoot,
2825+
finishedWork,
2826+
committedLanes,
2827+
includeWorkInProgressEffects,
2828+
);
27932829
}
27942830
break;
27952831
}
27962832
default: {
2797-
recursivelyTraverseReappearLayoutEffects(finishedWork);
2833+
recursivelyTraverseReappearLayoutEffects(
2834+
finishedRoot,
2835+
finishedWork,
2836+
committedLanes,
2837+
includeWorkInProgressEffects,
2838+
);
27982839
break;
27992840
}
28002841
}
28012842
}
28022843

2803-
function recursivelyTraverseReappearLayoutEffects(parentFiber: Fiber) {
2844+
function recursivelyTraverseReappearLayoutEffects(
2845+
finishedRoot: FiberRoot,
2846+
parentFiber: Fiber,
2847+
committedLanes: Lanes,
2848+
includeWorkInProgressEffects: boolean,
2849+
) {
2850+
// This function visits both newly finished work and nodes that were re-used
2851+
// from a previously committed tree. We cannot check non-static flags if the
2852+
// node was reused.
2853+
const childShouldIncludeWorkInProgressEffects =
2854+
includeWorkInProgressEffects &&
2855+
(parentFiber.subtreeFlags & LayoutMask) !== NoFlags;
2856+
28042857
// TODO (Offscreen) Check: flags & (RefStatic | LayoutStatic)
2858+
const prevDebugFiber = getCurrentDebugFiberInDEV();
28052859
let child = parentFiber.child;
28062860
while (child !== null) {
2807-
reappearLayoutEffects(child);
2861+
const current = child.alternate;
2862+
reappearLayoutEffects(
2863+
finishedRoot,
2864+
current,
2865+
child,
2866+
committedLanes,
2867+
childShouldIncludeWorkInProgressEffects,
2868+
);
28082869
child = child.sibling;
28092870
}
2871+
setCurrentDebugFiberInDEV(prevDebugFiber);
28102872
}
28112873

28122874
export function commitPassiveMountEffects(

0 commit comments

Comments
 (0)