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.
Andrew Clark committed
Jul 15, 2022 at 12:49 UTC
cfb6cfa25031992569a328530b4fb8612a5d377b
3 files changed
+489
-194
packages/react-reconciler/src/ReactFiberCommitWork.new.js
+159
-97
@@ -225,18 +225,6 @@ const callComponentWillUnmountWithTimer = function(current, instance) {
225
}
226
};
227
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
-
228
// Capture errors so they don't interrupt unmounting.
229
function safelyCallComponentWillUnmount(
230
current: Fiber,
@@ -250,19 +238,6 @@ function safelyCallComponentWillUnmount(
238
}
239
}
240
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
-
241
// Capture errors so they don't interrupt mounting.
242
function safelyAttachRef(current: Fiber, nearestMountedAncestor: Fiber | null) {
243
try {
@@ -706,7 +681,7 @@ export function commitPassiveEffectDurations(
681
}
682
}
683
709
-function commitHookLayoutEffects(finishedWork: Fiber) {
684
+function commitHookLayoutEffects(finishedWork: Fiber, hookFlags: HookFlags) {
685
// At this point layout effects have already been destroyed (during mutation phase).
686
// This is done to prevent sibling component effects from interfering with each other,
687
// e.g. a destroy function in one component should never override a ref set
@@ -718,14 +693,14 @@ function commitHookLayoutEffects(finishedWork: Fiber) {
693
) {
694
try {
695
startLayoutEffectTimer();
721
- commitHookEffectListMount(HookLayout | HookHasEffect, finishedWork);
696
+ commitHookEffectListMount(hookFlags, finishedWork);
697
} catch (error) {
698
captureCommitPhaseError(finishedWork, finishedWork.return, error);
699
}
700
recordLayoutEffectDuration(finishedWork);
701
} else {
702
try {
728
- commitHookEffectListMount(HookLayout | HookHasEffect, finishedWork);
703
+ commitHookEffectListMount(hookFlags, finishedWork);
704
} catch (error) {
705
captureCommitPhaseError(finishedWork, finishedWork.return, error);
706
}
@@ -853,7 +828,7 @@ function commitClassLayoutLifecycles(
828
}
829
}
830
856
-function commitClassCallbacks(finishedWork: Fiber, current: Fiber | null) {
831
+function commitClassCallbacks(finishedWork: Fiber) {
832
// TODO: I think this is now always non-null by the time it reaches the
833
// commit phase. Consider removing the type check.
834
const updateQueue: UpdateQueue<*> | null = (finishedWork.updateQueue: any);
@@ -897,7 +872,7 @@ function commitClassCallbacks(finishedWork: Fiber, current: Fiber | null) {
872
}
873
}
874
900
-function commitHostComponentMount(finishedWork: Fiber, current: Fiber | null) {
875
+function commitHostComponentMount(finishedWork: Fiber) {
876
const type = finishedWork.type;
877
const props = finishedWork.memoizedProps;
878
const instance: Instance = finishedWork.stateNode;
@@ -978,6 +953,8 @@ function commitLayoutEffectOnFiber(
953
finishedWork: Fiber,
954
committedLanes: Lanes,
955
): 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.
958
const flags = finishedWork.flags;
959
switch (finishedWork.tag) {
960
case FunctionComponent:
@@ -989,9 +966,7 @@ function commitLayoutEffectOnFiber(
966
committedLanes,
967
);
968
if (flags & Update) {
992
- if (!offscreenSubtreeWasHidden) {
993
- commitHookLayoutEffects(finishedWork);
994
- }
969
+ commitHookLayoutEffects(finishedWork, HookLayout | HookHasEffect);
970
}
971
break;
972
}
@@ -1002,19 +977,15 @@ function commitLayoutEffectOnFiber(
977
committedLanes,
978
);
979
if (flags & Update) {
1005
- if (!offscreenSubtreeWasHidden) {
1006
- commitClassLayoutLifecycles(finishedWork, current);
1007
- }
980
+ commitClassLayoutLifecycles(finishedWork, current);
981
}
982
983
if (flags & Callback) {
1011
- commitClassCallbacks(finishedWork, current);
984
+ commitClassCallbacks(finishedWork);
985
}
986
987
if (flags & Ref) {
1015
- if (!offscreenSubtreeWasHidden) {
1016
- safelyAttachRef(finishedWork, finishedWork.return);
1017
- }
988
+ safelyAttachRef(finishedWork, finishedWork.return);
989
}
990
break;
991
}
@@ -1063,13 +1034,11 @@ function commitLayoutEffectOnFiber(
1034
// These effects should only be committed when components are first mounted,
1035
// aka when there is no current/alternate.
1036
if (current === null && flags & Update) {
1066
- commitHostComponentMount(finishedWork, current);
1037
+ commitHostComponentMount(finishedWork);
1038
}
1039
1040
if (flags & Ref) {
1070
- if (!offscreenSubtreeWasHidden) {
1071
- safelyAttachRef(finishedWork, finishedWork.return);
1072
- }
1041
+ safelyAttachRef(finishedWork, finishedWork.return);
1042
}
1043
break;
1044
}
@@ -1117,19 +1086,25 @@ function commitLayoutEffectOnFiber(
1086
offscreenSubtreeWasHidden = newOffscreenSubtreeWasHidden;
1087
1088
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
+ );
1107
}
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
- );
1108
offscreenSubtreeIsHidden = prevOffscreenSubtreeIsHidden;
1109
offscreenSubtreeWasHidden = prevOffscreenSubtreeWasHidden;
1110
}
@@ -2723,90 +2698,177 @@ function recursivelyTraverseDisappearLayoutEffects(parentFiber: Fiber) {
2698
}
2699
}
2700
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
+) {
2711
// Turn on layout effects in a tree that previously disappeared.
2728
- // TODO (Offscreen) Check: flags & LayoutStatic
2712
+ const flags = finishedWork.flags;
2713
switch (finishedWork.tag) {
2714
case FunctionComponent:
2715
case ForwardRef:
2716
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);
2725
break;
2726
}
2727
case ClassComponent: {
2759
- recursivelyTraverseReappearLayoutEffects(finishedWork);
2728
+ recursivelyTraverseReappearLayoutEffects(
2729
+ finishedRoot,
2730
+ finishedWork,
2731
+ committedLanes,
2732
+ includeWorkInProgressEffects,
2733
+ );
2734
2761
- const instance = finishedWork.stateNode;
2735
// TODO: Check for LayoutStatic flag
2736
+ const instance = finishedWork.stateNode;
2737
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
+ }
2743
}
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.
2747
const updateQueue: UpdateQueue<
2748
*,
2749
> | null = (finishedWork.updateQueue: any);
2750
if (updateQueue !== null) {
2751
commitHiddenCallbacks(updateQueue, instance);
2752
}
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);
2761
break;
2762
}
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
+ // }
2769
case HostComponent: {
2781
- recursivelyTraverseReappearLayoutEffects(finishedWork);
2770
+ recursivelyTraverseReappearLayoutEffects(
2771
+ finishedRoot,
2772
+ finishedWork,
2773
+ committedLanes,
2774
+ includeWorkInProgressEffects,
2775
+ );
2776
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
2786
safelyAttachRef(finishedWork, finishedWork.return);
2787
break;
2788
}
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
+ }
2817
case OffscreenComponent: {
2788
- const isHidden = finishedWork.memoizedState !== null;
2818
+ const offscreenState: OffscreenState = finishedWork.memoizedState;
2819
+ const isHidden = offscreenState !== null;
2820
if (isHidden) {
2821
// Nested Offscreen tree is still hidden. Don't re-appear its effects.
2822
} else {
2792
- recursivelyTraverseReappearLayoutEffects(finishedWork);
2823
+ recursivelyTraverseReappearLayoutEffects(
2824
+ finishedRoot,
2825
+ finishedWork,
2826
+ committedLanes,
2827
+ includeWorkInProgressEffects,
2828
+ );
2829
}
2830
break;
2831
}
2832
default: {
2797
- recursivelyTraverseReappearLayoutEffects(finishedWork);
2833
+ recursivelyTraverseReappearLayoutEffects(
2834
+ finishedRoot,
2835
+ finishedWork,
2836
+ committedLanes,
2837
+ includeWorkInProgressEffects,
2838
+ );
2839
break;
2840
}
2841
}
2842
}
2843
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
+
2857
// TODO (Offscreen) Check: flags & (RefStatic | LayoutStatic)
2858
+ const prevDebugFiber = getCurrentDebugFiberInDEV();
2859
let child = parentFiber.child;
2860
while (child !== null) {
2807
- reappearLayoutEffects(child);
2861
+ const current = child.alternate;
2862
+ reappearLayoutEffects(
2863
+ finishedRoot,
2864
+ current,
2865
+ child,
2866
+ committedLanes,
2867
+ childShouldIncludeWorkInProgressEffects,
2868
+ );
2869
child = child.sibling;
2870
}
2871
+ setCurrentDebugFiberInDEV(prevDebugFiber);
2872
}
2873
2874
export function commitPassiveMountEffects(
packages/react-reconciler/src/ReactFiberCommitWork.old.js
+159
-97
@@ -225,18 +225,6 @@ const callComponentWillUnmountWithTimer = function(current, instance) {
225
}
226
};
227
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
-
228
// Capture errors so they don't interrupt unmounting.
229
function safelyCallComponentWillUnmount(
230
current: Fiber,
@@ -250,19 +238,6 @@ function safelyCallComponentWillUnmount(
238
}
239
}
240
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
-
241
// Capture errors so they don't interrupt mounting.
242
function safelyAttachRef(current: Fiber, nearestMountedAncestor: Fiber | null) {
243
try {
@@ -706,7 +681,7 @@ export function commitPassiveEffectDurations(
681
}
682
}
683
709
-function commitHookLayoutEffects(finishedWork: Fiber) {
684
+function commitHookLayoutEffects(finishedWork: Fiber, hookFlags: HookFlags) {
685
// At this point layout effects have already been destroyed (during mutation phase).
686
// This is done to prevent sibling component effects from interfering with each other,
687
// e.g. a destroy function in one component should never override a ref set
@@ -718,14 +693,14 @@ function commitHookLayoutEffects(finishedWork: Fiber) {
693
) {
694
try {
695
startLayoutEffectTimer();
721
- commitHookEffectListMount(HookLayout | HookHasEffect, finishedWork);
696
+ commitHookEffectListMount(hookFlags, finishedWork);
697
} catch (error) {
698
captureCommitPhaseError(finishedWork, finishedWork.return, error);
699
}
700
recordLayoutEffectDuration(finishedWork);
701
} else {
702
try {
728
- commitHookEffectListMount(HookLayout | HookHasEffect, finishedWork);
703
+ commitHookEffectListMount(hookFlags, finishedWork);
704
} catch (error) {
705
captureCommitPhaseError(finishedWork, finishedWork.return, error);
706
}
@@ -853,7 +828,7 @@ function commitClassLayoutLifecycles(
828
}
829
}
830
856
-function commitClassCallbacks(finishedWork: Fiber, current: Fiber | null) {
831
+function commitClassCallbacks(finishedWork: Fiber) {
832
// TODO: I think this is now always non-null by the time it reaches the
833
// commit phase. Consider removing the type check.
834
const updateQueue: UpdateQueue<*> | null = (finishedWork.updateQueue: any);
@@ -897,7 +872,7 @@ function commitClassCallbacks(finishedWork: Fiber, current: Fiber | null) {
872
}
873
}
874
900
-function commitHostComponentMount(finishedWork: Fiber, current: Fiber | null) {
875
+function commitHostComponentMount(finishedWork: Fiber) {
876
const type = finishedWork.type;
877
const props = finishedWork.memoizedProps;
878
const instance: Instance = finishedWork.stateNode;
@@ -978,6 +953,8 @@ function commitLayoutEffectOnFiber(
953
finishedWork: Fiber,
954
committedLanes: Lanes,
955
): 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.
958
const flags = finishedWork.flags;
959
switch (finishedWork.tag) {
960
case FunctionComponent:
@@ -989,9 +966,7 @@ function commitLayoutEffectOnFiber(
966
committedLanes,
967
);
968
if (flags & Update) {
992
- if (!offscreenSubtreeWasHidden) {
993
- commitHookLayoutEffects(finishedWork);
994
- }
969
+ commitHookLayoutEffects(finishedWork, HookLayout | HookHasEffect);
970
}
971
break;
972
}
@@ -1002,19 +977,15 @@ function commitLayoutEffectOnFiber(
977
committedLanes,
978
);
979
if (flags & Update) {
1005
- if (!offscreenSubtreeWasHidden) {
1006
- commitClassLayoutLifecycles(finishedWork, current);
1007
- }
980
+ commitClassLayoutLifecycles(finishedWork, current);
981
}
982
983
if (flags & Callback) {
1011
- commitClassCallbacks(finishedWork, current);
984
+ commitClassCallbacks(finishedWork);
985
}
986
987
if (flags & Ref) {
1015
- if (!offscreenSubtreeWasHidden) {
1016
- safelyAttachRef(finishedWork, finishedWork.return);
1017
- }
988
+ safelyAttachRef(finishedWork, finishedWork.return);
989
}
990
break;
991
}
@@ -1063,13 +1034,11 @@ function commitLayoutEffectOnFiber(
1034
// These effects should only be committed when components are first mounted,
1035
// aka when there is no current/alternate.
1036
if (current === null && flags & Update) {
1066
- commitHostComponentMount(finishedWork, current);
1037
+ commitHostComponentMount(finishedWork);
1038
}
1039
1040
if (flags & Ref) {
1070
- if (!offscreenSubtreeWasHidden) {
1071
- safelyAttachRef(finishedWork, finishedWork.return);
1072
- }
1041
+ safelyAttachRef(finishedWork, finishedWork.return);
1042
}
1043
break;
1044
}
@@ -1117,19 +1086,25 @@ function commitLayoutEffectOnFiber(
1086
offscreenSubtreeWasHidden = newOffscreenSubtreeWasHidden;
1087
1088
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
+ );
1107
}
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
- );
1108
offscreenSubtreeIsHidden = prevOffscreenSubtreeIsHidden;
1109
offscreenSubtreeWasHidden = prevOffscreenSubtreeWasHidden;
1110
}
@@ -2723,90 +2698,177 @@ function recursivelyTraverseDisappearLayoutEffects(parentFiber: Fiber) {
2698
}
2699
}
2700
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
+) {
2711
// Turn on layout effects in a tree that previously disappeared.
2728
- // TODO (Offscreen) Check: flags & LayoutStatic
2712
+ const flags = finishedWork.flags;
2713
switch (finishedWork.tag) {
2714
case FunctionComponent:
2715
case ForwardRef:
2716
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);
2725
break;
2726
}
2727
case ClassComponent: {
2759
- recursivelyTraverseReappearLayoutEffects(finishedWork);
2728
+ recursivelyTraverseReappearLayoutEffects(
2729
+ finishedRoot,
2730
+ finishedWork,
2731
+ committedLanes,
2732
+ includeWorkInProgressEffects,
2733
+ );
2734
2761
- const instance = finishedWork.stateNode;
2735
// TODO: Check for LayoutStatic flag
2736
+ const instance = finishedWork.stateNode;
2737
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
+ }
2743
}
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.
2747
const updateQueue: UpdateQueue<
2748
*,
2749
> | null = (finishedWork.updateQueue: any);
2750
if (updateQueue !== null) {
2751
commitHiddenCallbacks(updateQueue, instance);
2752
}
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);
2761
break;
2762
}
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
+ // }
2769
case HostComponent: {
2781
- recursivelyTraverseReappearLayoutEffects(finishedWork);
2770
+ recursivelyTraverseReappearLayoutEffects(
2771
+ finishedRoot,
2772
+ finishedWork,
2773
+ committedLanes,
2774
+ includeWorkInProgressEffects,
2775
+ );
2776
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
2786
safelyAttachRef(finishedWork, finishedWork.return);
2787
break;
2788
}
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
+ }
2817
case OffscreenComponent: {
2788
- const isHidden = finishedWork.memoizedState !== null;
2818
+ const offscreenState: OffscreenState = finishedWork.memoizedState;
2819
+ const isHidden = offscreenState !== null;
2820
if (isHidden) {
2821
// Nested Offscreen tree is still hidden. Don't re-appear its effects.
2822
} else {
2792
- recursivelyTraverseReappearLayoutEffects(finishedWork);
2823
+ recursivelyTraverseReappearLayoutEffects(
2824
+ finishedRoot,
2825
+ finishedWork,
2826
+ committedLanes,
2827
+ includeWorkInProgressEffects,
2828
+ );
2829
}
2830
break;
2831
}
2832
default: {
2797
- recursivelyTraverseReappearLayoutEffects(finishedWork);
2833
+ recursivelyTraverseReappearLayoutEffects(
2834
+ finishedRoot,
2835
+ finishedWork,
2836
+ committedLanes,
2837
+ includeWorkInProgressEffects,
2838
+ );
2839
break;
2840
}
2841
}
2842
}
2843
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
+
2857
// TODO (Offscreen) Check: flags & (RefStatic | LayoutStatic)
2858
+ const prevDebugFiber = getCurrentDebugFiberInDEV();
2859
let child = parentFiber.child;
2860
while (child !== null) {
2807
- reappearLayoutEffects(child);
2861
+ const current = child.alternate;
2862
+ reappearLayoutEffects(
2863
+ finishedRoot,
2864
+ current,
2865
+ child,
2866
+ committedLanes,
2867
+ childShouldIncludeWorkInProgressEffects,
2868
+ );
2869
child = child.sibling;
2870
}
2871
+ setCurrentDebugFiberInDEV(prevDebugFiber);
2872
}
2873
2874
export function commitPassiveMountEffects(
packages/react-reconciler/src/__tests__/ReactOffscreen-test.js
+171
@@ -696,4 +696,175 @@ describe('ReactOffscreen', () => {
696
]);
697
expect(root).toMatchRenderedOutput(<span prop="C" />);
698
});
699
+
700
+ // @gate enableOffscreen
701
+ it('does not call componentDidUpdate when reappearing a hidden class component', async () => {
702
+ class Child extends React.Component {
703
+ componentDidMount() {
704
+ Scheduler.unstable_yieldValue('componentDidMount');
705
+ }
706
+ componentDidUpdate() {
707
+ Scheduler.unstable_yieldValue('componentDidUpdate');
708
+ }
709
+ componentWillUnmount() {
710
+ Scheduler.unstable_yieldValue('componentWillUnmount');
711
+ }
712
+ render() {
713
+ return 'Child';
714
+ }
715
+ }
716
+
717
+ // Initial mount
718
+ const root = ReactNoop.createRoot();
719
+ await act(async () => {
720
+ root.render(
721
+ <Offscreen mode="visible">
722
+ <Child />
723
+ </Offscreen>,
724
+ );
725
+ });
726
+ expect(Scheduler).toHaveYielded(['componentDidMount']);
727
+
728
+ // Hide the class component
729
+ await act(async () => {
730
+ root.render(
731
+ <Offscreen mode="hidden">
732
+ <Child />
733
+ </Offscreen>,
734
+ );
735
+ });
736
+ expect(Scheduler).toHaveYielded(['componentWillUnmount']);
737
+
738
+ // Reappear the class component. componentDidMount should fire, not
739
+ // componentDidUpdate.
740
+ await act(async () => {
741
+ root.render(
742
+ <Offscreen mode="visible">
743
+ <Child />
744
+ </Offscreen>,
745
+ );
746
+ });
747
+ expect(Scheduler).toHaveYielded(['componentDidMount']);
748
+ });
749
+
750
+ // @gate enableOffscreen
751
+ it(
752
+ 'when reusing old components (hidden -> visible), layout effects fire ' +
753
+ 'with same timing as if it were brand new',
754
+ async () => {
755
+ function Child({label}) {
756
+ useLayoutEffect(() => {
757
+ Scheduler.unstable_yieldValue('Mount ' + label);
758
+ return () => {
759
+ Scheduler.unstable_yieldValue('Unmount ' + label);
760
+ };
761
+ }, [label]);
762
+ return label;
763
+ }
764
+
765
+ // Initial mount
766
+ const root = ReactNoop.createRoot();
767
+ await act(async () => {
768
+ root.render(
769
+ <Offscreen mode="visible">
770
+ <Child key="B" label="B" />
771
+ </Offscreen>,
772
+ );
773
+ });
774
+ expect(Scheduler).toHaveYielded(['Mount B']);
775
+
776
+ // Hide the component
777
+ await act(async () => {
778
+ root.render(
779
+ <Offscreen mode="hidden">
780
+ <Child key="B" label="B" />
781
+ </Offscreen>,
782
+ );
783
+ });
784
+ expect(Scheduler).toHaveYielded(['Unmount B']);
785
+
786
+ // Reappear the component and also add some new siblings.
787
+ await act(async () => {
788
+ root.render(
789
+ <Offscreen mode="visible">
790
+ <Child key="A" label="A" />
791
+ <Child key="B" label="B" />
792
+ <Child key="C" label="C" />
793
+ </Offscreen>,
794
+ );
795
+ });
796
+ // B's effect should fire in between A and C even though it's been reused
797
+ // from a previous render. In other words, it's the same order as if all
798
+ // three siblings were brand new.
799
+ expect(Scheduler).toHaveYielded(['Mount A', 'Mount B', 'Mount C']);
800
+ },
801
+ );
802
+
803
+ // @gate enableOffscreen
804
+ it(
805
+ 'when reusing old components (hidden -> visible), layout effects fire ' +
806
+ 'with same timing as if it were brand new (includes setState callback)',
807
+ async () => {
808
+ class Child extends React.Component {
809
+ componentDidMount() {
810
+ Scheduler.unstable_yieldValue('Mount ' + this.props.label);
811
+ }
812
+ componentWillUnmount() {
813
+ Scheduler.unstable_yieldValue('Unmount ' + this.props.label);
814
+ }
815
+ render() {
816
+ return this.props.label;
817
+ }
818
+ }
819
+
820
+ // Initial mount
821
+ const bRef = React.createRef();
822
+ const root = ReactNoop.createRoot();
823
+ await act(async () => {
824
+ root.render(
825
+ <Offscreen mode="visible">
826
+ <Child key="B" ref={bRef} label="B" />
827
+ </Offscreen>,
828
+ );
829
+ });
830
+ expect(Scheduler).toHaveYielded(['Mount B']);
831
+
832
+ // We're going to schedule an update on a hidden component, so stash a
833
+ // reference to its setState before the ref gets detached
834
+ const setStateB = bRef.current.setState.bind(bRef.current);
835
+
836
+ // Hide the component
837
+ await act(async () => {
838
+ root.render(
839
+ <Offscreen mode="hidden">
840
+ <Child key="B" ref={bRef} label="B" />
841
+ </Offscreen>,
842
+ );
843
+ });
844
+ expect(Scheduler).toHaveYielded(['Unmount B']);
845
+
846
+ // Reappear the component and also add some new siblings.
847
+ await act(async () => {
848
+ setStateB(null, () => {
849
+ Scheduler.unstable_yieldValue('setState callback B');
850
+ });
851
+ root.render(
852
+ <Offscreen mode="visible">
853
+ <Child key="A" label="A" />
854
+ <Child key="B" ref={bRef} label="B" />
855
+ <Child key="C" label="C" />
856
+ </Offscreen>,
857
+ );
858
+ });
859
+ // B's effect should fire in between A and C even though it's been reused
860
+ // from a previous render. In other words, it's the same order as if all
861
+ // three siblings were brand new.
862
+ expect(Scheduler).toHaveYielded([
863
+ 'Mount A',
864
+ 'Mount B',
865
+ 'setState callback B',
866
+ 'Mount C',
867
+ ]);
868
+ },
869
+ );
870
});