diff --git a/packages/react-reconciler/src/ReactFiberCommitWork.new.js b/packages/react-reconciler/src/ReactFiberCommitWork.new.js
index 124667aa5eadb..734d2bfe0e625 100644
--- a/packages/react-reconciler/src/ReactFiberCommitWork.new.js
+++ b/packages/react-reconciler/src/ReactFiberCommitWork.new.js
@@ -2087,13 +2087,26 @@ function commitDeletionEffectsOnFiber(
if (enableTransitionTracing) {
// We need to mark this fiber's parents as deleted
const instance: OffscreenInstance = deletedFiber.stateNode;
- const markers = instance.pendingMarkers;
- if (markers !== null) {
- markers.forEach(marker => {
- if (marker.pendingBoundaries.has(instance)) {
- marker.pendingBoundaries.delete(instance);
- }
- });
+ const transitions = instance.transitions;
+ if (transitions !== null) {
+ let name = null;
+ const parent = deletedFiber.return;
+ if (
+ parent !== null &&
+ parent.tag === SuspenseComponent &&
+ parent.memoizedProps.unstable_name
+ ) {
+ name = parent.memoizedProps.unstable_name;
+ }
+
+ recursivelyAbortParentMarkerTransitions(
+ deletedFiber,
+ nearestMountedAncestor,
+ {
+ type: 'suspense',
+ name,
+ },
+ );
}
}
diff --git a/packages/react-reconciler/src/ReactFiberTracingMarkerComponent.new.js b/packages/react-reconciler/src/ReactFiberTracingMarkerComponent.new.js
index 27b8d04492777..b17e70ed9bd50 100644
--- a/packages/react-reconciler/src/ReactFiberTracingMarkerComponent.new.js
+++ b/packages/react-reconciler/src/ReactFiberTracingMarkerComponent.new.js
@@ -169,6 +169,13 @@ function getFilteredDeletion(deletion: TransitionDeletion, endTime: number) {
endTime,
};
}
+ case 'suspense': {
+ return {
+ type: deletion.type,
+ name: deletion.name,
+ endTime,
+ };
+ }
default: {
return null;
}
diff --git a/packages/react-reconciler/src/ReactInternalTypes.js b/packages/react-reconciler/src/ReactInternalTypes.js
index 6a83f822c6a26..d5ed83d34a077 100644
--- a/packages/react-reconciler/src/ReactInternalTypes.js
+++ b/packages/react-reconciler/src/ReactInternalTypes.js
@@ -291,7 +291,7 @@ export type TransitionTracingCallbacks = {
startTime: number,
deletions: Array<{
type: string,
- name?: string,
+ name?: string | null,
endTime: number,
}>,
) => void,
@@ -313,7 +313,7 @@ export type TransitionTracingCallbacks = {
startTime: number,
deletions: Array<{
type: string,
- name?: string,
+ name?: string | null,
endTime: number,
}>,
) => void,
diff --git a/packages/react-reconciler/src/__tests__/ReactTransitionTracing-test.js b/packages/react-reconciler/src/__tests__/ReactTransitionTracing-test.js
index 1089f0f7e0d0a..a6637b55977c9 100644
--- a/packages/react-reconciler/src/__tests__/ReactTransitionTracing-test.js
+++ b/packages/react-reconciler/src/__tests__/ReactTransitionTracing-test.js
@@ -1531,8 +1531,9 @@ describe('ReactInteractionTracing', () => {
'Loading...',
'Suspend [Sibling Text]',
'Sibling Loading...',
- 'onMarkerIncomplete(transition one, marker one, 1000, [{endTime: 3000, name: marker one, type: marker}])',
- 'onMarkerIncomplete(transition one, parent, 1000, [{endTime: 3000, name: marker one, type: marker}])',
+ 'onMarkerProgress(transition one, parent, 1000, 3000, [suspense sibling])',
+ 'onMarkerIncomplete(transition one, marker one, 1000, [{endTime: 3000, name: marker one, type: marker}, {endTime: 3000, name: suspense page, type: suspense}])',
+ 'onMarkerIncomplete(transition one, parent, 1000, [{endTime: 3000, name: marker one, type: marker}, {endTime: 3000, name: suspense page, type: suspense}])',
]);
root.render();
@@ -1679,8 +1680,9 @@ describe('ReactInteractionTracing', () => {
expect(Scheduler).toFlushAndYield([
'Suspend [Page Two]',
'Loading Two...',
- 'onMarkerIncomplete(transition, one, 1000, [{endTime: 3000, name: one, type: marker}])',
- 'onMarkerIncomplete(transition, parent, 1000, [{endTime: 3000, name: one, type: marker}])',
+ 'onMarkerProgress(transition, parent, 1000, 3000, [suspense two])',
+ 'onMarkerIncomplete(transition, one, 1000, [{endTime: 3000, name: one, type: marker}, {endTime: 3000, name: suspense one, type: suspense}])',
+ 'onMarkerIncomplete(transition, parent, 1000, [{endTime: 3000, name: one, type: marker}, {endTime: 3000, name: suspense one, type: suspense}])',
]);
await resolveText('Page Two');
@@ -1698,6 +1700,416 @@ describe('ReactInteractionTracing', () => {
});
});
+ // @gate enableTransitionTracing
+ it('Suspense boundary added by the transition is deleted', async () => {
+ const transitionCallbacks = {
+ onTransitionStart: (name, startTime) => {
+ Scheduler.unstable_yieldValue(
+ `onTransitionStart(${name}, ${startTime})`,
+ );
+ },
+ onTransitionProgress: (name, startTime, endTime, pending) => {
+ const suspenseNames = pending.map(p => p.name || '').join(', ');
+ Scheduler.unstable_yieldValue(
+ `onTransitionProgress(${name}, ${startTime}, ${endTime}, [${suspenseNames}])`,
+ );
+ },
+ onTransitionComplete: (name, startTime, endTime) => {
+ Scheduler.unstable_yieldValue(
+ `onTransitionComplete(${name}, ${startTime}, ${endTime})`,
+ );
+ },
+ onMarkerProgress: (
+ transitioName,
+ markerName,
+ startTime,
+ currentTime,
+ pending,
+ ) => {
+ const suspenseNames = pending.map(p => p.name || '').join(', ');
+ Scheduler.unstable_yieldValue(
+ `onMarkerProgress(${transitioName}, ${markerName}, ${startTime}, ${currentTime}, [${suspenseNames}])`,
+ );
+ },
+ onMarkerIncomplete: (
+ transitionName,
+ markerName,
+ startTime,
+ deletions,
+ ) => {
+ Scheduler.unstable_yieldValue(
+ `onMarkerIncomplete(${transitionName}, ${markerName}, ${startTime}, [${stringifyDeletions(
+ deletions,
+ )}])`,
+ );
+ },
+ onMarkerComplete: (transitioName, markerName, startTime, endTime) => {
+ Scheduler.unstable_yieldValue(
+ `onMarkerComplete(${transitioName}, ${markerName}, ${startTime}, ${endTime})`,
+ );
+ },
+ };
+
+ function App({navigate, deleteOne}) {
+ return (
+
+ {navigate ? (
+
+
+ {!deleteOne ? (
+ }>
+
+
+ }>
+
+
+
+
+ ) : null}
+
+
+ }>
+
+
+
+
+ ) : (
+
+ )}
+
+ );
+ }
+ const root = ReactNoop.createRoot({
+ unstable_transitionCallbacks: transitionCallbacks,
+ });
+ await act(async () => {
+ root.render();
+
+ ReactNoop.expire(1000);
+ await advanceTimers(1000);
+ expect(Scheduler).toFlushAndYield(['Page One']);
+
+ startTransition(
+ () => root.render(),
+ {
+ name: 'transition',
+ },
+ );
+ ReactNoop.expire(1000);
+ await advanceTimers(1000);
+ expect(Scheduler).toFlushAndYield([
+ 'Suspend [Page One]',
+ 'Suspend [Child]',
+ 'Loading Child...',
+ 'Loading One...',
+ 'Suspend [Page Two]',
+ 'Loading Two...',
+ 'onTransitionStart(transition, 1000)',
+ 'onMarkerProgress(transition, parent, 1000, 2000, [suspense one, suspense two])',
+ 'onMarkerProgress(transition, one, 1000, 2000, [suspense one])',
+ 'onMarkerProgress(transition, two, 1000, 2000, [suspense two])',
+ 'onTransitionProgress(transition, 1000, 2000, [suspense one, suspense two])',
+ ]);
+
+ await resolveText('Page One');
+ ReactNoop.expire(1000);
+ await advanceTimers(1000);
+ expect(Scheduler).toFlushAndYield([
+ 'Page One',
+ 'Suspend [Child]',
+ 'Loading Child...',
+ 'onMarkerProgress(transition, parent, 1000, 3000, [suspense two, suspense child])',
+ 'onMarkerProgress(transition, one, 1000, 3000, [suspense child])',
+ 'onMarkerComplete(transition, page one, 1000, 3000)',
+ 'onTransitionProgress(transition, 1000, 3000, [suspense two, suspense child])',
+ ]);
+
+ root.render();
+ ReactNoop.expire(1000);
+ await advanceTimers(1000);
+ expect(Scheduler).toFlushAndYield([
+ 'Suspend [Page Two]',
+ 'Loading Two...',
+ // "suspense one" has unsuspended so shouldn't be included
+ // tracing marker "page one" has completed so shouldn't be included
+ // all children of "suspense child" haven't yet been rendered so shouldn't be included
+ 'onMarkerProgress(transition, one, 1000, 4000, [])',
+ 'onMarkerProgress(transition, parent, 1000, 4000, [suspense two])',
+ 'onMarkerIncomplete(transition, one, 1000, [{endTime: 4000, name: suspense child, type: suspense}])',
+ 'onMarkerIncomplete(transition, parent, 1000, [{endTime: 4000, name: suspense child, type: suspense}])',
+ ]);
+
+ await resolveText('Page Two');
+ ReactNoop.expire(1000);
+ await advanceTimers(1000);
+ expect(Scheduler).toFlushAndYield([
+ 'Page Two',
+ 'onMarkerProgress(transition, parent, 1000, 5000, [])',
+ 'onMarkerProgress(transition, two, 1000, 5000, [])',
+ 'onMarkerComplete(transition, two, 1000, 5000)',
+ 'onTransitionProgress(transition, 1000, 5000, [])',
+ ]);
+ });
+ });
+
+ // @gate enableTransitionTracing
+ it('Suspense boundary not added by the transition is deleted ', async () => {
+ const transitionCallbacks = {
+ onTransitionStart: (name, startTime) => {
+ Scheduler.unstable_yieldValue(
+ `onTransitionStart(${name}, ${startTime})`,
+ );
+ },
+ onTransitionProgress: (name, startTime, endTime, pending) => {
+ const suspenseNames = pending.map(p => p.name || '').join(', ');
+ Scheduler.unstable_yieldValue(
+ `onTransitionProgress(${name}, ${startTime}, ${endTime}, [${suspenseNames}])`,
+ );
+ },
+ onTransitionComplete: (name, startTime, endTime) => {
+ Scheduler.unstable_yieldValue(
+ `onTransitionComplete(${name}, ${startTime}, ${endTime})`,
+ );
+ },
+ onMarkerProgress: (
+ transitioName,
+ markerName,
+ startTime,
+ currentTime,
+ pending,
+ ) => {
+ const suspenseNames = pending.map(p => p.name || '').join(', ');
+ Scheduler.unstable_yieldValue(
+ `onMarkerProgress(${transitioName}, ${markerName}, ${startTime}, ${currentTime}, [${suspenseNames}])`,
+ );
+ },
+ onMarkerIncomplete: (
+ transitionName,
+ markerName,
+ startTime,
+ deletions,
+ ) => {
+ Scheduler.unstable_yieldValue(
+ `onMarkerIncomplete(${transitionName}, ${markerName}, ${startTime}, [${stringifyDeletions(
+ deletions,
+ )}])`,
+ );
+ },
+ onMarkerComplete: (transitioName, markerName, startTime, endTime) => {
+ Scheduler.unstable_yieldValue(
+ `onMarkerComplete(${transitioName}, ${markerName}, ${startTime}, ${endTime})`,
+ );
+ },
+ };
+
+ function App({show}) {
+ return (
+
+ {show ? (
+
+
+
+ ) : null}
+
+
+
+
+ );
+ }
+
+ const root = ReactNoop.createRoot({
+ unstable_transitionCallbacks: transitionCallbacks,
+ });
+ await act(async () => {
+ startTransition(() => root.render(), {
+ name: 'transition',
+ });
+ ReactNoop.expire(1000);
+ await advanceTimers(1000);
+
+ expect(Scheduler).toFlushAndYield([
+ 'Suspend [Child]',
+ 'onTransitionStart(transition, 0)',
+ 'onMarkerProgress(transition, parent, 0, 1000, [child])',
+ 'onTransitionProgress(transition, 0, 1000, [child])',
+ ]);
+
+ root.render();
+ ReactNoop.expire(1000);
+ await advanceTimers(1000);
+ // This appended child isn't part of the transition so we
+ // don't call any callback
+ expect(Scheduler).toFlushAndYield([
+ 'Suspend [Appended child]',
+ 'Suspend [Child]',
+ ]);
+
+ // This deleted child isn't part of the transition so we
+ // don't call any callbacks
+ root.render();
+ ReactNoop.expire(1000);
+ await advanceTimers(1000);
+ expect(Scheduler).toFlushAndYield(['Suspend [Child]']);
+
+ await resolveText('Child');
+ ReactNoop.expire(1000);
+ await advanceTimers(1000);
+
+ expect(Scheduler).toFlushAndYield([
+ 'Child',
+ 'onMarkerProgress(transition, parent, 0, 4000, [])',
+ 'onMarkerComplete(transition, parent, 0, 4000)',
+ 'onTransitionProgress(transition, 0, 4000, [])',
+ 'onTransitionComplete(transition, 0, 4000)',
+ ]);
+ });
+ });
+
+ // @gate enableTransitionTracing
+ it('marker incomplete gets called properly if child suspense marker is not part of it', async () => {
+ const transitionCallbacks = {
+ onTransitionStart: (name, startTime) => {
+ Scheduler.unstable_yieldValue(
+ `onTransitionStart(${name}, ${startTime})`,
+ );
+ },
+ onTransitionProgress: (name, startTime, endTime, pending) => {
+ const suspenseNames = pending.map(p => p.name || '').join(', ');
+ Scheduler.unstable_yieldValue(
+ `onTransitionProgress(${name}, ${startTime}, ${endTime}, [${suspenseNames}])`,
+ );
+ },
+ onTransitionComplete: (name, startTime, endTime) => {
+ Scheduler.unstable_yieldValue(
+ `onTransitionComplete(${name}, ${startTime}, ${endTime})`,
+ );
+ },
+ onMarkerProgress: (
+ transitioName,
+ markerName,
+ startTime,
+ currentTime,
+ pending,
+ ) => {
+ const suspenseNames = pending.map(p => p.name || '').join(', ');
+ Scheduler.unstable_yieldValue(
+ `onMarkerProgress(${transitioName}, ${markerName}, ${startTime}, ${currentTime}, [${suspenseNames}])`,
+ );
+ },
+ onMarkerIncomplete: (
+ transitionName,
+ markerName,
+ startTime,
+ deletions,
+ ) => {
+ Scheduler.unstable_yieldValue(
+ `onMarkerIncomplete(${transitionName}, ${markerName}, ${startTime}, [${stringifyDeletions(
+ deletions,
+ )}])`,
+ );
+ },
+ onMarkerComplete: (transitioName, markerName, startTime, endTime) => {
+ Scheduler.unstable_yieldValue(
+ `onMarkerComplete(${transitioName}, ${markerName}, ${startTime}, ${endTime})`,
+ );
+ },
+ };
+
+ function App({show, showSuspense}) {
+ return (
+
+ {show ? (
+
+ {showSuspense ? (
+
+
+
+ ) : null}
+
+ ) : null}
+
+
+
+
+ );
+ }
+
+ const root = ReactNoop.createRoot({
+ unstable_transitionCallbacks: transitionCallbacks,
+ });
+
+ await act(async () => {
+ startTransition(
+ () => root.render(),
+ {
+ name: 'transition one',
+ },
+ );
+
+ ReactNoop.expire(1000);
+ await advanceTimers(1000);
+ });
+
+ expect(Scheduler).toHaveYielded([
+ 'Suspend [Child]',
+ 'onTransitionStart(transition one, 0)',
+ 'onMarkerProgress(transition one, parent, 0, 1000, [child])',
+ 'onTransitionProgress(transition one, 0, 1000, [child])',
+ ]);
+
+ await act(async () => {
+ startTransition(
+ () => root.render(),
+ {
+ name: 'transition two',
+ },
+ );
+
+ ReactNoop.expire(1000);
+ await advanceTimers(1000);
+ });
+
+ expect(Scheduler).toHaveYielded([
+ 'Suspend [Appended child]',
+ 'Suspend [Child]',
+ 'onTransitionStart(transition two, 1000)',
+ 'onMarkerProgress(transition two, appended child, 1000, 2000, [appended child])',
+ 'onTransitionProgress(transition two, 1000, 2000, [appended child])',
+ ]);
+
+ await act(async () => {
+ root.render();
+ ReactNoop.expire(1000);
+ await advanceTimers(1000);
+ });
+
+ expect(Scheduler).toHaveYielded([
+ 'Suspend [Child]',
+ 'onMarkerProgress(transition two, appended child, 1000, 3000, [])',
+ 'onMarkerIncomplete(transition two, appended child, 1000, [{endTime: 3000, name: appended child, type: suspense}])',
+ ]);
+
+ await act(async () => {
+ resolveText('Child');
+ ReactNoop.expire(1000);
+ await advanceTimers(1000);
+ });
+
+ expect(Scheduler).toHaveYielded([
+ 'Child',
+ 'onMarkerProgress(transition one, parent, 0, 4000, [])',
+ 'onMarkerComplete(transition one, parent, 0, 4000)',
+ 'onTransitionProgress(transition one, 0, 4000, [])',
+ 'onTransitionComplete(transition one, 0, 4000)',
+ ]);
+ });
+
// @gate enableTransitionTracing
it('warns when marker name changes', async () => {
const transitionCallbacks = {