diff --git a/packages/react-reconciler/src/ReactFiberCommitWork.new.js b/packages/react-reconciler/src/ReactFiberCommitWork.new.js index 30709cffa5061..ef5ee28c07216 100644 --- a/packages/react-reconciler/src/ReactFiberCommitWork.new.js +++ b/packages/react-reconciler/src/ReactFiberCommitWork.new.js @@ -137,6 +137,7 @@ import { enqueuePendingPassiveProfilerEffect, restorePendingUpdaters, addTransitionStartCallbackToPendingTransition, + addTransitionProgressCallbackToPendingTransition, addTransitionCompleteCallbackToPendingTransition, addMarkerCompleteCallbackToPendingTransition, setIsRunningInsertionEffect, @@ -1093,6 +1094,7 @@ function commitTransitionProgress(offscreenFiber: Fiber) { const isHidden = nextState !== null; const pendingMarkers = offscreenInstance.pendingMarkers; + const transitions = offscreenInstance.transitions; // If there is a name on the suspense boundary, store that in // the pending boundaries. let name = null; @@ -1110,9 +1112,18 @@ function commitTransitionProgress(offscreenFiber: Fiber) { // to the pending boundary set if it's there if (pendingMarkers !== null) { pendingMarkers.forEach(pendingBoundaries => { - pendingBoundaries.set(offscreenInstance, { - name, - }); + if (!pendingBoundaries.has(offscreenInstance)) { + pendingBoundaries.set(offscreenInstance, { + name, + }); + + transitions.forEach(transition => { + addTransitionProgressCallbackToPendingTransition({ + transition, + pending: pendingBoundaries, + }); + }); + } }); } } else if (wasHidden && !isHidden) { @@ -1123,6 +1134,13 @@ function commitTransitionProgress(offscreenFiber: Fiber) { pendingMarkers.forEach(pendingBoundaries => { if (pendingBoundaries.has(offscreenInstance)) { pendingBoundaries.delete(offscreenInstance); + + transitions.forEach(transition => { + addTransitionProgressCallbackToPendingTransition({ + transition, + pending: pendingBoundaries, + }); + }); } }); } diff --git a/packages/react-reconciler/src/ReactFiberTracingMarkerComponent.new.js b/packages/react-reconciler/src/ReactFiberTracingMarkerComponent.new.js index 0eb87bc3d6dbd..34496532abc96 100644 --- a/packages/react-reconciler/src/ReactFiberTracingMarkerComponent.new.js +++ b/packages/react-reconciler/src/ReactFiberTracingMarkerComponent.new.js @@ -22,9 +22,15 @@ export type TransitionObject = { startTime: number, }; +export type TransitionProgressObject = { + transition: TransitionObject, + pending: PendingSuspenseBoundaries, +}; + export type MarkerTransitionObject = TransitionObject & {markerName: string}; export type PendingTransitionCallbacks = { transitionStart: Array | null, + transitionProgress: Array | null, transitionComplete: Array | null, markerComplete: Array | null, }; @@ -80,6 +86,25 @@ export function processTransitionCallbacks( }); } + const transitionProgress = pendingTransitions.transitionProgress; + if (transitionProgress !== null) { + const logged = new Set(); + transitionProgress.forEach(({transition, pending}) => { + if ( + callbacks.onTransitionProgress != null && + !logged.has(transition) + ) { + callbacks.onTransitionProgress( + transition.name, + transition.startTime, + endTime, + Array.from(pending.values()), + ); + logged.add(transition); + } + }); + } + const transitionComplete = pendingTransitions.transitionComplete; if (transitionComplete !== null) { transitionComplete.forEach(transition => { diff --git a/packages/react-reconciler/src/ReactFiberWorkLoop.new.js b/packages/react-reconciler/src/ReactFiberWorkLoop.new.js index de03d9f1daa28..53737d2bd4c3b 100644 --- a/packages/react-reconciler/src/ReactFiberWorkLoop.new.js +++ b/packages/react-reconciler/src/ReactFiberWorkLoop.new.js @@ -338,6 +338,7 @@ export function addTransitionStartCallbackToPendingTransition( if (currentPendingTransitionCallbacks === null) { currentPendingTransitionCallbacks = { transitionStart: [], + transitionProgress: null, transitionComplete: null, markerComplete: null, }; @@ -358,6 +359,7 @@ export function addMarkerCompleteCallbackToPendingTransition( if (currentPendingTransitionCallbacks === null) { currentPendingTransitionCallbacks = { transitionStart: null, + transitionProgress: null, transitionComplete: null, markerComplete: [], }; @@ -371,6 +373,27 @@ export function addMarkerCompleteCallbackToPendingTransition( } } +export function addTransitionProgressCallbackToPendingTransition( + transition: TransitionProgressObject, +) { + if (enableTransitionTracing) { + if (currentPendingTransitionCallbacks === null) { + currentPendingTransitionCallbacks = { + transitionStart: null, + transitionProgress: [], + transitionComplete: null, + markerComplete: null, + }; + } + + if (currentPendingTransitionCallbacks.transitionProgress === null) { + currentPendingTransitionCallbacks.transitionProgress = []; + } + + currentPendingTransitionCallbacks.transitionProgress.push(transition); + } +} + export function addTransitionCompleteCallbackToPendingTransition( transition: TransitionObject, ) { @@ -378,6 +401,7 @@ export function addTransitionCompleteCallbackToPendingTransition( if (currentPendingTransitionCallbacks === null) { currentPendingTransitionCallbacks = { transitionStart: null, + transitionProgress: null, transitionComplete: [], markerComplete: null, }; diff --git a/packages/react-reconciler/src/__tests__/ReactTransitionTracing-test.js b/packages/react-reconciler/src/__tests__/ReactTransitionTracing-test.js index 1926f47fb64d3..21f6bb3e55d08 100644 --- a/packages/react-reconciler/src/__tests__/ReactTransitionTracing-test.js +++ b/packages/react-reconciler/src/__tests__/ReactTransitionTracing-test.js @@ -168,6 +168,12 @@ describe('ReactInteractionTracing', () => { `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})`, @@ -283,6 +289,12 @@ describe('ReactInteractionTracing', () => { `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})`, @@ -301,7 +313,7 @@ describe('ReactInteractionTracing', () => { {navigate ? ( } - name="suspense page"> + unstable_name="suspense page"> ) : ( @@ -330,6 +342,7 @@ describe('ReactInteractionTracing', () => { 'Suspend [Page Two]', 'Loading...', 'onTransitionStart(page transition, 1000)', + 'onTransitionProgress(page transition, 1000, 2000, [suspense page])', ]); ReactNoop.expire(1000); @@ -338,6 +351,7 @@ describe('ReactInteractionTracing', () => { expect(Scheduler).toFlushAndYield([ 'Page Two', + 'onTransitionProgress(page transition, 1000, 3000, [])', 'onTransitionComplete(page transition, 1000, 3000)', ]); }); @@ -351,6 +365,12 @@ describe('ReactInteractionTracing', () => { `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})`, @@ -377,13 +397,15 @@ describe('ReactInteractionTracing', () => { {navigate ? ( <> {showText ? ( - }> + }> ) : null} } - name="suspense page"> + unstable_name="suspense page"> @@ -410,6 +432,7 @@ describe('ReactInteractionTracing', () => { 'Suspend [Page Two]', 'Loading...', 'onTransitionStart(page transition, 1000)', + 'onTransitionProgress(page transition, 1000, 1000, [suspense page])', ]); await resolveText('Page Two'); @@ -417,6 +440,7 @@ describe('ReactInteractionTracing', () => { await advanceTimers(1000); expect(Scheduler).toFlushAndYield([ 'Page Two', + 'onTransitionProgress(page transition, 1000, 2000, [])', 'onTransitionComplete(page transition, 1000, 2000)', ]); @@ -426,6 +450,7 @@ describe('ReactInteractionTracing', () => { 'Show Text Loading...', 'Page Two', 'onTransitionStart(text transition, 2000)', + 'onTransitionProgress(text transition, 2000, 2000, [show text])', ]); await resolveText('Show Text'); @@ -433,6 +458,7 @@ describe('ReactInteractionTracing', () => { await advanceTimers(1000); expect(Scheduler).toFlushAndYield([ 'Show Text', + 'onTransitionProgress(text transition, 2000, 3000, [])', 'onTransitionComplete(text transition, 2000, 3000)', ]); }); @@ -446,6 +472,12 @@ describe('ReactInteractionTracing', () => { `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})`, @@ -470,13 +502,15 @@ describe('ReactInteractionTracing', () => { {navigate ? ( <> {showText ? ( - }> + }> ) : null} } - name="suspense page"> + unstable_name="suspense page"> @@ -505,6 +539,7 @@ describe('ReactInteractionTracing', () => { 'Suspend [Page Two]', 'Loading...', 'onTransitionStart(page transition, 1000)', + 'onTransitionProgress(page transition, 1000, 2000, [suspense page])', ]); }); @@ -517,6 +552,7 @@ describe('ReactInteractionTracing', () => { 'Suspend [Page Two]', 'Loading...', 'onTransitionStart(show text, 2000)', + 'onTransitionProgress(show text, 2000, 2000, [show text])', ]); }); @@ -527,6 +563,7 @@ describe('ReactInteractionTracing', () => { expect(Scheduler).toFlushAndYield([ 'Page Two', + 'onTransitionProgress(page transition, 1000, 3000, [])', 'onTransitionComplete(page transition, 1000, 3000)', ]); @@ -536,11 +573,290 @@ describe('ReactInteractionTracing', () => { expect(Scheduler).toFlushAndYield([ 'Show Text', + 'onTransitionProgress(show text, 2000, 4000, [])', 'onTransitionComplete(show text, 2000, 4000)', ]); }); }); + it('trace interaction with nested and sibling suspense boundaries', 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})`, + ); + }, + }; + + let navigateToPageTwo; + function App() { + const [navigate, setNavigate] = useState(false); + navigateToPageTwo = () => { + setNavigate(true); + }; + + return ( +
+ {navigate ? ( + <> + } + unstable_name="suspense page"> + + }> + + +
+ }> + + +
+
+ + ) : ( + + )} +
+ ); + } + + const root = ReactNoop.createRoot({transitionCallbacks}); + await act(async () => { + root.render(); + ReactNoop.expire(1000); + await advanceTimers(1000); + + expect(Scheduler).toFlushAndYield(['Page One']); + }); + + await act(async () => { + startTransition(() => navigateToPageTwo(), {name: 'page transition'}); + ReactNoop.expire(1000); + await advanceTimers(1000); + + expect(Scheduler).toFlushAndYield([ + 'Suspend [Page Two]', + 'Suspend [Show Text One]', + 'Show Text One Loading...', + 'Suspend [Show Text Two]', + 'Show Text Two Loading...', + 'Loading...', + 'onTransitionStart(page transition, 1000)', + 'onTransitionProgress(page transition, 1000, 2000, [suspense page])', + ]); + + resolveText('Page Two'); + ReactNoop.expire(1000); + await advanceTimers(1000); + + expect(Scheduler).toFlushAndYield([ + 'Page Two', + 'Suspend [Show Text One]', + 'Show Text One Loading...', + 'Suspend [Show Text Two]', + 'Show Text Two Loading...', + 'onTransitionProgress(page transition, 1000, 3000, [show text one, show text two])', + ]); + + resolveText('Show Text One'); + ReactNoop.expire(1000); + await advanceTimers(1000); + + expect(Scheduler).toFlushAndYield([ + 'Show Text One', + 'onTransitionProgress(page transition, 1000, 4000, [show text two])', + ]); + + resolveText('Show Text Two'); + ReactNoop.expire(1000); + await advanceTimers(1000); + + expect(Scheduler).toFlushAndYield([ + 'Show Text Two', + 'onTransitionProgress(page transition, 1000, 5000, [])', + 'onTransitionComplete(page transition, 1000, 5000)', + ]); + }); + }); + + it('trace interactions with the same child suspense boundaries', 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})`, + ); + }, + }; + + let setNavigate; + let setShowTextOne; + let setShowTextTwo; + function App() { + const [navigate, _setNavigate] = useState(false); + const [showTextOne, _setShowTextOne] = useState(false); + const [showTextTwo, _setShowTextTwo] = useState(false); + + setNavigate = () => _setNavigate(true); + setShowTextOne = () => _setShowTextOne(true); + setShowTextTwo = () => _setShowTextTwo(true); + + return ( +
+ {navigate ? ( + <> + } + unstable_name="suspense page"> + + {/* showTextOne is entangled with navigate */} + {showTextOne ? ( + }> + + + ) : null} + }> + + + {/* showTextTwo's suspense boundaries shouldn't stop navigate's suspense boundaries + from completing */} + {showTextTwo ? ( + }> + + + ) : null} + + + ) : ( + + )} +
+ ); + } + + const root = ReactNoop.createRoot({transitionCallbacks}); + await act(async () => { + root.render(); + ReactNoop.expire(1000); + await advanceTimers(1000); + + expect(Scheduler).toFlushAndYield(['Page One']); + }); + + await act(async () => { + startTransition(() => setNavigate(), {name: 'navigate'}); + startTransition(() => setShowTextOne(), {name: 'show text one'}); + ReactNoop.expire(1000); + await advanceTimers(1000); + + expect(Scheduler).toFlushAndYield([ + 'Suspend [Page Two]', + 'Suspend [Show Text One]', + 'Show Text One Loading...', + 'Suspend [Show Text]', + 'Show Text Loading...', + 'Loading...', + 'onTransitionStart(navigate, 1000)', + 'onTransitionStart(show text one, 1000)', + 'onTransitionProgress(navigate, 1000, 2000, [suspense page])', + 'onTransitionProgress(show text one, 1000, 2000, [suspense page])', + ]); + + resolveText('Page Two'); + ReactNoop.expire(1000); + await advanceTimers(1000); + expect(Scheduler).toFlushAndYield([ + 'Page Two', + 'Suspend [Show Text One]', + 'Show Text One Loading...', + 'Suspend [Show Text]', + 'Show Text Loading...', + 'onTransitionProgress(navigate, 1000, 3000, [show text one, ])', + 'onTransitionProgress(show text one, 1000, 3000, [show text one, ])', + ]); + + startTransition(() => setShowTextTwo(), {name: 'show text two'}); + ReactNoop.expire(1000); + await advanceTimers(1000); + + expect(Scheduler).toFlushAndYield([ + 'Page Two', + 'Suspend [Show Text One]', + 'Show Text One Loading...', + 'Suspend [Show Text]', + 'Show Text Loading...', + 'Suspend [Show Text Two]', + 'Show Text Two Loading...', + 'onTransitionStart(show text two, 3000)', + 'onTransitionProgress(show text two, 3000, 4000, [show text two])', + ]); + + // This should not cause navigate to finish because it's entangled with + // show text one + resolveText('Show Text'); + ReactNoop.expire(1000); + await advanceTimers(1000); + + expect(Scheduler).toFlushAndYield([ + 'Show Text', + 'onTransitionProgress(navigate, 1000, 5000, [show text one])', + 'onTransitionProgress(show text one, 1000, 5000, [show text one])', + ]); + + // This should not cause show text two to finish but nothing else + resolveText('Show Text Two'); + ReactNoop.expire(1000); + await advanceTimers(1000); + expect(Scheduler).toFlushAndYield([ + 'Show Text Two', + 'onTransitionProgress(show text two, 3000, 6000, [])', + 'onTransitionComplete(show text two, 3000, 6000)', + ]); + + // This should cause everything to finish + resolveText('Show Text One'); + ReactNoop.expire(1000); + await advanceTimers(1000); + + expect(Scheduler).toFlushAndYield([ + 'Show Text One', + 'onTransitionProgress(navigate, 1000, 7000, [])', + 'onTransitionProgress(show text one, 1000, 7000, [])', + 'onTransitionComplete(navigate, 1000, 7000)', + 'onTransitionComplete(show text one, 1000, 7000)', + ]); + }); + }); + // @gate enableTransitionTracing it('should correctly trace interactions for tracing markers complete', async () => { const transitionCallbacks = {