@samitouri / QOS-React-2 / commits / 12a738f1a8

[Transition Tracing] Add Support for Multiple Transitions on Root (#24732)

We can think of transitions on the root as a bunch of tracing markers. Therefore, we can map each transition to a map of pending suspense boundaries. When a transition's pending suspense boundary map is empty, we know that it's complete. This PR: * Combines the `pendingSuspenseBoundaries` and `transitions` into one `incompleteTransitions` object. This object is a map from a `transition` to a map of `pendingSuspenseBoundaries` * Refactored code to make it so that every transition has its own `pendingSuspenseBoundaries` map rather than sharing just one. * Moves the transition complete callback to the root. Alternatively, we can also keep a map of pendingSuspenseBoundaries to transitions on the Offscreen marker, but it's simpler to just call the transition complete callback on the root instead. We also only do this if there are transitions pending, so it shouldn't make too big of a difference

Luna Ruan committed Jun 17, 2022 at 16:43 UTC 12a738f1a87889bb5f7c4159641573fd04140664
10 files changed +394 -198
packages/react-reconciler/src/ReactFiber.new.js
+2
@@ -717,6 +717,8 @@ export function createFiberFromOffscreen(
717 fiber.lanes = lanes;
718 const primaryChildInstance: OffscreenInstance = {
719 isHidden: false,
720 + pendingMarkers: null,
721 + transitions: null,
722 };
723 fiber.stateNode = primaryChildInstance;
724 return fiber;
packages/react-reconciler/src/ReactFiber.old.js
+2
@@ -717,6 +717,8 @@ export function createFiberFromOffscreen(
717 fiber.lanes = lanes;
718 const primaryChildInstance: OffscreenInstance = {
719 isHidden: false,
720 + pendingMarkers: null,
721 + transitions: null,
722 };
723 fiber.stateNode = primaryChildInstance;
724 return fiber;
packages/react-reconciler/src/ReactFiberBeginWork.new.js
+3 -12
@@ -675,7 +675,6 @@ function updateOffscreenComponent(
675 const nextState: OffscreenState = {
676 baseLanes: NoLanes,
677 cachePool: null,
678 - transitions: null,
678 };
679 workInProgress.memoizedState = nextState;
680 if (enableCache) {
@@ -709,7 +708,6 @@ function updateOffscreenComponent(
708 const nextState: OffscreenState = {
709 baseLanes: nextBaseLanes,
710 cachePool: spawnedCachePool,
712 - transitions: null,
711 };
712 workInProgress.memoizedState = nextState;
713 workInProgress.updateQueue = null;
@@ -745,7 +743,6 @@ function updateOffscreenComponent(
743 const nextState: OffscreenState = {
744 baseLanes: NoLanes,
745 cachePool: null,
748 - transitions: null,
746 };
747 workInProgress.memoizedState = nextState;
748 // Push the lanes that were skipped when we bailed out.
@@ -780,13 +777,10 @@ function updateOffscreenComponent(
777 }
778
779 let transitions = null;
783 - if (
784 - workInProgress.memoizedState !== null &&
785 - workInProgress.memoizedState.transitions !== null
786 - ) {
780 + if (enableTransitionTracing) {
781 // We have now gone from hidden to visible, so any transitions should
782 // be added to the stack to get added to any Offscreen/suspense children
789 - transitions = workInProgress.memoizedState.transitions;
783 + transitions = workInProgress.stateNode.transitions;
784 }
785
786 pushTransition(workInProgress, prevCachePool, transitions);
@@ -1323,8 +1317,7 @@ function updateHostRoot(current, workInProgress, renderLanes) {
1317 element: nextChildren,
1318 isDehydrated: false,
1319 cache: nextState.cache,
1326 - pendingSuspenseBoundaries: nextState.pendingSuspenseBoundaries,
1327 - transitions: nextState.transitions,
1320 + incompleteTransitions: nextState.incompleteTransitions,
1321 };
1322 const updateQueue: UpdateQueue<RootState> = (workInProgress.updateQueue: any);
1323 // `baseState` can always be the last state because the root doesn't
@@ -1920,7 +1913,6 @@ function mountSuspenseOffscreenState(renderLanes: Lanes): OffscreenState {
1913 return {
1914 baseLanes: renderLanes,
1915 cachePool: getSuspendedCache(),
1923 - transitions: null,
1916 };
1917 }
1918
@@ -1955,7 +1947,6 @@ function updateSuspenseOffscreenState(
1947 return {
1948 baseLanes: mergeLanes(prevOffscreenState.baseLanes, renderLanes),
1949 cachePool,
1958 - transitions: prevOffscreenState.transitions,
1950 };
1951 }
1952
packages/react-reconciler/src/ReactFiberBeginWork.old.js
+3 -12
@@ -675,7 +675,6 @@ function updateOffscreenComponent(
675 const nextState: OffscreenState = {
676 baseLanes: NoLanes,
677 cachePool: null,
678 - transitions: null,
678 };
679 workInProgress.memoizedState = nextState;
680 if (enableCache) {
@@ -709,7 +708,6 @@ function updateOffscreenComponent(
708 const nextState: OffscreenState = {
709 baseLanes: nextBaseLanes,
710 cachePool: spawnedCachePool,
712 - transitions: null,
711 };
712 workInProgress.memoizedState = nextState;
713 workInProgress.updateQueue = null;
@@ -745,7 +743,6 @@ function updateOffscreenComponent(
743 const nextState: OffscreenState = {
744 baseLanes: NoLanes,
745 cachePool: null,
748 - transitions: null,
746 };
747 workInProgress.memoizedState = nextState;
748 // Push the lanes that were skipped when we bailed out.
@@ -780,13 +777,10 @@ function updateOffscreenComponent(
777 }
778
779 let transitions = null;
783 - if (
784 - workInProgress.memoizedState !== null &&
785 - workInProgress.memoizedState.transitions !== null
786 - ) {
780 + if (enableTransitionTracing) {
781 // We have now gone from hidden to visible, so any transitions should
782 // be added to the stack to get added to any Offscreen/suspense children
789 - transitions = workInProgress.memoizedState.transitions;
783 + transitions = workInProgress.stateNode.transitions;
784 }
785
786 pushTransition(workInProgress, prevCachePool, transitions);
@@ -1323,8 +1317,7 @@ function updateHostRoot(current, workInProgress, renderLanes) {
1317 element: nextChildren,
1318 isDehydrated: false,
1319 cache: nextState.cache,
1326 - pendingSuspenseBoundaries: nextState.pendingSuspenseBoundaries,
1327 - transitions: nextState.transitions,
1320 + incompleteTransitions: nextState.incompleteTransitions,
1321 };
1322 const updateQueue: UpdateQueue<RootState> = (workInProgress.updateQueue: any);
1323 // `baseState` can always be the last state because the root doesn't
@@ -1920,7 +1913,6 @@ function mountSuspenseOffscreenState(renderLanes: Lanes): OffscreenState {
1913 return {
1914 baseLanes: renderLanes,
1915 cachePool: getSuspendedCache(),
1923 - transitions: null,
1916 };
1917 }
1918
@@ -1955,7 +1947,6 @@ function updateSuspenseOffscreenState(
1947 return {
1948 baseLanes: mergeLanes(prevOffscreenState.baseLanes, renderLanes),
1949 cachePool,
1958 - transitions: prevOffscreenState.transitions,
1950 };
1951 }
1952
packages/react-reconciler/src/ReactFiberCommitWork.new.js
+81 -80
@@ -1066,10 +1066,7 @@ function reappearLayoutEffectsOnFiber(node: Fiber) {
1066 }
1067 }
1068
1069 -function commitTransitionProgress(
1070 - finishedRoot: FiberRoot,
1071 - offscreenFiber: Fiber,
1072 -) {
1069 +function commitTransitionProgress(offscreenFiber: Fiber) {
1070 if (enableTransitionTracing) {
1071 // This function adds suspense boundaries to the root
1072 // or tracing marker's pendingSuspenseBoundaries map.
@@ -1094,12 +1091,7 @@ function commitTransitionProgress(
1091 const wasHidden = prevState !== null;
1092 const isHidden = nextState !== null;
1093
1097 - const rootState: RootState = finishedRoot.current.memoizedState;
1098 - // TODO(luna) move pendingSuspenseBoundaries and transitions from
1099 - // HostRoot fiber to FiberRoot
1100 - const rootPendingBoundaries = rootState.pendingSuspenseBoundaries;
1101 - const rootTransitions = rootState.transitions;
1102 -
1094 + const pendingMarkers = offscreenInstance.pendingMarkers;
1095 // If there is a name on the suspense boundary, store that in
1096 // the pending boundaries.
1097 let name = null;
@@ -1112,38 +1104,26 @@ function commitTransitionProgress(
1104 name = parent.memoizedProps.unstable_name;
1105 }
1106
1115 - if (rootPendingBoundaries !== null) {
1116 - if (previousFiber === null) {
1117 - // Initial mount
1118 - if (isHidden) {
1119 - rootPendingBoundaries.set(offscreenInstance, {
1107 + if (!wasHidden && isHidden) {
1108 + // The suspense boundaries was just hidden. Add the boundary
1109 + // to the pending boundary set if it's there
1110 + if (pendingMarkers !== null) {
1111 + pendingMarkers.forEach(pendingBoundaries => {
1112 + pendingBoundaries.set(offscreenInstance, {
1113 name,
1114 });
1122 - }
1123 - } else {
1124 - if (wasHidden && !isHidden) {
1125 - // The suspense boundary went from hidden to visible. Remove
1126 - // the boundary from the pending suspense boundaries set
1127 - // if it's there
1128 - if (rootPendingBoundaries.has(offscreenInstance)) {
1129 - rootPendingBoundaries.delete(offscreenInstance);
1130 -
1131 - if (rootPendingBoundaries.size === 0 && rootTransitions !== null) {
1132 - rootTransitions.forEach(transition => {
1133 - addTransitionCompleteCallbackToPendingTransition({
1134 - transitionName: transition.name,
1135 - startTime: transition.startTime,
1136 - });
1137 - });
1138 - }
1115 + });
1116 + }
1117 + } else if (wasHidden && !isHidden) {
1118 + // The suspense boundary went from hidden to visible. Remove
1119 + // the boundary from the pending suspense boundaries set
1120 + // if it's there
1121 + if (pendingMarkers !== null) {
1122 + pendingMarkers.forEach(pendingBoundaries => {
1123 + if (pendingBoundaries.has(offscreenInstance)) {
1124 + pendingBoundaries.delete(offscreenInstance);
1125 }
1140 - } else if (!wasHidden && isHidden) {
1141 - // The suspense boundaries was just hidden. Add the boundary
1142 - // to the pending boundary set if it's there
1143 - rootPendingBoundaries.set(offscreenInstance, {
1144 - name,
1145 - });
1146 - }
1126 + });
1127 }
1128 }
1129 }
@@ -2830,45 +2810,46 @@ function commitPassiveMountOnFiber(
2810 // Get the transitions that were initiatized during the render
2811 // and add a start transition callback for each of them
2812 const state = finishedWork.memoizedState;
2833 - // TODO Since it's a mutable field, this should live on the FiberRoot
2834 - if (state.transitions === null) {
2835 - state.transitions = new Set([]);
2836 - }
2837 - const pendingTransitions = state.transitions;
2838 - const pendingSuspenseBoundaries = state.pendingSuspenseBoundaries;
2839 -
2813 + let incompleteTransitions = state.incompleteTransitions;
2814 // Initial render
2815 if (committedTransitions !== null) {
2816 + if (state.incompleteTransitions === null) {
2817 + state.incompleteTransitions = incompleteTransitions = new Map();
2818 + }
2819 +
2820 committedTransitions.forEach(transition => {
2821 addTransitionStartCallbackToPendingTransition({
2822 transitionName: transition.name,
2823 startTime: transition.startTime,
2824 });
2847 - pendingTransitions.add(transition);
2825 +
2826 + if (!incompleteTransitions.has(transition)) {
2827 + incompleteTransitions.set(transition, null);
2828 + }
2829 });
2830
2850 - if (
2851 - pendingSuspenseBoundaries === null ||
2852 - pendingSuspenseBoundaries.size === 0
2853 - ) {
2854 - pendingTransitions.forEach(transition => {
2831 + clearTransitionsForLanes(finishedRoot, committedLanes);
2832 + }
2833 +
2834 + if (incompleteTransitions !== null) {
2835 + incompleteTransitions.forEach((pendingBoundaries, transition) => {
2836 + if (pendingBoundaries === null || pendingBoundaries.size === 0) {
2837 addTransitionCompleteCallbackToPendingTransition({
2838 transitionName: transition.name,
2839 startTime: transition.startTime,
2840 });
2859 - });
2860 - }
2861 -
2862 - clearTransitionsForLanes(finishedRoot, committedLanes);
2841 + incompleteTransitions.delete(transition);
2842 + }
2843 + });
2844 }
2845
2846 // If there are no more pending suspense boundaries we
2847 // clear the transitions because they are all complete.
2848 if (
2868 - pendingSuspenseBoundaries === null ||
2869 - pendingSuspenseBoundaries.size === 0
2849 + incompleteTransitions === null ||
2850 + incompleteTransitions.size === 0
2851 ) {
2871 - state.transitions = null;
2852 + state.incompleteTransitions = null;
2853 }
2854 }
2855 break;
@@ -2909,39 +2890,59 @@ function commitPassiveMountOnFiber(
2890 const isFallback = finishedWork.memoizedState;
2891 const queue = (finishedWork.updateQueue: any);
2892 const rootMemoizedState = finishedRoot.current.memoizedState;
2893 + const instance = finishedWork.stateNode;
2894
2895 if (queue !== null) {
2914 - // We have one instance of the pendingSuspenseBoundaries map.
2915 - // We only need one because we update it during the commit phase.
2916 - // We instantiate a new Map if we haven't already
2917 - if (rootMemoizedState.pendingSuspenseBoundaries === null) {
2918 - rootMemoizedState.pendingSuspenseBoundaries = new Map();
2919 - }
2920 -
2896 if (isFallback) {
2897 const transitions = queue.transitions;
2923 - let prevTransitions = finishedWork.memoizedState.transitions;
2924 - // Add all the transitions saved in the update queue during
2925 - // the render phase (ie the transitions associated with this boundary)
2926 - // into the transitions set.
2927 - if (transitions !== null) {
2928 - if (prevTransitions === null) {
2929 - // We only have one instance of the transitions set
2930 - // because we update it only during the commit phase. We
2931 - // will create the set on a as needed basis in the commit phase
2932 - finishedWork.memoizedState.transitions = prevTransitions = new Set();
2933 - }
2898 + let prevTransitions = instance.transitions;
2899 + let rootIncompleteTransitions =
2900 + rootMemoizedState.incompleteTransitions;
2901 +
2902 + // We lazily instantiate transition tracing relevant maps
2903 + // and sets in the commit phase as we need to use them. We only
2904 + // instantiate them in the fallback phase on an as needed basis
2905 + if (rootMemoizedState.incompleteTransitions === null) {
2906 + // TODO(luna): Move this to the fiber root
2907 + rootMemoizedState.incompleteTransitions = rootIncompleteTransitions = new Map();
2908 + }
2909 + if (instance.pendingMarkers === null) {
2910 + instance.pendingMarkers = new Set();
2911 + }
2912 + if (transitions !== null && prevTransitions === null) {
2913 + instance.transitions = prevTransitions = new Set();
2914 + }
2915
2916 + if (transitions !== null) {
2917 transitions.forEach(transition => {
2918 + // Add all the transitions saved in the update queue during
2919 + // the render phase (ie the transitions associated with this boundary)
2920 + // into the transitions set.
2921 prevTransitions.add(transition);
2922 +
2923 + // Add the root transition's pending suspense boundary set to
2924 + // the queue's marker set. We will iterate through the marker
2925 + // set when we toggle state on the suspense boundary and
2926 + // add or remove the pending suspense boundaries as needed.
2927 + if (!rootIncompleteTransitions.has(transition)) {
2928 + rootIncompleteTransitions.set(transition, new Map());
2929 + }
2930 + instance.pendingMarkers.add(
2931 + rootIncompleteTransitions.get(transition),
2932 + );
2933 });
2934 }
2935 }
2940 - }
2936
2942 - commitTransitionProgress(finishedRoot, finishedWork);
2937 + commitTransitionProgress(finishedWork);
2938
2944 - finishedWork.updateQueue = null;
2939 + if (
2940 + instance.pendingMarkers === null ||
2941 + instance.pendingMarkers.size === 0
2942 + ) {
2943 + finishedWork.updateQueue = null;
2944 + }
2945 + }
2946 }
2947
2948 break;
packages/react-reconciler/src/ReactFiberCommitWork.old.js
+81 -80
@@ -1066,10 +1066,7 @@ function reappearLayoutEffectsOnFiber(node: Fiber) {
1066 }
1067 }
1068
1069 -function commitTransitionProgress(
1070 - finishedRoot: FiberRoot,
1071 - offscreenFiber: Fiber,
1072 -) {
1069 +function commitTransitionProgress(offscreenFiber: Fiber) {
1070 if (enableTransitionTracing) {
1071 // This function adds suspense boundaries to the root
1072 // or tracing marker's pendingSuspenseBoundaries map.
@@ -1094,12 +1091,7 @@ function commitTransitionProgress(
1091 const wasHidden = prevState !== null;
1092 const isHidden = nextState !== null;
1093
1097 - const rootState: RootState = finishedRoot.current.memoizedState;
1098 - // TODO(luna) move pendingSuspenseBoundaries and transitions from
1099 - // HostRoot fiber to FiberRoot
1100 - const rootPendingBoundaries = rootState.pendingSuspenseBoundaries;
1101 - const rootTransitions = rootState.transitions;
1102 -
1094 + const pendingMarkers = offscreenInstance.pendingMarkers;
1095 // If there is a name on the suspense boundary, store that in
1096 // the pending boundaries.
1097 let name = null;
@@ -1112,38 +1104,26 @@ function commitTransitionProgress(
1104 name = parent.memoizedProps.unstable_name;
1105 }
1106
1115 - if (rootPendingBoundaries !== null) {
1116 - if (previousFiber === null) {
1117 - // Initial mount
1118 - if (isHidden) {
1119 - rootPendingBoundaries.set(offscreenInstance, {
1107 + if (!wasHidden && isHidden) {
1108 + // The suspense boundaries was just hidden. Add the boundary
1109 + // to the pending boundary set if it's there
1110 + if (pendingMarkers !== null) {
1111 + pendingMarkers.forEach(pendingBoundaries => {
1112 + pendingBoundaries.set(offscreenInstance, {
1113 name,
1114 });
1122 - }
1123 - } else {
1124 - if (wasHidden && !isHidden) {
1125 - // The suspense boundary went from hidden to visible. Remove
1126 - // the boundary from the pending suspense boundaries set
1127 - // if it's there
1128 - if (rootPendingBoundaries.has(offscreenInstance)) {
1129 - rootPendingBoundaries.delete(offscreenInstance);
1130 -
1131 - if (rootPendingBoundaries.size === 0 && rootTransitions !== null) {
1132 - rootTransitions.forEach(transition => {
1133 - addTransitionCompleteCallbackToPendingTransition({
1134 - transitionName: transition.name,
1135 - startTime: transition.startTime,
1136 - });
1137 - });
1138 - }
1115 + });
1116 + }
1117 + } else if (wasHidden && !isHidden) {
1118 + // The suspense boundary went from hidden to visible. Remove
1119 + // the boundary from the pending suspense boundaries set
1120 + // if it's there
1121 + if (pendingMarkers !== null) {
1122 + pendingMarkers.forEach(pendingBoundaries => {
1123 + if (pendingBoundaries.has(offscreenInstance)) {
1124 + pendingBoundaries.delete(offscreenInstance);
1125 }
1140 - } else if (!wasHidden && isHidden) {
1141 - // The suspense boundaries was just hidden. Add the boundary
1142 - // to the pending boundary set if it's there
1143 - rootPendingBoundaries.set(offscreenInstance, {
1144 - name,
1145 - });
1146 - }
1126 + });
1127 }
1128 }
1129 }
@@ -2830,45 +2810,46 @@ function commitPassiveMountOnFiber(
2810 // Get the transitions that were initiatized during the render
2811 // and add a start transition callback for each of them
2812 const state = finishedWork.memoizedState;
2833 - // TODO Since it's a mutable field, this should live on the FiberRoot
2834 - if (state.transitions === null) {
2835 - state.transitions = new Set([]);
2836 - }
2837 - const pendingTransitions = state.transitions;
2838 - const pendingSuspenseBoundaries = state.pendingSuspenseBoundaries;
2839 -
2813 + let incompleteTransitions = state.incompleteTransitions;
2814 // Initial render
2815 if (committedTransitions !== null) {
2816 + if (state.incompleteTransitions === null) {
2817 + state.incompleteTransitions = incompleteTransitions = new Map();
2818 + }
2819 +
2820 committedTransitions.forEach(transition => {
2821 addTransitionStartCallbackToPendingTransition({
2822 transitionName: transition.name,
2823 startTime: transition.startTime,
2824 });
2847 - pendingTransitions.add(transition);
2825 +
2826 + if (!incompleteTransitions.has(transition)) {
2827 + incompleteTransitions.set(transition, null);
2828 + }
2829 });
2830
2850 - if (
2851 - pendingSuspenseBoundaries === null ||
2852 - pendingSuspenseBoundaries.size === 0
2853 - ) {
2854 - pendingTransitions.forEach(transition => {
2831 + clearTransitionsForLanes(finishedRoot, committedLanes);
2832 + }
2833 +
2834 + if (incompleteTransitions !== null) {
2835 + incompleteTransitions.forEach((pendingBoundaries, transition) => {
2836 + if (pendingBoundaries === null || pendingBoundaries.size === 0) {
2837 addTransitionCompleteCallbackToPendingTransition({
2838 transitionName: transition.name,
2839 startTime: transition.startTime,
2840 });
2859 - });
2860 - }
2861 -
2862 - clearTransitionsForLanes(finishedRoot, committedLanes);
2841 + incompleteTransitions.delete(transition);
2842 + }
2843 + });
2844 }
2845
2846 // If there are no more pending suspense boundaries we
2847 // clear the transitions because they are all complete.
2848 if (
2868 - pendingSuspenseBoundaries === null ||
2869 - pendingSuspenseBoundaries.size === 0
2849 + incompleteTransitions === null ||
2850 + incompleteTransitions.size === 0
2851 ) {
2871 - state.transitions = null;
2852 + state.incompleteTransitions = null;
2853 }
2854 }
2855 break;
@@ -2909,39 +2890,59 @@ function commitPassiveMountOnFiber(
2890 const isFallback = finishedWork.memoizedState;
2891 const queue = (finishedWork.updateQueue: any);
2892 const rootMemoizedState = finishedRoot.current.memoizedState;
2893 + const instance = finishedWork.stateNode;
2894
2895 if (queue !== null) {
2914 - // We have one instance of the pendingSuspenseBoundaries map.
2915 - // We only need one because we update it during the commit phase.
2916 - // We instantiate a new Map if we haven't already
2917 - if (rootMemoizedState.pendingSuspenseBoundaries === null) {
2918 - rootMemoizedState.pendingSuspenseBoundaries = new Map();
2919 - }
2920 -
2896 if (isFallback) {
2897 const transitions = queue.transitions;
2923 - let prevTransitions = finishedWork.memoizedState.transitions;
2924 - // Add all the transitions saved in the update queue during
2925 - // the render phase (ie the transitions associated with this boundary)
2926 - // into the transitions set.
2927 - if (transitions !== null) {
2928 - if (prevTransitions === null) {
2929 - // We only have one instance of the transitions set
2930 - // because we update it only during the commit phase. We
2931 - // will create the set on a as needed basis in the commit phase
2932 - finishedWork.memoizedState.transitions = prevTransitions = new Set();
2933 - }
2898 + let prevTransitions = instance.transitions;
2899 + let rootIncompleteTransitions =
2900 + rootMemoizedState.incompleteTransitions;
2901 +
2902 + // We lazily instantiate transition tracing relevant maps
2903 + // and sets in the commit phase as we need to use them. We only
2904 + // instantiate them in the fallback phase on an as needed basis
2905 + if (rootMemoizedState.incompleteTransitions === null) {
2906 + // TODO(luna): Move this to the fiber root
2907 + rootMemoizedState.incompleteTransitions = rootIncompleteTransitions = new Map();
2908 + }
2909 + if (instance.pendingMarkers === null) {
2910 + instance.pendingMarkers = new Set();
2911 + }
2912 + if (transitions !== null && prevTransitions === null) {
2913 + instance.transitions = prevTransitions = new Set();
2914 + }
2915
2916 + if (transitions !== null) {
2917 transitions.forEach(transition => {
2918 + // Add all the transitions saved in the update queue during
2919 + // the render phase (ie the transitions associated with this boundary)
2920 + // into the transitions set.
2921 prevTransitions.add(transition);
2922 +
2923 + // Add the root transition's pending suspense boundary set to
2924 + // the queue's marker set. We will iterate through the marker
2925 + // set when we toggle state on the suspense boundary and
2926 + // add or remove the pending suspense boundaries as needed.
2927 + if (!rootIncompleteTransitions.has(transition)) {
2928 + rootIncompleteTransitions.set(transition, new Map());
2929 + }
2930 + instance.pendingMarkers.add(
2931 + rootIncompleteTransitions.get(transition),
2932 + );
2933 });
2934 }
2935 }
2940 - }
2936
2942 - commitTransitionProgress(finishedRoot, finishedWork);
2937 + commitTransitionProgress(finishedWork);
2938
2944 - finishedWork.updateQueue = null;
2939 + if (
2940 + instance.pendingMarkers === null ||
2941 + instance.pendingMarkers.size === 0
2942 + ) {
2943 + finishedWork.updateQueue = null;
2944 + }
2945 + }
2946 }
2947
2948 break;
packages/react-reconciler/src/ReactFiberOffscreenComponent.js
+6 -2
@@ -10,7 +10,10 @@
10 import type {ReactNodeList, OffscreenMode} from 'shared/ReactTypes';
11 import type {Lanes} from './ReactFiberLane.old';
12 import type {SpawnedCachePool} from './ReactFiberCacheComponent.new';
13 -import type {Transition} from './ReactFiberTracingMarkerComponent.new';
13 +import type {
14 + Transition,
15 + PendingSuspenseBoundaries,
16 +} from './ReactFiberTracingMarkerComponent.new';
17
18 export type OffscreenProps = {|
19 // TODO: Pick an API before exposing the Offscreen type. I've chosen an enum
@@ -31,7 +34,6 @@ export type OffscreenState = {|
34 // order to unhide the component.
35 baseLanes: Lanes,
36 cachePool: SpawnedCachePool | null,
34 - transitions: Set<Transition> | null,
37 |};
38
39 export type OffscreenQueue = {|
@@ -40,4 +42,6 @@ export type OffscreenQueue = {|
42
43 export type OffscreenInstance = {|
44 isHidden: boolean,
45 + pendingMarkers: Set<PendingSuspenseBoundaries> | null,
46 + transitions: Set<Transition> | null,
47 |};
packages/react-reconciler/src/ReactFiberRoot.new.js
+9 -6
@@ -45,8 +45,13 @@ export type RootState = {
45 element: any,
46 isDehydrated: boolean,
47 cache: Cache,
48 - pendingSuspenseBoundaries: PendingSuspenseBoundaries | null,
49 - transitions: Set<Transition> | null,
48 + // Transitions on the root can be represented as a bunch of tracing markers.
49 + // Each entangled group of transitions can be treated as a tracing marker.
50 + // It will have a set of pending suspense boundaries. These transitions
51 + // are considered complete when the pending suspense boundaries set is
52 + // empty. We can represent this as a Map of transitions to suspense
53 + // boundary sets
54 + incompleteTransitions: Map<Transition, PendingSuspenseBoundaries> | null,
55 };
56
57 function FiberRootNode(
@@ -189,8 +194,7 @@ export function createFiberRoot(
194 element: initialChildren,
195 isDehydrated: hydrate,
196 cache: initialCache,
192 - transitions: null,
193 - pendingSuspenseBoundaries: null,
197 + incompleteTransitions: null,
198 };
199 uninitializedFiber.memoizedState = initialState;
200 } else {
@@ -198,8 +202,7 @@ export function createFiberRoot(
202 element: initialChildren,
203 isDehydrated: hydrate,
204 cache: (null: any), // not enabled yet
201 - transitions: null,
202 - pendingSuspenseBoundaries: null,
205 + incompleteTransitions: null,
206 };
207 uninitializedFiber.memoizedState = initialState;
208 }
packages/react-reconciler/src/ReactFiberRoot.old.js
+9 -6
@@ -45,8 +45,13 @@ export type RootState = {
45 element: any,
46 isDehydrated: boolean,
47 cache: Cache,
48 - pendingSuspenseBoundaries: PendingSuspenseBoundaries | null,
49 - transitions: Set<Transition> | null,
48 + // Transitions on the root can be represented as a bunch of tracing markers.
49 + // Each entangled group of transitions can be treated as a tracing marker.
50 + // It will have a set of pending suspense boundaries. These transitions
51 + // are considered complete when the pending suspense boundaries set is
52 + // empty. We can represent this as a Map of transitions to suspense
53 + // boundary sets
54 + incompleteTransitions: Map<Transition, PendingSuspenseBoundaries> | null,
55 };
56
57 function FiberRootNode(
@@ -187,8 +192,7 @@ export function createFiberRoot(
192 element: initialChildren,
193 isDehydrated: hydrate,
194 cache: initialCache,
190 - transitions: null,
191 - pendingSuspenseBoundaries: null,
195 + incompleteTransitions: null,
196 };
197 uninitializedFiber.memoizedState = initialState;
198 } else {
@@ -196,8 +200,7 @@ export function createFiberRoot(
200 element: initialChildren,
201 isDehydrated: hydrate,
202 cache: (null: any), // not enabled yet
199 - transitions: null,
200 - pendingSuspenseBoundaries: null,
203 + incompleteTransitions: null,
204 };
205 uninitializedFiber.memoizedState = initialState;
206 }
packages/react-reconciler/src/__tests__/ReactTransitionTracing-test.js
+198
@@ -279,4 +279,202 @@ describe('ReactInteractionTracing', () => {
279 ]);
280 });
281 });
282 +
283 + // @gate enableTransitionTracing
284 + it('should correctly trace multiple separate root interactions', async () => {
285 + const transitionCallbacks = {
286 + onTransitionStart: (name, startTime) => {
287 + Scheduler.unstable_yieldValue(
288 + `onTransitionStart(${name}, ${startTime})`,
289 + );
290 + },
291 + onTransitionComplete: (name, startTime, endTime) => {
292 + Scheduler.unstable_yieldValue(
293 + `onTransitionComplete(${name}, ${startTime}, ${endTime})`,
294 + );
295 + },
296 + };
297 +
298 + let navigateToPageTwo;
299 + let showTextFn;
300 + function App() {
301 + const [navigate, setNavigate] = useState(false);
302 + const [showText, setShowText] = useState(false);
303 +
304 + navigateToPageTwo = () => {
305 + setNavigate(true);
306 + };
307 +
308 + showTextFn = () => {
309 + setShowText(true);
310 + };
311 +
312 + return (
313 + <div>
314 + {navigate ? (
315 + <>
316 + {showText ? (
317 + <Suspense fallback={<Text text="Show Text Loading..." />}>
318 + <AsyncText text="Show Text" />
319 + </Suspense>
320 + ) : null}
321 + <Suspense
322 + fallback={<Text text="Loading..." />}
323 + name="suspense page">
324 + <AsyncText text="Page Two" />
325 + </Suspense>
326 + </>
327 + ) : (
328 + <Text text="Page One" />
329 + )}
330 + </div>
331 + );
332 + }
333 +
334 + const root = ReactNoop.createRoot({transitionCallbacks});
335 + await act(async () => {
336 + root.render(<App />);
337 + ReactNoop.expire(1000);
338 + await advanceTimers(1000);
339 +
340 + expect(Scheduler).toFlushAndYield(['Page One']);
341 + });
342 +
343 + await act(async () => {
344 + startTransition(() => navigateToPageTwo(), {name: 'page transition'});
345 +
346 + expect(Scheduler).toFlushAndYield([
347 + 'Suspend [Page Two]',
348 + 'Loading...',
349 + 'onTransitionStart(page transition, 1000)',
350 + ]);
351 +
352 + await resolveText('Page Two');
353 + ReactNoop.expire(1000);
354 + await advanceTimers(1000);
355 + expect(Scheduler).toFlushAndYield([
356 + 'Page Two',
357 + 'onTransitionComplete(page transition, 1000, 2000)',
358 + ]);
359 +
360 + startTransition(() => showTextFn(), {name: 'text transition'});
361 + expect(Scheduler).toFlushAndYield([
362 + 'Suspend [Show Text]',
363 + 'Show Text Loading...',
364 + 'Page Two',
365 + 'onTransitionStart(text transition, 2000)',
366 + ]);
367 +
368 + await resolveText('Show Text');
369 + ReactNoop.expire(1000);
370 + await advanceTimers(1000);
371 + expect(Scheduler).toFlushAndYield([
372 + 'Show Text',
373 + 'onTransitionComplete(text transition, 2000, 3000)',
374 + ]);
375 + });
376 + });
377 +
378 + // @gate enableTransitionTracing
379 + it('should correctly trace multiple intertwined root interactions', async () => {
380 + const transitionCallbacks = {
381 + onTransitionStart: (name, startTime) => {
382 + Scheduler.unstable_yieldValue(
383 + `onTransitionStart(${name}, ${startTime})`,
384 + );
385 + },
386 + onTransitionComplete: (name, startTime, endTime) => {
387 + Scheduler.unstable_yieldValue(
388 + `onTransitionComplete(${name}, ${startTime}, ${endTime})`,
389 + );
390 + },
391 + };
392 + let navigateToPageTwo;
393 + let showTextFn;
394 + function App() {
395 + const [navigate, setNavigate] = useState(false);
396 + const [showText, setShowText] = useState(false);
397 + navigateToPageTwo = () => {
398 + setNavigate(true);
399 + };
400 +
401 + showTextFn = () => {
402 + setShowText(true);
403 + };
404 +
405 + return (
406 + <div>
407 + {navigate ? (
408 + <>
409 + {showText ? (
410 + <Suspense fallback={<Text text="Show Text Loading..." />}>
411 + <AsyncText text="Show Text" />
412 + </Suspense>
413 + ) : null}
414 + <Suspense
415 + fallback={<Text text="Loading..." />}
416 + name="suspense page">
417 + <AsyncText text="Page Two" />
418 + </Suspense>
419 + </>
420 + ) : (
421 + <Text text="Page One" />
422 + )}
423 + </div>
424 + );
425 + }
426 +
427 + const root = ReactNoop.createRoot({transitionCallbacks});
428 + await act(async () => {
429 + root.render(<App />);
430 + ReactNoop.expire(1000);
431 + await advanceTimers(1000);
432 +
433 + expect(Scheduler).toFlushAndYield(['Page One']);
434 + });
435 +
436 + await act(async () => {
437 + startTransition(() => navigateToPageTwo(), {name: 'page transition'});
438 + ReactNoop.expire(1000);
439 + await advanceTimers(1000);
440 +
441 + expect(Scheduler).toFlushAndYield([
442 + 'Suspend [Page Two]',
443 + 'Loading...',
444 + 'onTransitionStart(page transition, 1000)',
445 + ]);
446 + });
447 +
448 + await act(async () => {
449 + startTransition(() => showTextFn(), {name: 'show text'});
450 +
451 + expect(Scheduler).toFlushAndYield([
452 + 'Suspend [Show Text]',
453 + 'Show Text Loading...',
454 + 'Suspend [Page Two]',
455 + 'Loading...',
456 + 'onTransitionStart(show text, 2000)',
457 + ]);
458 + });
459 +
460 + await act(async () => {
461 + await resolveText('Page Two');
462 + ReactNoop.expire(1000);
463 + await advanceTimers(1000);
464 +
465 + expect(Scheduler).toFlushAndYield([
466 + 'Page Two',
467 + 'onTransitionComplete(page transition, 1000, 3000)',
468 + ]);
469 +
470 + await resolveText('Show Text');
471 + ReactNoop.expire(1000);
472 + await advanceTimers(1000);
473 +
474 + expect(Scheduler).toFlushAndYield([
475 + 'Show Text',
476 + 'onTransitionComplete(show text, 2000, 4000)',
477 + ]);
478 + });
479 + });
480 });