@samitouri / QOS-React-2 / commits / cfb6cfa250

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 });