[Transition Tracing] onMarkerIncomplete - Tracing Marker/Suspense Boundary Deletions (#24885)
This PR adds the `onMarkerIncomplete` callback for tracing marker name changes. Specifically, this PR: * Adds the `onMarkerIncomplete` callback * When a tracing marker is deleted, call `onMarkerIncomplete` with the `name` of the tracing marker for the tracing marker. * When a tracing marker/suspense boundary is deleted, call `onMarkerIncomplete` for every parent tracing marker with the `name` of the tracing marker that caused the transition to be incomplete. * Don't call `onTransitionComplete` or `onMarkerComplete` when `onMarkerIncomplete` is called for all tracing markers with the same transitions, but continue to call `onTransitionProgress`
Luna Ruan committed
Aug 25, 2022 at 19:03 UTC
11ed7010c6d34c40342e8b829a48fec138215ae0
14 files changed
+1571
-106
packages/react-reconciler/src/ReactFiber.new.js
+2
@@ -774,6 +774,8 @@ export function createFiberFromTracingMarker(
774
tag: TransitionTracingMarker,
775
transitions: null,
776
pendingBoundaries: null,
777
+ aborts: null,
778
+ name: pendingProps.name,
779
};
780
fiber.stateNode = tracingMarkerInstance;
781
return fiber;
packages/react-reconciler/src/ReactFiber.old.js
+2
@@ -774,6 +774,8 @@ export function createFiberFromTracingMarker(
774
tag: TransitionTracingMarker,
775
transitions: null,
776
pendingBoundaries: null,
777
+ aborts: null,
778
+ name: pendingProps.name,
779
};
780
fiber.stateNode = tracingMarkerInstance;
781
return fiber;
packages/react-reconciler/src/ReactFiberBeginWork.new.js
+9
-1
@@ -89,6 +89,7 @@ import {
89
StaticMask,
90
ShouldCapture,
91
ForceClientRender,
92
+ Passive,
93
} from './ReactFiberFlags';
94
import ReactSharedInternals from 'shared/ReactSharedInternals';
95
import {
@@ -979,10 +980,17 @@ function updateTracingMarkerComponent(
980
const markerInstance: TracingMarkerInstance = {
981
tag: TransitionTracingMarker,
982
transitions: new Set(currentTransitions),
982
- pendingBoundaries: new Map(),
983
+ pendingBoundaries: null,
984
name: workInProgress.pendingProps.name,
985
+ aborts: null,
986
};
987
workInProgress.stateNode = markerInstance;
988
+
989
+ // We call the marker complete callback when all child suspense boundaries resolve.
990
+ // We do this in the commit phase on Offscreen. If the marker has no child suspense
991
+ // boundaries, we need to schedule a passive effect to make sure we call the marker
992
+ // complete callback.
993
+ workInProgress.flags |= Passive;
994
}
995
} else {
996
if (__DEV__) {
packages/react-reconciler/src/ReactFiberBeginWork.old.js
+9
-1
@@ -89,6 +89,7 @@ import {
89
StaticMask,
90
ShouldCapture,
91
ForceClientRender,
92
+ Passive,
93
} from './ReactFiberFlags';
94
import ReactSharedInternals from 'shared/ReactSharedInternals';
95
import {
@@ -979,10 +980,17 @@ function updateTracingMarkerComponent(
980
const markerInstance: TracingMarkerInstance = {
981
tag: TransitionTracingMarker,
982
transitions: new Set(currentTransitions),
982
- pendingBoundaries: new Map(),
983
+ pendingBoundaries: null,
984
name: workInProgress.pendingProps.name,
985
+ aborts: null,
986
};
987
workInProgress.stateNode = markerInstance;
988
+
989
+ // We call the marker complete callback when all child suspense boundaries resolve.
990
+ // We do this in the commit phase on Offscreen. If the marker has no child suspense
991
+ // boundaries, we need to schedule a passive effect to make sure we call the marker
992
+ // complete callback.
993
+ workInProgress.flags |= Passive;
994
}
995
} else {
996
if (__DEV__) {
packages/react-reconciler/src/ReactFiberCommitWork.new.js
+250
-33
@@ -30,7 +30,11 @@ import type {
30
import type {HookFlags} from './ReactHookEffectTags';
31
import type {Cache} from './ReactFiberCacheComponent.new';
32
import type {RootState} from './ReactFiberRoot.new';
33
-import type {Transition} from './ReactFiberTracingMarkerComponent.new';
33
+import type {
34
+ Transition,
35
+ TracingMarkerInstance,
36
+ TransitionAbort,
37
+} from './ReactFiberTracingMarkerComponent.new';
38
39
import {
40
enableCreateEventHandleAPI,
@@ -145,6 +149,7 @@ import {
149
addTransitionProgressCallbackToPendingTransition,
150
addTransitionCompleteCallbackToPendingTransition,
151
addMarkerProgressCallbackToPendingTransition,
152
+ addMarkerIncompleteCallbackToPendingTransition,
153
addMarkerCompleteCallbackToPendingTransition,
154
setIsRunningInsertionEffect,
155
getExecutionContext,
@@ -1130,6 +1135,141 @@ function commitLayoutEffectOnFiber(
1135
}
1136
}
1137
1138
+function abortRootTransitions(
1139
+ root: FiberRoot,
1140
+ abort: TransitionAbort,
1141
+ deletedTransitions: Set<Transition>,
1142
+ deletedOffscreenInstance: OffscreenInstance | null,
1143
+ isInDeletedTree: boolean,
1144
+) {
1145
+ if (enableTransitionTracing) {
1146
+ const rootTransitions = root.incompleteTransitions;
1147
+ deletedTransitions.forEach(transition => {
1148
+ if (rootTransitions.has(transition)) {
1149
+ const transitionInstance: TracingMarkerInstance = (rootTransitions.get(
1150
+ transition,
1151
+ ): any);
1152
+ if (transitionInstance.aborts === null) {
1153
+ transitionInstance.aborts = [];
1154
+ }
1155
+ transitionInstance.aborts.push(abort);
1156
+
1157
+ if (deletedOffscreenInstance !== null) {
1158
+ if (
1159
+ transitionInstance.pendingBoundaries !== null &&
1160
+ transitionInstance.pendingBoundaries.has(deletedOffscreenInstance)
1161
+ ) {
1162
+ transitionInstance.pendingBoundaries.delete(
1163
+ deletedOffscreenInstance,
1164
+ );
1165
+ }
1166
+ }
1167
+ }
1168
+ });
1169
+ }
1170
+}
1171
+
1172
+function abortTracingMarkerTransitions(
1173
+ abortedFiber: Fiber,
1174
+ abort: TransitionAbort,
1175
+ deletedTransitions: Set<Transition>,
1176
+ deletedOffscreenInstance: OffscreenInstance | null,
1177
+ isInDeletedTree: boolean,
1178
+) {
1179
+ if (enableTransitionTracing) {
1180
+ const markerInstance: TracingMarkerInstance = abortedFiber.stateNode;
1181
+ const markerTransitions = markerInstance.transitions;
1182
+ const pendingBoundaries = markerInstance.pendingBoundaries;
1183
+ if (markerTransitions !== null) {
1184
+ // TODO: Refactor this code. Is there a way to move this code to
1185
+ // the deletions phase instead of calculating it here while making sure
1186
+ // complete is called appropriately?
1187
+ deletedTransitions.forEach(transition => {
1188
+ // If one of the transitions on the tracing marker is a transition
1189
+ // that was in an aborted subtree, we will abort that tracing marker
1190
+ if (
1191
+ abortedFiber !== null &&
1192
+ markerTransitions.has(transition) &&
1193
+ (markerInstance.aborts === null ||
1194
+ !markerInstance.aborts.includes(abort))
1195
+ ) {
1196
+ if (markerInstance.transitions !== null) {
1197
+ if (markerInstance.aborts === null) {
1198
+ markerInstance.aborts = [abort];
1199
+ addMarkerIncompleteCallbackToPendingTransition(
1200
+ abortedFiber.memoizedProps.name,
1201
+ markerInstance.transitions,
1202
+ markerInstance.aborts,
1203
+ );
1204
+ } else {
1205
+ markerInstance.aborts.push(abort);
1206
+ }
1207
+
1208
+ // We only want to call onTransitionProgress when the marker hasn't been
1209
+ // deleted
1210
+ if (
1211
+ deletedOffscreenInstance !== null &&
1212
+ !isInDeletedTree &&
1213
+ pendingBoundaries !== null &&
1214
+ pendingBoundaries.has(deletedOffscreenInstance)
1215
+ ) {
1216
+ pendingBoundaries.delete(deletedOffscreenInstance);
1217
+
1218
+ addMarkerProgressCallbackToPendingTransition(
1219
+ abortedFiber.memoizedProps.name,
1220
+ deletedTransitions,
1221
+ pendingBoundaries,
1222
+ );
1223
+ }
1224
+ }
1225
+ }
1226
+ });
1227
+ }
1228
+ }
1229
+}
1230
+
1231
+function abortParentMarkerTransitionsForDeletedFiber(
1232
+ abortedFiber: Fiber,
1233
+ abort: TransitionAbort,
1234
+ deletedTransitions: Set<Transition>,
1235
+ deletedOffscreenInstance: OffscreenInstance | null,
1236
+ isInDeletedTree: boolean,
1237
+) {
1238
+ if (enableTransitionTracing) {
1239
+ // Find all pending markers that are waiting on child suspense boundaries in the
1240
+ // aborted subtree and cancels them
1241
+ let fiber = abortedFiber;
1242
+ while (fiber !== null) {
1243
+ switch (fiber.tag) {
1244
+ case TracingMarkerComponent:
1245
+ abortTracingMarkerTransitions(
1246
+ fiber,
1247
+ abort,
1248
+ deletedTransitions,
1249
+ deletedOffscreenInstance,
1250
+ isInDeletedTree,
1251
+ );
1252
+ break;
1253
+ case HostRoot:
1254
+ const root = fiber.stateNode;
1255
+ abortRootTransitions(
1256
+ root,
1257
+ abort,
1258
+ deletedTransitions,
1259
+ deletedOffscreenInstance,
1260
+ isInDeletedTree,
1261
+ );
1262
+
1263
+ break;
1264
+ default:
1265
+ break;
1266
+ }
1267
+
1268
+ fiber = fiber.return;
1269
+ }
1270
+ }
1271
+}
1272
+
1273
function commitTransitionProgress(offscreenFiber: Fiber) {
1274
if (enableTransitionTracing) {
1275
// This function adds suspense boundaries to the root
@@ -1175,6 +1315,7 @@ function commitTransitionProgress(offscreenFiber: Fiber) {
1315
pendingMarkers.forEach(markerInstance => {
1316
const pendingBoundaries = markerInstance.pendingBoundaries;
1317
const transitions = markerInstance.transitions;
1318
+ const markerName = markerInstance.name;
1319
if (
1320
pendingBoundaries !== null &&
1321
!pendingBoundaries.has(offscreenInstance)
@@ -1185,10 +1326,10 @@ function commitTransitionProgress(offscreenFiber: Fiber) {
1326
if (transitions !== null) {
1327
if (
1328
markerInstance.tag === TransitionTracingMarker &&
1188
- markerInstance.name !== undefined
1329
+ markerName !== null
1330
) {
1331
addMarkerProgressCallbackToPendingTransition(
1191
- markerInstance.name,
1332
+ markerName,
1333
transitions,
1334
pendingBoundaries,
1335
);
@@ -1212,6 +1353,7 @@ function commitTransitionProgress(offscreenFiber: Fiber) {
1353
pendingMarkers.forEach(markerInstance => {
1354
const pendingBoundaries = markerInstance.pendingBoundaries;
1355
const transitions = markerInstance.transitions;
1356
+ const markerName = markerInstance.name;
1357
if (
1358
pendingBoundaries !== null &&
1359
pendingBoundaries.has(offscreenInstance)
@@ -1220,13 +1362,27 @@ function commitTransitionProgress(offscreenFiber: Fiber) {
1362
if (transitions !== null) {
1363
if (
1364
markerInstance.tag === TransitionTracingMarker &&
1223
- markerInstance.name !== undefined
1365
+ markerName !== null
1366
) {
1367
addMarkerProgressCallbackToPendingTransition(
1226
- markerInstance.name,
1368
+ markerName,
1369
transitions,
1370
pendingBoundaries,
1371
);
1372
+
1373
+ // If there are no more unresolved suspense boundaries, the interaction
1374
+ // is considered finished
1375
+ if (pendingBoundaries.size === 0) {
1376
+ if (markerInstance.aborts === null) {
1377
+ addMarkerCompleteCallbackToPendingTransition(
1378
+ markerName,
1379
+ transitions,
1380
+ );
1381
+ }
1382
+ markerInstance.transitions = null;
1383
+ markerInstance.pendingBoundaries = null;
1384
+ markerInstance.aborts = null;
1385
+ }
1386
} else if (markerInstance.tag === TransitionRoot) {
1387
transitions.forEach(transition => {
1388
addTransitionProgressCallbackToPendingTransition(
@@ -1737,6 +1893,7 @@ function commitDeletionEffects(
1893
'a bug in React. Please file an issue.',
1894
);
1895
}
1896
+
1897
commitDeletionEffectsOnFiber(root, returnFiber, deletedFiber);
1898
hostParent = null;
1899
hostParentIsContainer = false;
@@ -1979,6 +2136,7 @@ function commitDeletionEffectsOnFiber(
2136
const prevOffscreenSubtreeWasHidden = offscreenSubtreeWasHidden;
2137
offscreenSubtreeWasHidden =
2138
prevOffscreenSubtreeWasHidden || deletedFiber.memoizedState !== null;
2139
+
2140
recursivelyTraverseDeletionEffects(
2141
finishedRoot,
2142
nearestMountedAncestor,
@@ -2957,6 +3115,12 @@ function commitOffscreenPassiveMountEffects(
3115
}
3116
3117
commitTransitionProgress(finishedWork);
3118
+
3119
+ // TODO: Refactor this into an if/else branch
3120
+ if (!isHidden) {
3121
+ instance.transitions = null;
3122
+ instance.pendingMarkers = null;
3123
+ }
3124
}
3125
}
3126
@@ -2987,20 +3151,18 @@ function commitCachePassiveMountEffect(
3151
function commitTracingMarkerPassiveMountEffect(finishedWork: Fiber) {
3152
// Get the transitions that were initiatized during the render
3153
// and add a start transition callback for each of them
3154
+ // We will only call this on initial mount of the tracing marker
3155
+ // only if there are no suspense children
3156
const instance = finishedWork.stateNode;
2991
- if (
2992
- instance.transitions !== null &&
2993
- (instance.pendingBoundaries === null ||
2994
- instance.pendingBoundaries.size === 0)
2995
- ) {
2996
- instance.transitions.forEach(transition => {
2997
- addMarkerCompleteCallbackToPendingTransition(
2998
- finishedWork.memoizedProps.name,
2999
- instance.transitions,
3000
- );
3001
- });
3157
+ if (instance.transitions !== null && instance.pendingBoundaries === null) {
3158
+ addMarkerCompleteCallbackToPendingTransition(
3159
+ finishedWork.memoizedProps.name,
3160
+ instance.transitions,
3161
+ );
3162
instance.transitions = null;
3163
instance.pendingBoundaries = null;
3164
+ instance.aborts = null;
3165
+ instance.name = null;
3166
}
3167
}
3168
@@ -3102,7 +3264,7 @@ function commitPassiveMountOnFiber(
3264
if (enableTransitionTracing) {
3265
// Get the transitions that were initiatized during the render
3266
// and add a start transition callback for each of them
3105
- const root = finishedWork.stateNode;
3267
+ const root: FiberRoot = finishedWork.stateNode;
3268
const incompleteTransitions = root.incompleteTransitions;
3269
// Initial render
3270
if (committedTransitions !== null) {
@@ -3116,7 +3278,9 @@ function commitPassiveMountOnFiber(
3278
incompleteTransitions.forEach((markerInstance, transition) => {
3279
const pendingBoundaries = markerInstance.pendingBoundaries;
3280
if (pendingBoundaries === null || pendingBoundaries.size === 0) {
3119
- addTransitionCompleteCallbackToPendingTransition(transition);
3281
+ if (markerInstance.aborts === null) {
3282
+ addTransitionCompleteCallbackToPendingTransition(transition);
3283
+ }
3284
incompleteTransitions.delete(transition);
3285
}
3286
});
@@ -3489,21 +3653,6 @@ function commitAtomicPassiveEffects(
3653
}
3654
break;
3655
}
3492
- case TracingMarkerComponent: {
3493
- if (enableTransitionTracing) {
3494
- recursivelyTraverseAtomicPassiveEffects(
3495
- finishedRoot,
3496
- finishedWork,
3497
- committedLanes,
3498
- committedTransitions,
3499
- );
3500
- if (flags & Passive) {
3501
- commitTracingMarkerPassiveMountEffect(finishedWork);
3502
- }
3503
- break;
3504
- }
3505
- // Intentional fallthrough to next branch
3506
- }
3656
// eslint-disable-next-line-no-fallthrough
3657
default: {
3658
recursivelyTraverseAtomicPassiveEffects(
@@ -3827,6 +3976,43 @@ function commitPassiveUnmountInsideDeletedTreeOnFiber(
3976
}
3977
break;
3978
}
3979
+ case SuspenseComponent: {
3980
+ if (enableTransitionTracing) {
3981
+ // We need to mark this fiber's parents as deleted
3982
+ const offscreenFiber: Fiber = (current.child: any);
3983
+ const instance: OffscreenInstance = offscreenFiber.stateNode;
3984
+ const transitions = instance.transitions;
3985
+ if (transitions !== null) {
3986
+ const abortReason = {
3987
+ reason: 'suspense',
3988
+ name: current.memoizedProps.unstable_name || null,
3989
+ };
3990
+ if (
3991
+ current.memoizedState === null ||
3992
+ current.memoizedState.dehydrated === null
3993
+ ) {
3994
+ abortParentMarkerTransitionsForDeletedFiber(
3995
+ offscreenFiber,
3996
+ abortReason,
3997
+ transitions,
3998
+ instance,
3999
+ true,
4000
+ );
4001
+
4002
+ if (nearestMountedAncestor !== null) {
4003
+ abortParentMarkerTransitionsForDeletedFiber(
4004
+ nearestMountedAncestor,
4005
+ abortReason,
4006
+ transitions,
4007
+ instance,
4008
+ false,
4009
+ );
4010
+ }
4011
+ }
4012
+ }
4013
+ }
4014
+ break;
4015
+ }
4016
case CacheComponent: {
4017
if (enableCache) {
4018
const cache = current.memoizedState.cache;
@@ -3834,6 +4020,37 @@ function commitPassiveUnmountInsideDeletedTreeOnFiber(
4020
}
4021
break;
4022
}
4023
+ case TracingMarkerComponent: {
4024
+ if (enableTransitionTracing) {
4025
+ // We need to mark this fiber's parents as deleted
4026
+ const instance: TracingMarkerInstance = current.stateNode;
4027
+ const transitions = instance.transitions;
4028
+ if (transitions !== null) {
4029
+ const abortReason = {
4030
+ reason: 'marker',
4031
+ name: current.memoizedProps.name,
4032
+ };
4033
+ abortParentMarkerTransitionsForDeletedFiber(
4034
+ current,
4035
+ abortReason,
4036
+ transitions,
4037
+ null,
4038
+ true,
4039
+ );
4040
+
4041
+ if (nearestMountedAncestor !== null) {
4042
+ abortParentMarkerTransitionsForDeletedFiber(
4043
+ nearestMountedAncestor,
4044
+ abortReason,
4045
+ transitions,
4046
+ null,
4047
+ false,
4048
+ );
4049
+ }
4050
+ }
4051
+ }
4052
+ break;
4053
+ }
4054
}
4055
}
4056
packages/react-reconciler/src/ReactFiberCommitWork.old.js
+250
-33
@@ -30,7 +30,11 @@ import type {
30
import type {HookFlags} from './ReactHookEffectTags';
31
import type {Cache} from './ReactFiberCacheComponent.old';
32
import type {RootState} from './ReactFiberRoot.old';
33
-import type {Transition} from './ReactFiberTracingMarkerComponent.old';
33
+import type {
34
+ Transition,
35
+ TracingMarkerInstance,
36
+ TransitionAbort,
37
+} from './ReactFiberTracingMarkerComponent.old';
38
39
import {
40
enableCreateEventHandleAPI,
@@ -145,6 +149,7 @@ import {
149
addTransitionProgressCallbackToPendingTransition,
150
addTransitionCompleteCallbackToPendingTransition,
151
addMarkerProgressCallbackToPendingTransition,
152
+ addMarkerIncompleteCallbackToPendingTransition,
153
addMarkerCompleteCallbackToPendingTransition,
154
setIsRunningInsertionEffect,
155
getExecutionContext,
@@ -1130,6 +1135,141 @@ function commitLayoutEffectOnFiber(
1135
}
1136
}
1137
1138
+function abortRootTransitions(
1139
+ root: FiberRoot,
1140
+ abort: TransitionAbort,
1141
+ deletedTransitions: Set<Transition>,
1142
+ deletedOffscreenInstance: OffscreenInstance | null,
1143
+ isInDeletedTree: boolean,
1144
+) {
1145
+ if (enableTransitionTracing) {
1146
+ const rootTransitions = root.incompleteTransitions;
1147
+ deletedTransitions.forEach(transition => {
1148
+ if (rootTransitions.has(transition)) {
1149
+ const transitionInstance: TracingMarkerInstance = (rootTransitions.get(
1150
+ transition,
1151
+ ): any);
1152
+ if (transitionInstance.aborts === null) {
1153
+ transitionInstance.aborts = [];
1154
+ }
1155
+ transitionInstance.aborts.push(abort);
1156
+
1157
+ if (deletedOffscreenInstance !== null) {
1158
+ if (
1159
+ transitionInstance.pendingBoundaries !== null &&
1160
+ transitionInstance.pendingBoundaries.has(deletedOffscreenInstance)
1161
+ ) {
1162
+ transitionInstance.pendingBoundaries.delete(
1163
+ deletedOffscreenInstance,
1164
+ );
1165
+ }
1166
+ }
1167
+ }
1168
+ });
1169
+ }
1170
+}
1171
+
1172
+function abortTracingMarkerTransitions(
1173
+ abortedFiber: Fiber,
1174
+ abort: TransitionAbort,
1175
+ deletedTransitions: Set<Transition>,
1176
+ deletedOffscreenInstance: OffscreenInstance | null,
1177
+ isInDeletedTree: boolean,
1178
+) {
1179
+ if (enableTransitionTracing) {
1180
+ const markerInstance: TracingMarkerInstance = abortedFiber.stateNode;
1181
+ const markerTransitions = markerInstance.transitions;
1182
+ const pendingBoundaries = markerInstance.pendingBoundaries;
1183
+ if (markerTransitions !== null) {
1184
+ // TODO: Refactor this code. Is there a way to move this code to
1185
+ // the deletions phase instead of calculating it here while making sure
1186
+ // complete is called appropriately?
1187
+ deletedTransitions.forEach(transition => {
1188
+ // If one of the transitions on the tracing marker is a transition
1189
+ // that was in an aborted subtree, we will abort that tracing marker
1190
+ if (
1191
+ abortedFiber !== null &&
1192
+ markerTransitions.has(transition) &&
1193
+ (markerInstance.aborts === null ||
1194
+ !markerInstance.aborts.includes(abort))
1195
+ ) {
1196
+ if (markerInstance.transitions !== null) {
1197
+ if (markerInstance.aborts === null) {
1198
+ markerInstance.aborts = [abort];
1199
+ addMarkerIncompleteCallbackToPendingTransition(
1200
+ abortedFiber.memoizedProps.name,
1201
+ markerInstance.transitions,
1202
+ markerInstance.aborts,
1203
+ );
1204
+ } else {
1205
+ markerInstance.aborts.push(abort);
1206
+ }
1207
+
1208
+ // We only want to call onTransitionProgress when the marker hasn't been
1209
+ // deleted
1210
+ if (
1211
+ deletedOffscreenInstance !== null &&
1212
+ !isInDeletedTree &&
1213
+ pendingBoundaries !== null &&
1214
+ pendingBoundaries.has(deletedOffscreenInstance)
1215
+ ) {
1216
+ pendingBoundaries.delete(deletedOffscreenInstance);
1217
+
1218
+ addMarkerProgressCallbackToPendingTransition(
1219
+ abortedFiber.memoizedProps.name,
1220
+ deletedTransitions,
1221
+ pendingBoundaries,
1222
+ );
1223
+ }
1224
+ }
1225
+ }
1226
+ });
1227
+ }
1228
+ }
1229
+}
1230
+
1231
+function abortParentMarkerTransitionsForDeletedFiber(
1232
+ abortedFiber: Fiber,
1233
+ abort: TransitionAbort,
1234
+ deletedTransitions: Set<Transition>,
1235
+ deletedOffscreenInstance: OffscreenInstance | null,
1236
+ isInDeletedTree: boolean,
1237
+) {
1238
+ if (enableTransitionTracing) {
1239
+ // Find all pending markers that are waiting on child suspense boundaries in the
1240
+ // aborted subtree and cancels them
1241
+ let fiber = abortedFiber;
1242
+ while (fiber !== null) {
1243
+ switch (fiber.tag) {
1244
+ case TracingMarkerComponent:
1245
+ abortTracingMarkerTransitions(
1246
+ fiber,
1247
+ abort,
1248
+ deletedTransitions,
1249
+ deletedOffscreenInstance,
1250
+ isInDeletedTree,
1251
+ );
1252
+ break;
1253
+ case HostRoot:
1254
+ const root = fiber.stateNode;
1255
+ abortRootTransitions(
1256
+ root,
1257
+ abort,
1258
+ deletedTransitions,
1259
+ deletedOffscreenInstance,
1260
+ isInDeletedTree,
1261
+ );
1262
+
1263
+ break;
1264
+ default:
1265
+ break;
1266
+ }
1267
+
1268
+ fiber = fiber.return;
1269
+ }
1270
+ }
1271
+}
1272
+
1273
function commitTransitionProgress(offscreenFiber: Fiber) {
1274
if (enableTransitionTracing) {
1275
// This function adds suspense boundaries to the root
@@ -1175,6 +1315,7 @@ function commitTransitionProgress(offscreenFiber: Fiber) {
1315
pendingMarkers.forEach(markerInstance => {
1316
const pendingBoundaries = markerInstance.pendingBoundaries;
1317
const transitions = markerInstance.transitions;
1318
+ const markerName = markerInstance.name;
1319
if (
1320
pendingBoundaries !== null &&
1321
!pendingBoundaries.has(offscreenInstance)
@@ -1185,10 +1326,10 @@ function commitTransitionProgress(offscreenFiber: Fiber) {
1326
if (transitions !== null) {
1327
if (
1328
markerInstance.tag === TransitionTracingMarker &&
1188
- markerInstance.name !== undefined
1329
+ markerName !== null
1330
) {
1331
addMarkerProgressCallbackToPendingTransition(
1191
- markerInstance.name,
1332
+ markerName,
1333
transitions,
1334
pendingBoundaries,
1335
);
@@ -1212,6 +1353,7 @@ function commitTransitionProgress(offscreenFiber: Fiber) {
1353
pendingMarkers.forEach(markerInstance => {
1354
const pendingBoundaries = markerInstance.pendingBoundaries;
1355
const transitions = markerInstance.transitions;
1356
+ const markerName = markerInstance.name;
1357
if (
1358
pendingBoundaries !== null &&
1359
pendingBoundaries.has(offscreenInstance)
@@ -1220,13 +1362,27 @@ function commitTransitionProgress(offscreenFiber: Fiber) {
1362
if (transitions !== null) {
1363
if (
1364
markerInstance.tag === TransitionTracingMarker &&
1223
- markerInstance.name !== undefined
1365
+ markerName !== null
1366
) {
1367
addMarkerProgressCallbackToPendingTransition(
1226
- markerInstance.name,
1368
+ markerName,
1369
transitions,
1370
pendingBoundaries,
1371
);
1372
+
1373
+ // If there are no more unresolved suspense boundaries, the interaction
1374
+ // is considered finished
1375
+ if (pendingBoundaries.size === 0) {
1376
+ if (markerInstance.aborts === null) {
1377
+ addMarkerCompleteCallbackToPendingTransition(
1378
+ markerName,
1379
+ transitions,
1380
+ );
1381
+ }
1382
+ markerInstance.transitions = null;
1383
+ markerInstance.pendingBoundaries = null;
1384
+ markerInstance.aborts = null;
1385
+ }
1386
} else if (markerInstance.tag === TransitionRoot) {
1387
transitions.forEach(transition => {
1388
addTransitionProgressCallbackToPendingTransition(
@@ -1737,6 +1893,7 @@ function commitDeletionEffects(
1893
'a bug in React. Please file an issue.',
1894
);
1895
}
1896
+
1897
commitDeletionEffectsOnFiber(root, returnFiber, deletedFiber);
1898
hostParent = null;
1899
hostParentIsContainer = false;
@@ -1979,6 +2136,7 @@ function commitDeletionEffectsOnFiber(
2136
const prevOffscreenSubtreeWasHidden = offscreenSubtreeWasHidden;
2137
offscreenSubtreeWasHidden =
2138
prevOffscreenSubtreeWasHidden || deletedFiber.memoizedState !== null;
2139
+
2140
recursivelyTraverseDeletionEffects(
2141
finishedRoot,
2142
nearestMountedAncestor,
@@ -2957,6 +3115,12 @@ function commitOffscreenPassiveMountEffects(
3115
}
3116
3117
commitTransitionProgress(finishedWork);
3118
+
3119
+ // TODO: Refactor this into an if/else branch
3120
+ if (!isHidden) {
3121
+ instance.transitions = null;
3122
+ instance.pendingMarkers = null;
3123
+ }
3124
}
3125
}
3126
@@ -2987,20 +3151,18 @@ function commitCachePassiveMountEffect(
3151
function commitTracingMarkerPassiveMountEffect(finishedWork: Fiber) {
3152
// Get the transitions that were initiatized during the render
3153
// and add a start transition callback for each of them
3154
+ // We will only call this on initial mount of the tracing marker
3155
+ // only if there are no suspense children
3156
const instance = finishedWork.stateNode;
2991
- if (
2992
- instance.transitions !== null &&
2993
- (instance.pendingBoundaries === null ||
2994
- instance.pendingBoundaries.size === 0)
2995
- ) {
2996
- instance.transitions.forEach(transition => {
2997
- addMarkerCompleteCallbackToPendingTransition(
2998
- finishedWork.memoizedProps.name,
2999
- instance.transitions,
3000
- );
3001
- });
3157
+ if (instance.transitions !== null && instance.pendingBoundaries === null) {
3158
+ addMarkerCompleteCallbackToPendingTransition(
3159
+ finishedWork.memoizedProps.name,
3160
+ instance.transitions,
3161
+ );
3162
instance.transitions = null;
3163
instance.pendingBoundaries = null;
3164
+ instance.aborts = null;
3165
+ instance.name = null;
3166
}
3167
}
3168
@@ -3102,7 +3264,7 @@ function commitPassiveMountOnFiber(
3264
if (enableTransitionTracing) {
3265
// Get the transitions that were initiatized during the render
3266
// and add a start transition callback for each of them
3105
- const root = finishedWork.stateNode;
3267
+ const root: FiberRoot = finishedWork.stateNode;
3268
const incompleteTransitions = root.incompleteTransitions;
3269
// Initial render
3270
if (committedTransitions !== null) {
@@ -3116,7 +3278,9 @@ function commitPassiveMountOnFiber(
3278
incompleteTransitions.forEach((markerInstance, transition) => {
3279
const pendingBoundaries = markerInstance.pendingBoundaries;
3280
if (pendingBoundaries === null || pendingBoundaries.size === 0) {
3119
- addTransitionCompleteCallbackToPendingTransition(transition);
3281
+ if (markerInstance.aborts === null) {
3282
+ addTransitionCompleteCallbackToPendingTransition(transition);
3283
+ }
3284
incompleteTransitions.delete(transition);
3285
}
3286
});
@@ -3489,21 +3653,6 @@ function commitAtomicPassiveEffects(
3653
}
3654
break;
3655
}
3492
- case TracingMarkerComponent: {
3493
- if (enableTransitionTracing) {
3494
- recursivelyTraverseAtomicPassiveEffects(
3495
- finishedRoot,
3496
- finishedWork,
3497
- committedLanes,
3498
- committedTransitions,
3499
- );
3500
- if (flags & Passive) {
3501
- commitTracingMarkerPassiveMountEffect(finishedWork);
3502
- }
3503
- break;
3504
- }
3505
- // Intentional fallthrough to next branch
3506
- }
3656
// eslint-disable-next-line-no-fallthrough
3657
default: {
3658
recursivelyTraverseAtomicPassiveEffects(
@@ -3827,6 +3976,43 @@ function commitPassiveUnmountInsideDeletedTreeOnFiber(
3976
}
3977
break;
3978
}
3979
+ case SuspenseComponent: {
3980
+ if (enableTransitionTracing) {
3981
+ // We need to mark this fiber's parents as deleted
3982
+ const offscreenFiber: Fiber = (current.child: any);
3983
+ const instance: OffscreenInstance = offscreenFiber.stateNode;
3984
+ const transitions = instance.transitions;
3985
+ if (transitions !== null) {
3986
+ const abortReason = {
3987
+ reason: 'suspense',
3988
+ name: current.memoizedProps.unstable_name || null,
3989
+ };
3990
+ if (
3991
+ current.memoizedState === null ||
3992
+ current.memoizedState.dehydrated === null
3993
+ ) {
3994
+ abortParentMarkerTransitionsForDeletedFiber(
3995
+ offscreenFiber,
3996
+ abortReason,
3997
+ transitions,
3998
+ instance,
3999
+ true,
4000
+ );
4001
+
4002
+ if (nearestMountedAncestor !== null) {
4003
+ abortParentMarkerTransitionsForDeletedFiber(
4004
+ nearestMountedAncestor,
4005
+ abortReason,
4006
+ transitions,
4007
+ instance,
4008
+ false,
4009
+ );
4010
+ }
4011
+ }
4012
+ }
4013
+ }
4014
+ break;
4015
+ }
4016
case CacheComponent: {
4017
if (enableCache) {
4018
const cache = current.memoizedState.cache;
@@ -3834,6 +4020,37 @@ function commitPassiveUnmountInsideDeletedTreeOnFiber(
4020
}
4021
break;
4022
}
4023
+ case TracingMarkerComponent: {
4024
+ if (enableTransitionTracing) {
4025
+ // We need to mark this fiber's parents as deleted
4026
+ const instance: TracingMarkerInstance = current.stateNode;
4027
+ const transitions = instance.transitions;
4028
+ if (transitions !== null) {
4029
+ const abortReason = {
4030
+ reason: 'marker',
4031
+ name: current.memoizedProps.name,
4032
+ };
4033
+ abortParentMarkerTransitionsForDeletedFiber(
4034
+ current,
4035
+ abortReason,
4036
+ transitions,
4037
+ null,
4038
+ true,
4039
+ );
4040
+
4041
+ if (nearestMountedAncestor !== null) {
4042
+ abortParentMarkerTransitionsForDeletedFiber(
4043
+ nearestMountedAncestor,
4044
+ abortReason,
4045
+ transitions,
4046
+ null,
4047
+ false,
4048
+ );
4049
+ }
4050
+ }
4051
+ }
4052
+ break;
4053
+ }
4054
}
4055
}
4056
packages/react-reconciler/src/ReactFiberCompleteWork.new.js
-10
@@ -1589,16 +1589,6 @@ function completeWork(
1589
popMarkerInstance(workInProgress);
1590
}
1591
bubbleProperties(workInProgress);
1592
-
1593
- if (
1594
- current === null ||
1595
- (workInProgress.subtreeFlags & Visibility) !== NoFlags
1596
- ) {
1597
- // If any of our suspense children toggle visibility, this means that
1598
- // the pending boundaries array needs to be updated, which we only
1599
- // do in the passive phase.
1600
- workInProgress.flags |= Passive;
1601
- }
1592
}
1593
return null;
1594
}
packages/react-reconciler/src/ReactFiberCompleteWork.old.js
-10
@@ -1589,16 +1589,6 @@ function completeWork(
1589
popMarkerInstance(workInProgress);
1590
}
1591
bubbleProperties(workInProgress);
1592
-
1593
- if (
1594
- current === null ||
1595
- (workInProgress.subtreeFlags & Visibility) !== NoFlags
1596
- ) {
1597
- // If any of our suspense children toggle visibility, this means that
1598
- // the pending boundaries array needs to be updated, which we only
1599
- // do in the passive phase.
1600
- workInProgress.flags |= Passive;
1601
- }
1592
}
1593
return null;
1594
}
packages/react-reconciler/src/ReactFiberTracingMarkerComponent.new.js
+68
-5
@@ -7,7 +7,11 @@
7
* @flow
8
*/
9
10
-import type {TransitionTracingCallbacks, Fiber} from './ReactInternalTypes';
10
+import type {
11
+ TransitionTracingCallbacks,
12
+ Fiber,
13
+ FiberRoot,
14
+} from './ReactInternalTypes';
15
import type {OffscreenInstance} from './ReactFiberOffscreenComponent';
16
import type {StackCursor} from './ReactFiberStack.new';
17
@@ -21,7 +25,14 @@ export type PendingTransitionCallbacks = {
25
transitionStart: Array<Transition> | null,
26
transitionProgress: Map<Transition, PendingBoundaries> | null,
27
transitionComplete: Array<Transition> | null,
24
- markerProgress: Map<string, TracingMarkerInstance> | null,
28
+ markerProgress: Map<
29
+ string,
30
+ {pendingBoundaries: PendingBoundaries, transitions: Set<Transition>},
31
+ > | null,
32
+ markerIncomplete: Map<
33
+ string,
34
+ {aborts: Array<TransitionAbort>, transitions: Set<Transition>},
35
+ > | null,
36
markerComplete: Map<string, Set<Transition>> | null,
37
};
38
@@ -36,11 +47,18 @@ export type BatchConfigTransition = {
47
_updatedFibers?: Set<Fiber>,
48
};
49
50
+// TODO: Is there a way to not include the tag or name here?
51
export type TracingMarkerInstance = {|
52
tag?: TracingMarkerTag,
41
- pendingBoundaries: PendingBoundaries | null,
53
transitions: Set<Transition> | null,
43
- name?: string,
54
+ pendingBoundaries: PendingBoundaries | null,
55
+ aborts: Array<TransitionAbort> | null,
56
+ name: string | null,
57
+|};
58
+
59
+export type TransitionAbort = {|
60
+ reason: 'error' | 'unknown' | 'marker' | 'suspense',
61
+ name?: string | null,
62
|};
63
64
export const TransitionRoot = 0;
@@ -69,6 +87,7 @@ export function processTransitionCallbacks(
87
if (onMarkerProgress != null && markerProgress !== null) {
88
markerProgress.forEach((markerInstance, markerName) => {
89
if (markerInstance.transitions !== null) {
90
+ // TODO: Clone the suspense object so users can't modify it
91
const pending =
92
markerInstance.pendingBoundaries !== null
93
? Array.from(markerInstance.pendingBoundaries.values())
@@ -101,6 +120,48 @@ export function processTransitionCallbacks(
120
});
121
}
122
123
+ const markerIncomplete = pendingTransitions.markerIncomplete;
124
+ const onMarkerIncomplete = callbacks.onMarkerIncomplete;
125
+ if (onMarkerIncomplete != null && markerIncomplete !== null) {
126
+ markerIncomplete.forEach(({transitions, aborts}, markerName) => {
127
+ transitions.forEach(transition => {
128
+ const filteredAborts = [];
129
+ aborts.forEach(abort => {
130
+ switch (abort.reason) {
131
+ case 'marker': {
132
+ filteredAborts.push({
133
+ type: 'marker',
134
+ name: abort.name,
135
+ endTime,
136
+ });
137
+ break;
138
+ }
139
+ case 'suspense': {
140
+ filteredAborts.push({
141
+ type: 'suspense',
142
+ name: abort.name,
143
+ endTime,
144
+ });
145
+ break;
146
+ }
147
+ default: {
148
+ break;
149
+ }
150
+ }
151
+ });
152
+
153
+ if (filteredAborts.length > 0) {
154
+ onMarkerIncomplete(
155
+ transition.name,
156
+ markerName,
157
+ transition.startTime,
158
+ filteredAborts,
159
+ );
160
+ }
161
+ });
162
+ });
163
+ }
164
+
165
const transitionProgress = pendingTransitions.transitionProgress;
166
const onTransitionProgress = callbacks.onTransitionProgress;
167
if (onTransitionProgress != null && transitionProgress !== null) {
@@ -145,7 +206,7 @@ export function pushRootMarkerInstance(workInProgress: Fiber): void {
206
// transitions map. Each entry in this map functions like a tracing
207
// marker does, so we can push it onto the marker instance stack
208
const transitions = getWorkInProgressTransitions();
148
- const root = workInProgress.stateNode;
209
+ const root: FiberRoot = workInProgress.stateNode;
210
211
if (transitions !== null) {
212
transitions.forEach(transition => {
@@ -154,6 +215,8 @@ export function pushRootMarkerInstance(workInProgress: Fiber): void {
215
tag: TransitionRoot,
216
transitions: new Set([transition]),
217
pendingBoundaries: null,
218
+ aborts: null,
219
+ name: null,
220
};
221
root.incompleteTransitions.set(transition, markerInstance);
222
}
packages/react-reconciler/src/ReactFiberTracingMarkerComponent.old.js
+68
-5
@@ -7,7 +7,11 @@
7
* @flow
8
*/
9
10
-import type {TransitionTracingCallbacks, Fiber} from './ReactInternalTypes';
10
+import type {
11
+ TransitionTracingCallbacks,
12
+ Fiber,
13
+ FiberRoot,
14
+} from './ReactInternalTypes';
15
import type {OffscreenInstance} from './ReactFiberOffscreenComponent';
16
import type {StackCursor} from './ReactFiberStack.old';
17
@@ -21,7 +25,14 @@ export type PendingTransitionCallbacks = {
25
transitionStart: Array<Transition> | null,
26
transitionProgress: Map<Transition, PendingBoundaries> | null,
27
transitionComplete: Array<Transition> | null,
24
- markerProgress: Map<string, TracingMarkerInstance> | null,
28
+ markerProgress: Map<
29
+ string,
30
+ {pendingBoundaries: PendingBoundaries, transitions: Set<Transition>},
31
+ > | null,
32
+ markerIncomplete: Map<
33
+ string,
34
+ {aborts: Array<TransitionAbort>, transitions: Set<Transition>},
35
+ > | null,
36
markerComplete: Map<string, Set<Transition>> | null,
37
};
38
@@ -36,11 +47,18 @@ export type BatchConfigTransition = {
47
_updatedFibers?: Set<Fiber>,
48
};
49
50
+// TODO: Is there a way to not include the tag or name here?
51
export type TracingMarkerInstance = {|
52
tag?: TracingMarkerTag,
41
- pendingBoundaries: PendingBoundaries | null,
53
transitions: Set<Transition> | null,
43
- name?: string,
54
+ pendingBoundaries: PendingBoundaries | null,
55
+ aborts: Array<TransitionAbort> | null,
56
+ name: string | null,
57
+|};
58
+
59
+export type TransitionAbort = {|
60
+ reason: 'error' | 'unknown' | 'marker' | 'suspense',
61
+ name?: string | null,
62
|};
63
64
export const TransitionRoot = 0;
@@ -69,6 +87,7 @@ export function processTransitionCallbacks(
87
if (onMarkerProgress != null && markerProgress !== null) {
88
markerProgress.forEach((markerInstance, markerName) => {
89
if (markerInstance.transitions !== null) {
90
+ // TODO: Clone the suspense object so users can't modify it
91
const pending =
92
markerInstance.pendingBoundaries !== null
93
? Array.from(markerInstance.pendingBoundaries.values())
@@ -101,6 +120,48 @@ export function processTransitionCallbacks(
120
});
121
}
122
123
+ const markerIncomplete = pendingTransitions.markerIncomplete;
124
+ const onMarkerIncomplete = callbacks.onMarkerIncomplete;
125
+ if (onMarkerIncomplete != null && markerIncomplete !== null) {
126
+ markerIncomplete.forEach(({transitions, aborts}, markerName) => {
127
+ transitions.forEach(transition => {
128
+ const filteredAborts = [];
129
+ aborts.forEach(abort => {
130
+ switch (abort.reason) {
131
+ case 'marker': {
132
+ filteredAborts.push({
133
+ type: 'marker',
134
+ name: abort.name,
135
+ endTime,
136
+ });
137
+ break;
138
+ }
139
+ case 'suspense': {
140
+ filteredAborts.push({
141
+ type: 'suspense',
142
+ name: abort.name,
143
+ endTime,
144
+ });
145
+ break;
146
+ }
147
+ default: {
148
+ break;
149
+ }
150
+ }
151
+ });
152
+
153
+ if (filteredAborts.length > 0) {
154
+ onMarkerIncomplete(
155
+ transition.name,
156
+ markerName,
157
+ transition.startTime,
158
+ filteredAborts,
159
+ );
160
+ }
161
+ });
162
+ });
163
+ }
164
+
165
const transitionProgress = pendingTransitions.transitionProgress;
166
const onTransitionProgress = callbacks.onTransitionProgress;
167
if (onTransitionProgress != null && transitionProgress !== null) {
@@ -145,7 +206,7 @@ export function pushRootMarkerInstance(workInProgress: Fiber): void {
206
// transitions map. Each entry in this map functions like a tracing
207
// marker does, so we can push it onto the marker instance stack
208
const transitions = getWorkInProgressTransitions();
148
- const root = workInProgress.stateNode;
209
+ const root: FiberRoot = workInProgress.stateNode;
210
211
if (transitions !== null) {
212
transitions.forEach(transition => {
@@ -154,6 +215,8 @@ export function pushRootMarkerInstance(workInProgress: Fiber): void {
215
tag: TransitionRoot,
216
transitions: new Set([transition]),
217
pendingBoundaries: null,
218
+ aborts: null,
219
+ name: null,
220
};
221
root.incompleteTransitions.set(transition, markerInstance);
222
}
packages/react-reconciler/src/ReactFiberWorkLoop.new.js
+35
-1
@@ -19,6 +19,7 @@ import type {
19
PendingTransitionCallbacks,
20
PendingBoundaries,
21
Transition,
22
+ TransitionAbort,
23
} from './ReactFiberTracingMarkerComponent.new';
24
import type {OffscreenInstance} from './ReactFiberOffscreenComponent';
25
@@ -361,6 +362,7 @@ export function addTransitionStartCallbackToPendingTransition(
362
transitionProgress: null,
363
transitionComplete: null,
364
markerProgress: null,
365
+ markerIncomplete: null,
366
markerComplete: null,
367
};
368
}
@@ -376,7 +378,7 @@ export function addTransitionStartCallbackToPendingTransition(
378
export function addMarkerProgressCallbackToPendingTransition(
379
markerName: string,
380
transitions: Set<Transition>,
379
- pendingBoundaries: PendingBoundaries | null,
381
+ pendingBoundaries: PendingBoundaries,
382
) {
383
if (enableTransitionTracing) {
384
if (currentPendingTransitionCallbacks === null) {
@@ -385,6 +387,7 @@ export function addMarkerProgressCallbackToPendingTransition(
387
transitionProgress: null,
388
transitionComplete: null,
389
markerProgress: new Map(),
390
+ markerIncomplete: null,
391
markerComplete: null,
392
};
393
}
@@ -400,6 +403,34 @@ export function addMarkerProgressCallbackToPendingTransition(
403
}
404
}
405
406
+export function addMarkerIncompleteCallbackToPendingTransition(
407
+ markerName: string,
408
+ transitions: Set<Transition>,
409
+ aborts: Array<TransitionAbort>,
410
+) {
411
+ if (enableTransitionTracing) {
412
+ if (currentPendingTransitionCallbacks === null) {
413
+ currentPendingTransitionCallbacks = {
414
+ transitionStart: null,
415
+ transitionProgress: null,
416
+ transitionComplete: null,
417
+ markerProgress: null,
418
+ markerIncomplete: new Map(),
419
+ markerComplete: null,
420
+ };
421
+ }
422
+
423
+ if (currentPendingTransitionCallbacks.markerIncomplete === null) {
424
+ currentPendingTransitionCallbacks.markerIncomplete = new Map();
425
+ }
426
+
427
+ currentPendingTransitionCallbacks.markerIncomplete.set(markerName, {
428
+ transitions,
429
+ aborts,
430
+ });
431
+ }
432
+}
433
+
434
export function addMarkerCompleteCallbackToPendingTransition(
435
markerName: string,
436
transitions: Set<Transition>,
@@ -411,6 +442,7 @@ export function addMarkerCompleteCallbackToPendingTransition(
442
transitionProgress: null,
443
transitionComplete: null,
444
markerProgress: null,
445
+ markerIncomplete: null,
446
markerComplete: new Map(),
447
};
448
}
@@ -437,6 +469,7 @@ export function addTransitionProgressCallbackToPendingTransition(
469
transitionProgress: new Map(),
470
transitionComplete: null,
471
markerProgress: null,
472
+ markerIncomplete: null,
473
markerComplete: null,
474
};
475
}
@@ -462,6 +495,7 @@ export function addTransitionCompleteCallbackToPendingTransition(
495
transitionProgress: null,
496
transitionComplete: [],
497
markerProgress: null,
498
+ markerIncomplete: null,
499
markerComplete: null,
500
};
501
}
packages/react-reconciler/src/ReactFiberWorkLoop.old.js
+35
-1
@@ -19,6 +19,7 @@ import type {
19
PendingTransitionCallbacks,
20
PendingBoundaries,
21
Transition,
22
+ TransitionAbort,
23
} from './ReactFiberTracingMarkerComponent.old';
24
import type {OffscreenInstance} from './ReactFiberOffscreenComponent';
25
@@ -361,6 +362,7 @@ export function addTransitionStartCallbackToPendingTransition(
362
transitionProgress: null,
363
transitionComplete: null,
364
markerProgress: null,
365
+ markerIncomplete: null,
366
markerComplete: null,
367
};
368
}
@@ -376,7 +378,7 @@ export function addTransitionStartCallbackToPendingTransition(
378
export function addMarkerProgressCallbackToPendingTransition(
379
markerName: string,
380
transitions: Set<Transition>,
379
- pendingBoundaries: PendingBoundaries | null,
381
+ pendingBoundaries: PendingBoundaries,
382
) {
383
if (enableTransitionTracing) {
384
if (currentPendingTransitionCallbacks === null) {
@@ -385,6 +387,7 @@ export function addMarkerProgressCallbackToPendingTransition(
387
transitionProgress: null,
388
transitionComplete: null,
389
markerProgress: new Map(),
390
+ markerIncomplete: null,
391
markerComplete: null,
392
};
393
}
@@ -400,6 +403,34 @@ export function addMarkerProgressCallbackToPendingTransition(
403
}
404
}
405
406
+export function addMarkerIncompleteCallbackToPendingTransition(
407
+ markerName: string,
408
+ transitions: Set<Transition>,
409
+ aborts: Array<TransitionAbort>,
410
+) {
411
+ if (enableTransitionTracing) {
412
+ if (currentPendingTransitionCallbacks === null) {
413
+ currentPendingTransitionCallbacks = {
414
+ transitionStart: null,
415
+ transitionProgress: null,
416
+ transitionComplete: null,
417
+ markerProgress: null,
418
+ markerIncomplete: new Map(),
419
+ markerComplete: null,
420
+ };
421
+ }
422
+
423
+ if (currentPendingTransitionCallbacks.markerIncomplete === null) {
424
+ currentPendingTransitionCallbacks.markerIncomplete = new Map();
425
+ }
426
+
427
+ currentPendingTransitionCallbacks.markerIncomplete.set(markerName, {
428
+ transitions,
429
+ aborts,
430
+ });
431
+ }
432
+}
433
+
434
export function addMarkerCompleteCallbackToPendingTransition(
435
markerName: string,
436
transitions: Set<Transition>,
@@ -411,6 +442,7 @@ export function addMarkerCompleteCallbackToPendingTransition(
442
transitionProgress: null,
443
transitionComplete: null,
444
markerProgress: null,
445
+ markerIncomplete: null,
446
markerComplete: new Map(),
447
};
448
}
@@ -437,6 +469,7 @@ export function addTransitionProgressCallbackToPendingTransition(
469
transitionProgress: new Map(),
470
transitionComplete: null,
471
markerProgress: null,
472
+ markerIncomplete: null,
473
markerComplete: null,
474
};
475
}
@@ -462,6 +495,7 @@ export function addTransitionCompleteCallbackToPendingTransition(
495
transitionProgress: null,
496
transitionComplete: [],
497
markerProgress: null,
498
+ markerIncomplete: null,
499
markerComplete: null,
500
};
501
}
packages/react-reconciler/src/ReactInternalTypes.js
+3
-5
@@ -291,8 +291,7 @@ export type TransitionTracingCallbacks = {
291
startTime: number,
292
deletions: Array<{
293
type: string,
294
- name?: string,
295
- newName?: string,
294
+ name?: string | null,
295
endTime: number,
296
}>,
297
) => void,
@@ -314,8 +313,7 @@ export type TransitionTracingCallbacks = {
313
startTime: number,
314
deletions: Array<{
315
type: string,
317
- name?: string,
318
- newName?: string,
316
+ name?: string | null,
317
endTime: number,
318
}>,
319
) => void,
@@ -337,7 +335,7 @@ type TransitionTracingOnlyFiberRootProperties = {|
335
// are considered complete when the pending suspense boundaries set is
336
// empty. We can represent this as a Map of transitions to suspense
337
// boundary sets
340
- incompleteTransitions: Map<Array<Transition>, TracingMarkerInstance>,
338
+ incompleteTransitions: Map<Transition, TracingMarkerInstance>,
339
|};
340
341
// Exported FiberRoot type includes all properties,
packages/react-reconciler/src/__tests__/ReactTransitionTracing-test.js
+840
-1
@@ -23,6 +23,17 @@ let caches;
23
let seededCache;
24
25
describe('ReactInteractionTracing', () => {
26
+ function stringifyDeletions(deletions) {
27
+ return deletions
28
+ .map(
29
+ d =>
30
+ `{${Object.keys(d)
31
+ .map(key => `${key}: ${d[key]}`)
32
+ .sort()
33
+ .join(', ')}}`,
34
+ )
35
+ .join(', ');
36
+ }
37
beforeEach(() => {
38
jest.resetModules();
39
@@ -1284,18 +1295,846 @@ describe('ReactInteractionTracing', () => {
1295
});
1296
1297
// @gate enableTransitionTracing
1287
- it('warns when marker name changes', async () => {
1298
+ it.skip('warn and calls marker incomplete if name changes before transition completes', async () => {
1299
+ const transitionCallbacks = {
1300
+ onTransitionStart: (name, startTime) => {
1301
+ Scheduler.unstable_yieldValue(
1302
+ `onTransitionStart(${name}, ${startTime})`,
1303
+ );
1304
+ },
1305
+ onTransitionProgress: (name, startTime, endTime, pending) => {
1306
+ const suspenseNames = pending.map(p => p.name || '<null>').join(', ');
1307
+ Scheduler.unstable_yieldValue(
1308
+ `onTransitionProgress(${name}, ${startTime}, ${endTime}, [${suspenseNames}])`,
1309
+ );
1310
+ },
1311
+ onTransitionComplete: (name, startTime, endTime) => {
1312
+ Scheduler.unstable_yieldValue(
1313
+ `onTransitionComplete(${name}, ${startTime}, ${endTime})`,
1314
+ );
1315
+ },
1316
+ onMarkerProgress: (
1317
+ transitioName,
1318
+ markerName,
1319
+ startTime,
1320
+ currentTime,
1321
+ pending,
1322
+ ) => {
1323
+ const suspenseNames = pending.map(p => p.name || '<null>').join(', ');
1324
+ Scheduler.unstable_yieldValue(
1325
+ `onMarkerProgress(${transitioName}, ${markerName}, ${startTime}, ${currentTime}, [${suspenseNames}])`,
1326
+ );
1327
+ },
1328
+ onMarkerIncomplete: (
1329
+ transitionName,
1330
+ markerName,
1331
+ startTime,
1332
+ deletions,
1333
+ ) => {
1334
+ Scheduler.unstable_yieldValue(
1335
+ `onMarkerIncomplete(${transitionName}, ${markerName}, ${startTime}, [${stringifyDeletions(
1336
+ deletions,
1337
+ )}])`,
1338
+ );
1339
+ },
1340
+ onMarkerComplete: (transitioName, markerName, startTime, endTime) => {
1341
+ Scheduler.unstable_yieldValue(
1342
+ `onMarkerComplete(${transitioName}, ${markerName}, ${startTime}, ${endTime})`,
1343
+ );
1344
+ },
1345
+ };
1346
+
1347
+ function App({navigate, markerName}) {
1348
+ return (
1349
+ <div>
1350
+ {navigate ? (
1351
+ <React.unstable_TracingMarker name={markerName}>
1352
+ <Suspense fallback={<Text text="Loading..." />}>
1353
+ <AsyncText text="Page Two" />
1354
+ </Suspense>
1355
+ </React.unstable_TracingMarker>
1356
+ ) : (
1357
+ <Text text="Page One" />
1358
+ )}
1359
+ </div>
1360
+ );
1361
+ }
1362
+
1363
+ const root = ReactNoop.createRoot({
1364
+ unstable_transitionCallbacks: transitionCallbacks,
1365
+ });
1366
+ await act(async () => {
1367
+ root.render(<App navigate={false} markerName="marker one" />);
1368
+ ReactNoop.expire(1000);
1369
+ await advanceTimers(1000);
1370
+ expect(Scheduler).toFlushAndYield(['Page One']);
1371
+
1372
+ startTransition(
1373
+ () => root.render(<App navigate={true} markerName="marker one" />),
1374
+ {
1375
+ name: 'transition one',
1376
+ },
1377
+ );
1378
+ ReactNoop.expire(1000);
1379
+ await advanceTimers(1000);
1380
+
1381
+ expect(Scheduler).toFlushAndYield([
1382
+ 'Suspend [Page Two]',
1383
+ 'Loading...',
1384
+ 'onTransitionStart(transition one, 1000)',
1385
+ 'onMarkerProgress(transition one, marker one, 1000, 2000, [<null>])',
1386
+ 'onTransitionProgress(transition one, 1000, 2000, [<null>])',
1387
+ ]);
1388
+
1389
+ root.render(<App navigate={true} markerName="marker two" />);
1390
+ ReactNoop.expire(1000);
1391
+ await advanceTimers(1000);
1392
+ expect(() =>
1393
+ expect(Scheduler).toFlushAndYield([
1394
+ 'Suspend [Page Two]',
1395
+ 'Loading...',
1396
+ 'onMarkerIncomplete(transition one, marker one, 1000, [{endTime: 3000, name: marker one, newName: marker two, type: marker}])',
1397
+ ]),
1398
+ ).toErrorDev('');
1399
+
1400
+ resolveText('Page Two');
1401
+ ReactNoop.expire(1000);
1402
+ await advanceTimers(1000);
1403
+ expect(Scheduler).toFlushAndYield([
1404
+ 'Page Two',
1405
+ 'onMarkerProgress(transition one, marker one, 1000, 4000, [])',
1406
+ 'onTransitionProgress(transition one, 1000, 4000, [])',
1407
+ 'onTransitionComplete(transition one, 1000, 4000)',
1408
+ ]);
1409
+ });
1410
+ });
1411
+
1412
+ // @gate enableTransitionTracing
1413
+ it('marker incomplete for tree with parent and sibling tracing markers', async () => {
1414
+ const transitionCallbacks = {
1415
+ onTransitionStart: (name, startTime) => {
1416
+ Scheduler.unstable_yieldValue(
1417
+ `onTransitionStart(${name}, ${startTime})`,
1418
+ );
1419
+ },
1420
+ onTransitionProgress: (name, startTime, endTime, pending) => {
1421
+ const suspenseNames = pending.map(p => p.name || '<null>').join(', ');
1422
+ Scheduler.unstable_yieldValue(
1423
+ `onTransitionProgress(${name}, ${startTime}, ${endTime}, [${suspenseNames}])`,
1424
+ );
1425
+ },
1426
+ onTransitionComplete: (name, startTime, endTime) => {
1427
+ Scheduler.unstable_yieldValue(
1428
+ `onTransitionComplete(${name}, ${startTime}, ${endTime})`,
1429
+ );
1430
+ },
1431
+ onMarkerProgress: (
1432
+ transitioName,
1433
+ markerName,
1434
+ startTime,
1435
+ currentTime,
1436
+ pending,
1437
+ ) => {
1438
+ const suspenseNames = pending.map(p => p.name || '<null>').join(', ');
1439
+ Scheduler.unstable_yieldValue(
1440
+ `onMarkerProgress(${transitioName}, ${markerName}, ${startTime}, ${currentTime}, [${suspenseNames}])`,
1441
+ );
1442
+ },
1443
+ onMarkerIncomplete: (
1444
+ transitionName,
1445
+ markerName,
1446
+ startTime,
1447
+ deletions,
1448
+ ) => {
1449
+ Scheduler.unstable_yieldValue(
1450
+ `onMarkerIncomplete(${transitionName}, ${markerName}, ${startTime}, [${stringifyDeletions(
1451
+ deletions,
1452
+ )}])`,
1453
+ );
1454
+ },
1455
+ onMarkerComplete: (transitioName, markerName, startTime, endTime) => {
1456
+ Scheduler.unstable_yieldValue(
1457
+ `onMarkerComplete(${transitioName}, ${markerName}, ${startTime}, ${endTime})`,
1458
+ );
1459
+ },
1460
+ };
1461
+
1462
+ function App({navigate, showMarker}) {
1463
+ return (
1464
+ <div>
1465
+ {navigate ? (
1466
+ <React.unstable_TracingMarker name="parent">
1467
+ {showMarker ? (
1468
+ <React.unstable_TracingMarker name="marker one">
1469
+ <Suspense
1470
+ unstable_name="suspense page"
1471
+ fallback={<Text text="Loading..." />}>
1472
+ <AsyncText text="Page Two" />
1473
+ </Suspense>
1474
+ </React.unstable_TracingMarker>
1475
+ ) : (
1476
+ <Suspense
1477
+ unstable_name="suspense page"
1478
+ fallback={<Text text="Loading..." />}>
1479
+ <AsyncText text="Page Two" />
1480
+ </Suspense>
1481
+ )}
1482
+ <React.unstable_TracingMarker name="sibling">
1483
+ <Suspense
1484
+ unstable_name="suspense sibling"
1485
+ fallback={<Text text="Sibling Loading..." />}>
1486
+ <AsyncText text="Sibling Text" />
1487
+ </Suspense>
1488
+ </React.unstable_TracingMarker>
1489
+ </React.unstable_TracingMarker>
1490
+ ) : (
1491
+ <Text text="Page One" />
1492
+ )}
1493
+ </div>
1494
+ );
1495
+ }
1496
+
1497
+ const root = ReactNoop.createRoot({
1498
+ unstable_transitionCallbacks: transitionCallbacks,
1499
+ });
1500
+ await act(async () => {
1501
+ root.render(<App navigate={false} showMarker={true} />);
1502
+ ReactNoop.expire(1000);
1503
+ await advanceTimers(1000);
1504
+ expect(Scheduler).toFlushAndYield(['Page One']);
1505
+
1506
+ startTransition(
1507
+ () => root.render(<App navigate={true} showMarker={true} />),
1508
+ {
1509
+ name: 'transition one',
1510
+ },
1511
+ );
1512
+ ReactNoop.expire(1000);
1513
+ await advanceTimers(1000);
1514
+ expect(Scheduler).toFlushAndYield([
1515
+ 'Suspend [Page Two]',
1516
+ 'Loading...',
1517
+ 'Suspend [Sibling Text]',
1518
+ 'Sibling Loading...',
1519
+ 'onTransitionStart(transition one, 1000)',
1520
+ 'onMarkerProgress(transition one, parent, 1000, 2000, [suspense page, suspense sibling])',
1521
+ 'onMarkerProgress(transition one, marker one, 1000, 2000, [suspense page])',
1522
+ 'onMarkerProgress(transition one, sibling, 1000, 2000, [suspense sibling])',
1523
+ 'onTransitionProgress(transition one, 1000, 2000, [suspense page, suspense sibling])',
1524
+ ]);
1525
+ root.render(<App navigate={true} showMarker={false} />);
1526
+
1527
+ ReactNoop.expire(1000);
1528
+ await advanceTimers(1000);
1529
+ expect(Scheduler).toFlushAndYield([
1530
+ 'Suspend [Page Two]',
1531
+ 'Loading...',
1532
+ 'Suspend [Sibling Text]',
1533
+ 'Sibling Loading...',
1534
+ 'onMarkerProgress(transition one, parent, 1000, 3000, [suspense sibling])',
1535
+ 'onMarkerIncomplete(transition one, marker one, 1000, [{endTime: 3000, name: marker one, type: marker}, {endTime: 3000, name: suspense page, type: suspense}])',
1536
+ 'onMarkerIncomplete(transition one, parent, 1000, [{endTime: 3000, name: marker one, type: marker}, {endTime: 3000, name: suspense page, type: suspense}])',
1537
+ ]);
1538
+
1539
+ root.render(<App navigate={true} showMarker={true} />);
1540
+ ReactNoop.expire(1000);
1541
+ await advanceTimers(1000);
1542
+ expect(Scheduler).toFlushAndYield([
1543
+ 'Suspend [Page Two]',
1544
+ 'Loading...',
1545
+ 'Suspend [Sibling Text]',
1546
+ 'Sibling Loading...',
1547
+ ]);
1548
+ });
1549
+
1550
+ resolveText('Page Two');
1551
+ ReactNoop.expire(1000);
1552
+ await advanceTimers(1000);
1553
+ expect(Scheduler).toFlushAndYield(['Page Two']);
1554
+
1555
+ resolveText('Sibling Text');
1556
+ ReactNoop.expire(1000);
1557
+ await advanceTimers(1000);
1558
+ expect(Scheduler).toFlushAndYield([
1559
+ 'Sibling Text',
1560
+ 'onMarkerProgress(transition one, parent, 1000, 6000, [])',
1561
+ 'onMarkerProgress(transition one, sibling, 1000, 6000, [])',
1562
+ // Calls markerComplete and transitionComplete for all parents
1563
+ 'onMarkerComplete(transition one, sibling, 1000, 6000)',
1564
+ 'onTransitionProgress(transition one, 1000, 6000, [])',
1565
+ ]);
1566
+ });
1567
+
1568
+ // @gate enableTransitionTracing
1569
+ it('marker gets deleted', async () => {
1570
const transitionCallbacks = {
1571
onTransitionStart: (name, startTime) => {
1572
Scheduler.unstable_yieldValue(
1573
`onTransitionStart(${name}, ${startTime})`,
1574
);
1575
},
1576
+ onTransitionProgress: (name, startTime, endTime, pending) => {
1577
+ const suspenseNames = pending.map(p => p.name || '<null>').join(', ');
1578
+ Scheduler.unstable_yieldValue(
1579
+ `onTransitionProgress(${name}, ${startTime}, ${endTime}, [${suspenseNames}])`,
1580
+ );
1581
+ },
1582
onTransitionComplete: (name, startTime, endTime) => {
1583
Scheduler.unstable_yieldValue(
1584
`onTransitionComplete(${name}, ${startTime}, ${endTime})`,
1585
);
1586
},
1587
+ onMarkerProgress: (
1588
+ transitioName,
1589
+ markerName,
1590
+ startTime,
1591
+ currentTime,
1592
+ pending,
1593
+ ) => {
1594
+ const suspenseNames = pending.map(p => p.name || '<null>').join(', ');
1595
+ Scheduler.unstable_yieldValue(
1596
+ `onMarkerProgress(${transitioName}, ${markerName}, ${startTime}, ${currentTime}, [${suspenseNames}])`,
1597
+ );
1598
+ },
1599
+ onMarkerIncomplete: (
1600
+ transitionName,
1601
+ markerName,
1602
+ startTime,
1603
+ deletions,
1604
+ ) => {
1605
+ Scheduler.unstable_yieldValue(
1606
+ `onMarkerIncomplete(${transitionName}, ${markerName}, ${startTime}, [${stringifyDeletions(
1607
+ deletions,
1608
+ )}])`,
1609
+ );
1610
+ },
1611
+ onMarkerComplete: (transitioName, markerName, startTime, endTime) => {
1612
+ Scheduler.unstable_yieldValue(
1613
+ `onMarkerComplete(${transitioName}, ${markerName}, ${startTime}, ${endTime})`,
1614
+ );
1615
+ },
1616
+ };
1617
+
1618
+ function App({navigate, deleteOne}) {
1619
+ return (
1620
+ <div>
1621
+ {navigate ? (
1622
+ <React.unstable_TracingMarker name="parent">
1623
+ {!deleteOne ? (
1624
+ <div>
1625
+ <React.unstable_TracingMarker name="one">
1626
+ <Suspense
1627
+ unstable_name="suspense one"
1628
+ fallback={<Text text="Loading One..." />}>
1629
+ <AsyncText text="Page One" />
1630
+ </Suspense>
1631
+ </React.unstable_TracingMarker>
1632
+ </div>
1633
+ ) : null}
1634
+ <React.unstable_TracingMarker name="two">
1635
+ <Suspense
1636
+ unstable_name="suspense two"
1637
+ fallback={<Text text="Loading Two..." />}>
1638
+ <AsyncText text="Page Two" />
1639
+ </Suspense>
1640
+ </React.unstable_TracingMarker>
1641
+ </React.unstable_TracingMarker>
1642
+ ) : (
1643
+ <Text text="Page One" />
1644
+ )}
1645
+ </div>
1646
+ );
1647
+ }
1648
+ const root = ReactNoop.createRoot({
1649
+ unstable_transitionCallbacks: transitionCallbacks,
1650
+ });
1651
+ await act(async () => {
1652
+ root.render(<App navigate={false} deleteOne={false} />);
1653
+ ReactNoop.expire(1000);
1654
+ await advanceTimers(1000);
1655
+ expect(Scheduler).toFlushAndYield(['Page One']);
1656
+
1657
+ startTransition(
1658
+ () => root.render(<App navigate={true} deleteOne={false} />),
1659
+ {
1660
+ name: 'transition',
1661
+ },
1662
+ );
1663
+ ReactNoop.expire(1000);
1664
+ await advanceTimers(1000);
1665
+ expect(Scheduler).toFlushAndYield([
1666
+ 'Suspend [Page One]',
1667
+ 'Loading One...',
1668
+ 'Suspend [Page Two]',
1669
+ 'Loading Two...',
1670
+ 'onTransitionStart(transition, 1000)',
1671
+ 'onMarkerProgress(transition, parent, 1000, 2000, [suspense one, suspense two])',
1672
+ 'onMarkerProgress(transition, one, 1000, 2000, [suspense one])',
1673
+ 'onMarkerProgress(transition, two, 1000, 2000, [suspense two])',
1674
+ 'onTransitionProgress(transition, 1000, 2000, [suspense one, suspense two])',
1675
+ ]);
1676
+
1677
+ root.render(<App navigate={true} deleteOne={true} />);
1678
+ ReactNoop.expire(1000);
1679
+ await advanceTimers(1000);
1680
+ expect(Scheduler).toFlushAndYield([
1681
+ 'Suspend [Page Two]',
1682
+ 'Loading Two...',
1683
+ 'onMarkerProgress(transition, parent, 1000, 3000, [suspense two])',
1684
+ 'onMarkerIncomplete(transition, one, 1000, [{endTime: 3000, name: one, type: marker}, {endTime: 3000, name: suspense one, type: suspense}])',
1685
+ 'onMarkerIncomplete(transition, parent, 1000, [{endTime: 3000, name: one, type: marker}, {endTime: 3000, name: suspense one, type: suspense}])',
1686
+ ]);
1687
+
1688
+ await resolveText('Page Two');
1689
+ ReactNoop.expire(1000);
1690
+ await advanceTimers(1000);
1691
+ expect(Scheduler).toFlushAndYield([
1692
+ 'Page Two',
1693
+ // Marker progress will still get called after incomplete but not marker complete
1694
+ 'onMarkerProgress(transition, parent, 1000, 4000, [])',
1695
+ 'onMarkerProgress(transition, two, 1000, 4000, [])',
1696
+ 'onMarkerComplete(transition, two, 1000, 4000)',
1697
+ // Transition progress will still get called after incomplete but not transition complete
1698
+ 'onTransitionProgress(transition, 1000, 4000, [])',
1699
+ ]);
1700
+ });
1701
+ });
1702
+
1703
+ // @gate enableTransitionTracing
1704
+ it('Suspense boundary added by the transition is deleted', async () => {
1705
+ const transitionCallbacks = {
1706
+ onTransitionStart: (name, startTime) => {
1707
+ Scheduler.unstable_yieldValue(
1708
+ `onTransitionStart(${name}, ${startTime})`,
1709
+ );
1710
+ },
1711
+ onTransitionProgress: (name, startTime, endTime, pending) => {
1712
+ const suspenseNames = pending.map(p => p.name || '<null>').join(', ');
1713
+ Scheduler.unstable_yieldValue(
1714
+ `onTransitionProgress(${name}, ${startTime}, ${endTime}, [${suspenseNames}])`,
1715
+ );
1716
+ },
1717
+ onTransitionComplete: (name, startTime, endTime) => {
1718
+ Scheduler.unstable_yieldValue(
1719
+ `onTransitionComplete(${name}, ${startTime}, ${endTime})`,
1720
+ );
1721
+ },
1722
+ onMarkerProgress: (
1723
+ transitioName,
1724
+ markerName,
1725
+ startTime,
1726
+ currentTime,
1727
+ pending,
1728
+ ) => {
1729
+ const suspenseNames = pending.map(p => p.name || '<null>').join(', ');
1730
+ Scheduler.unstable_yieldValue(
1731
+ `onMarkerProgress(${transitioName}, ${markerName}, ${startTime}, ${currentTime}, [${suspenseNames}])`,
1732
+ );
1733
+ },
1734
+ onMarkerIncomplete: (
1735
+ transitionName,
1736
+ markerName,
1737
+ startTime,
1738
+ deletions,
1739
+ ) => {
1740
+ Scheduler.unstable_yieldValue(
1741
+ `onMarkerIncomplete(${transitionName}, ${markerName}, ${startTime}, [${stringifyDeletions(
1742
+ deletions,
1743
+ )}])`,
1744
+ );
1745
+ },
1746
+ onMarkerComplete: (transitioName, markerName, startTime, endTime) => {
1747
+ Scheduler.unstable_yieldValue(
1748
+ `onMarkerComplete(${transitioName}, ${markerName}, ${startTime}, ${endTime})`,
1749
+ );
1750
+ },
1751
+ };
1752
+
1753
+ function App({navigate, deleteOne}) {
1754
+ return (
1755
+ <div>
1756
+ {navigate ? (
1757
+ <React.unstable_TracingMarker name="parent">
1758
+ <React.unstable_TracingMarker name="one">
1759
+ {!deleteOne ? (
1760
+ <Suspense
1761
+ unstable_name="suspense one"
1762
+ fallback={<Text text="Loading One..." />}>
1763
+ <AsyncText text="Page One" />
1764
+ <React.unstable_TracingMarker name="page one" />
1765
+ <Suspense
1766
+ unstable_name="suspense child"
1767
+ fallback={<Text text="Loading Child..." />}>
1768
+ <React.unstable_TracingMarker name="child" />
1769
+ <AsyncText text="Child" />
1770
+ </Suspense>
1771
+ </Suspense>
1772
+ ) : null}
1773
+ </React.unstable_TracingMarker>
1774
+ <React.unstable_TracingMarker name="two">
1775
+ <Suspense
1776
+ unstable_name="suspense two"
1777
+ fallback={<Text text="Loading Two..." />}>
1778
+ <AsyncText text="Page Two" />
1779
+ </Suspense>
1780
+ </React.unstable_TracingMarker>
1781
+ </React.unstable_TracingMarker>
1782
+ ) : (
1783
+ <Text text="Page One" />
1784
+ )}
1785
+ </div>
1786
+ );
1787
+ }
1788
+ const root = ReactNoop.createRoot({
1789
+ unstable_transitionCallbacks: transitionCallbacks,
1790
+ });
1791
+ await act(async () => {
1792
+ root.render(<App navigate={false} deleteOne={false} />);
1793
+
1794
+ ReactNoop.expire(1000);
1795
+ await advanceTimers(1000);
1796
+ expect(Scheduler).toFlushAndYield(['Page One']);
1797
+
1798
+ startTransition(
1799
+ () => root.render(<App navigate={true} deleteOne={false} />),
1800
+ {
1801
+ name: 'transition',
1802
+ },
1803
+ );
1804
+ ReactNoop.expire(1000);
1805
+ await advanceTimers(1000);
1806
+ expect(Scheduler).toFlushAndYield([
1807
+ 'Suspend [Page One]',
1808
+ 'Suspend [Child]',
1809
+ 'Loading Child...',
1810
+ 'Loading One...',
1811
+ 'Suspend [Page Two]',
1812
+ 'Loading Two...',
1813
+ 'onTransitionStart(transition, 1000)',
1814
+ 'onMarkerProgress(transition, parent, 1000, 2000, [suspense one, suspense two])',
1815
+ 'onMarkerProgress(transition, one, 1000, 2000, [suspense one])',
1816
+ 'onMarkerProgress(transition, two, 1000, 2000, [suspense two])',
1817
+ 'onTransitionProgress(transition, 1000, 2000, [suspense one, suspense two])',
1818
+ ]);
1819
+
1820
+ await resolveText('Page One');
1821
+ ReactNoop.expire(1000);
1822
+ await advanceTimers(1000);
1823
+ expect(Scheduler).toFlushAndYield([
1824
+ 'Page One',
1825
+ 'Suspend [Child]',
1826
+ 'Loading Child...',
1827
+ 'onMarkerProgress(transition, parent, 1000, 3000, [suspense two, suspense child])',
1828
+ 'onMarkerProgress(transition, one, 1000, 3000, [suspense child])',
1829
+ 'onMarkerComplete(transition, page one, 1000, 3000)',
1830
+ 'onTransitionProgress(transition, 1000, 3000, [suspense two, suspense child])',
1831
+ ]);
1832
+
1833
+ root.render(<App navigate={true} deleteOne={true} />);
1834
+ ReactNoop.expire(1000);
1835
+ await advanceTimers(1000);
1836
+ expect(Scheduler).toFlushAndYield([
1837
+ 'Suspend [Page Two]',
1838
+ 'Loading Two...',
1839
+ // "suspense one" has unsuspended so shouldn't be included
1840
+ // tracing marker "page one" has completed so shouldn't be included
1841
+ // all children of "suspense child" haven't yet been rendered so shouldn't be included
1842
+ 'onMarkerProgress(transition, one, 1000, 4000, [])',
1843
+ 'onMarkerProgress(transition, parent, 1000, 4000, [suspense two])',
1844
+ 'onMarkerIncomplete(transition, one, 1000, [{endTime: 4000, name: suspense child, type: suspense}])',
1845
+ 'onMarkerIncomplete(transition, parent, 1000, [{endTime: 4000, name: suspense child, type: suspense}])',
1846
+ ]);
1847
+
1848
+ await resolveText('Page Two');
1849
+ ReactNoop.expire(1000);
1850
+ await advanceTimers(1000);
1851
+ expect(Scheduler).toFlushAndYield([
1852
+ 'Page Two',
1853
+ 'onMarkerProgress(transition, parent, 1000, 5000, [])',
1854
+ 'onMarkerProgress(transition, two, 1000, 5000, [])',
1855
+ 'onMarkerComplete(transition, two, 1000, 5000)',
1856
+ 'onTransitionProgress(transition, 1000, 5000, [])',
1857
+ ]);
1858
+ });
1859
+ });
1860
+
1861
+ // @gate enableTransitionTracing
1862
+ it('Suspense boundary not added by the transition is deleted ', async () => {
1863
+ const transitionCallbacks = {
1864
+ onTransitionStart: (name, startTime) => {
1865
+ Scheduler.unstable_yieldValue(
1866
+ `onTransitionStart(${name}, ${startTime})`,
1867
+ );
1868
+ },
1869
+ onTransitionProgress: (name, startTime, endTime, pending) => {
1870
+ const suspenseNames = pending.map(p => p.name || '<null>').join(', ');
1871
+ Scheduler.unstable_yieldValue(
1872
+ `onTransitionProgress(${name}, ${startTime}, ${endTime}, [${suspenseNames}])`,
1873
+ );
1874
+ },
1875
+ onTransitionComplete: (name, startTime, endTime) => {
1876
+ Scheduler.unstable_yieldValue(
1877
+ `onTransitionComplete(${name}, ${startTime}, ${endTime})`,
1878
+ );
1879
+ },
1880
+ onMarkerProgress: (
1881
+ transitioName,
1882
+ markerName,
1883
+ startTime,
1884
+ currentTime,
1885
+ pending,
1886
+ ) => {
1887
+ const suspenseNames = pending.map(p => p.name || '<null>').join(', ');
1888
+ Scheduler.unstable_yieldValue(
1889
+ `onMarkerProgress(${transitioName}, ${markerName}, ${startTime}, ${currentTime}, [${suspenseNames}])`,
1890
+ );
1891
+ },
1892
+ onMarkerIncomplete: (
1893
+ transitionName,
1894
+ markerName,
1895
+ startTime,
1896
+ deletions,
1897
+ ) => {
1898
+ Scheduler.unstable_yieldValue(
1899
+ `onMarkerIncomplete(${transitionName}, ${markerName}, ${startTime}, [${stringifyDeletions(
1900
+ deletions,
1901
+ )}])`,
1902
+ );
1903
+ },
1904
+ onMarkerComplete: (transitioName, markerName, startTime, endTime) => {
1905
+ Scheduler.unstable_yieldValue(
1906
+ `onMarkerComplete(${transitioName}, ${markerName}, ${startTime}, ${endTime})`,
1907
+ );
1908
+ },
1909
+ };
1910
+
1911
+ function App({show}) {
1912
+ return (
1913
+ <React.unstable_TracingMarker name="parent">
1914
+ {show ? (
1915
+ <Suspense unstable_name="appended child">
1916
+ <AsyncText text="Appended child" />
1917
+ </Suspense>
1918
+ ) : null}
1919
+ <Suspense unstable_name="child">
1920
+ <AsyncText text="Child" />
1921
+ </Suspense>
1922
+ </React.unstable_TracingMarker>
1923
+ );
1924
+ }
1925
+
1926
+ const root = ReactNoop.createRoot({
1927
+ unstable_transitionCallbacks: transitionCallbacks,
1928
+ });
1929
+ await act(async () => {
1930
+ startTransition(() => root.render(<App show={false} />), {
1931
+ name: 'transition',
1932
+ });
1933
+ ReactNoop.expire(1000);
1934
+ await advanceTimers(1000);
1935
+
1936
+ expect(Scheduler).toFlushAndYield([
1937
+ 'Suspend [Child]',
1938
+ 'onTransitionStart(transition, 0)',
1939
+ 'onMarkerProgress(transition, parent, 0, 1000, [child])',
1940
+ 'onTransitionProgress(transition, 0, 1000, [child])',
1941
+ ]);
1942
+
1943
+ root.render(<App show={true} />);
1944
+ ReactNoop.expire(1000);
1945
+ await advanceTimers(1000);
1946
+ // This appended child isn't part of the transition so we
1947
+ // don't call any callback
1948
+ expect(Scheduler).toFlushAndYield([
1949
+ 'Suspend [Appended child]',
1950
+ 'Suspend [Child]',
1951
+ ]);
1952
+
1953
+ // This deleted child isn't part of the transition so we
1954
+ // don't call any callbacks
1955
+ root.render(<App show={false} />);
1956
+ ReactNoop.expire(1000);
1957
+ await advanceTimers(1000);
1958
+ expect(Scheduler).toFlushAndYield(['Suspend [Child]']);
1959
+
1960
+ await resolveText('Child');
1961
+ ReactNoop.expire(1000);
1962
+ await advanceTimers(1000);
1963
+
1964
+ expect(Scheduler).toFlushAndYield([
1965
+ 'Child',
1966
+ 'onMarkerProgress(transition, parent, 0, 4000, [])',
1967
+ 'onMarkerComplete(transition, parent, 0, 4000)',
1968
+ 'onTransitionProgress(transition, 0, 4000, [])',
1969
+ 'onTransitionComplete(transition, 0, 4000)',
1970
+ ]);
1971
+ });
1972
+ });
1973
+
1974
+ // @gate enableTransitionTracing
1975
+ it('marker incomplete gets called properly if child suspense marker is not part of it', async () => {
1976
+ const transitionCallbacks = {
1977
+ onTransitionStart: (name, startTime) => {
1978
+ Scheduler.unstable_yieldValue(
1979
+ `onTransitionStart(${name}, ${startTime})`,
1980
+ );
1981
+ },
1982
+ onTransitionProgress: (name, startTime, endTime, pending) => {
1983
+ const suspenseNames = pending.map(p => p.name || '<null>').join(', ');
1984
+ Scheduler.unstable_yieldValue(
1985
+ `onTransitionProgress(${name}, ${startTime}, ${endTime}, [${suspenseNames}])`,
1986
+ );
1987
+ },
1988
+ onTransitionComplete: (name, startTime, endTime) => {
1989
+ Scheduler.unstable_yieldValue(
1990
+ `onTransitionComplete(${name}, ${startTime}, ${endTime})`,
1991
+ );
1992
+ },
1993
+ onMarkerProgress: (
1994
+ transitioName,
1995
+ markerName,
1996
+ startTime,
1997
+ currentTime,
1998
+ pending,
1999
+ ) => {
2000
+ const suspenseNames = pending.map(p => p.name || '<null>').join(', ');
2001
+ Scheduler.unstable_yieldValue(
2002
+ `onMarkerProgress(${transitioName}, ${markerName}, ${startTime}, ${currentTime}, [${suspenseNames}])`,
2003
+ );
2004
+ },
2005
+ onMarkerIncomplete: (
2006
+ transitionName,
2007
+ markerName,
2008
+ startTime,
2009
+ deletions,
2010
+ ) => {
2011
+ Scheduler.unstable_yieldValue(
2012
+ `onMarkerIncomplete(${transitionName}, ${markerName}, ${startTime}, [${stringifyDeletions(
2013
+ deletions,
2014
+ )}])`,
2015
+ );
2016
+ },
2017
+ onMarkerComplete: (transitioName, markerName, startTime, endTime) => {
2018
+ Scheduler.unstable_yieldValue(
2019
+ `onMarkerComplete(${transitioName}, ${markerName}, ${startTime}, ${endTime})`,
2020
+ );
2021
+ },
2022
+ };
2023
+
2024
+ function App({show, showSuspense}) {
2025
+ return (
2026
+ <React.unstable_TracingMarker name="parent">
2027
+ {show ? (
2028
+ <React.unstable_TracingMarker name="appended child">
2029
+ {showSuspense ? (
2030
+ <Suspense unstable_name="appended child">
2031
+ <AsyncText text="Appended child" />
2032
+ </Suspense>
2033
+ ) : null}
2034
+ </React.unstable_TracingMarker>
2035
+ ) : null}
2036
+ <Suspense unstable_name="child">
2037
+ <AsyncText text="Child" />
2038
+ </Suspense>
2039
+ </React.unstable_TracingMarker>
2040
+ );
2041
+ }
2042
+
2043
+ const root = ReactNoop.createRoot({
2044
+ unstable_transitionCallbacks: transitionCallbacks,
2045
+ });
2046
+
2047
+ await act(async () => {
2048
+ startTransition(
2049
+ () => root.render(<App show={false} showSuspense={false} />),
2050
+ {
2051
+ name: 'transition one',
2052
+ },
2053
+ );
2054
+
2055
+ ReactNoop.expire(1000);
2056
+ await advanceTimers(1000);
2057
+ });
2058
+
2059
+ expect(Scheduler).toHaveYielded([
2060
+ 'Suspend [Child]',
2061
+ 'onTransitionStart(transition one, 0)',
2062
+ 'onMarkerProgress(transition one, parent, 0, 1000, [child])',
2063
+ 'onTransitionProgress(transition one, 0, 1000, [child])',
2064
+ ]);
2065
+
2066
+ await act(async () => {
2067
+ startTransition(
2068
+ () => root.render(<App show={true} showSuspense={true} />),
2069
+ {
2070
+ name: 'transition two',
2071
+ },
2072
+ );
2073
+
2074
+ ReactNoop.expire(1000);
2075
+ await advanceTimers(1000);
2076
+ });
2077
+
2078
+ expect(Scheduler).toHaveYielded([
2079
+ 'Suspend [Appended child]',
2080
+ 'Suspend [Child]',
2081
+ 'onTransitionStart(transition two, 1000)',
2082
+ 'onMarkerProgress(transition two, appended child, 1000, 2000, [appended child])',
2083
+ 'onTransitionProgress(transition two, 1000, 2000, [appended child])',
2084
+ ]);
2085
+
2086
+ await act(async () => {
2087
+ root.render(<App show={true} showSuspense={false} />);
2088
+ ReactNoop.expire(1000);
2089
+ await advanceTimers(1000);
2090
+ });
2091
+
2092
+ expect(Scheduler).toHaveYielded([
2093
+ 'Suspend [Child]',
2094
+ 'onMarkerProgress(transition two, appended child, 1000, 3000, [])',
2095
+ 'onMarkerIncomplete(transition two, appended child, 1000, [{endTime: 3000, name: appended child, type: suspense}])',
2096
+ ]);
2097
+
2098
+ await act(async () => {
2099
+ resolveText('Child');
2100
+ ReactNoop.expire(1000);
2101
+ await advanceTimers(1000);
2102
+ });
2103
+
2104
+ expect(Scheduler).toHaveYielded([
2105
+ 'Child',
2106
+ 'onMarkerProgress(transition one, parent, 0, 4000, [])',
2107
+ 'onMarkerComplete(transition one, parent, 0, 4000)',
2108
+ 'onTransitionProgress(transition one, 0, 4000, [])',
2109
+ 'onTransitionComplete(transition one, 0, 4000)',
2110
+ ]);
2111
+ });
2112
+
2113
+ // @gate enableTransitionTracing
2114
+ it('warns when marker name changes', async () => {
2115
+ const transitionCallbacks = {
2116
+ onTransitionStart: (name, startTime) => {
2117
+ Scheduler.unstable_yieldValue(
2118
+ `onTransitionStart(${name}, ${startTime})`,
2119
+ );
2120
+ },
2121
+ onTransitionComplete: (name, startTime, endTime) => {
2122
+ Scheduler.unstable_yieldValue(
2123
+ `onTransitionComplete(${name}, ${startTime}, ${endTime})`,
2124
+ );
2125
+ },
2126
+ onMarkerIncomplete: (
2127
+ transitionName,
2128
+ markerName,
2129
+ startTime,
2130
+ deletions,
2131
+ ) => {
2132
+ Scheduler.unstable_yieldValue(
2133
+ `onMarkerIncomplete(${transitionName}, ${markerName}, ${startTime}, [${stringifyDeletions(
2134
+ deletions,
2135
+ )}])`,
2136
+ );
2137
+ },
2138
onMarkerComplete: (transitioName, markerName, startTime, endTime) => {
2139
Scheduler.unstable_yieldValue(
2140
`onMarkerComplete(${transitioName}, ${markerName}, ${startTime}, ${endTime})`,