From 11ed7010c6d34c40342e8b829a48fec138215ae0 Mon Sep 17 00:00:00 2001 From: Luna Ruan Date: Thu, 25 Aug 2022 19:03:03 +0100 Subject: [PATCH] [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` --- .../react-reconciler/src/ReactFiber.new.js | 2 + .../react-reconciler/src/ReactFiber.old.js | 2 + .../src/ReactFiberBeginWork.new.js | 10 +- .../src/ReactFiberBeginWork.old.js | 10 +- .../src/ReactFiberCommitWork.new.js | 283 +++++- .../src/ReactFiberCommitWork.old.js | 283 +++++- .../src/ReactFiberCompleteWork.new.js | 10 - .../src/ReactFiberCompleteWork.old.js | 10 - .../ReactFiberTracingMarkerComponent.new.js | 73 +- .../ReactFiberTracingMarkerComponent.old.js | 73 +- .../src/ReactFiberWorkLoop.new.js | 36 +- .../src/ReactFiberWorkLoop.old.js | 36 +- .../src/ReactInternalTypes.js | 8 +- .../__tests__/ReactTransitionTracing-test.js | 841 +++++++++++++++++- 14 files changed, 1571 insertions(+), 106 deletions(-) diff --git a/packages/react-reconciler/src/ReactFiber.new.js b/packages/react-reconciler/src/ReactFiber.new.js index fc2d03109fd2b..d6a6c8f7adc19 100644 --- a/packages/react-reconciler/src/ReactFiber.new.js +++ b/packages/react-reconciler/src/ReactFiber.new.js @@ -774,6 +774,8 @@ export function createFiberFromTracingMarker( tag: TransitionTracingMarker, transitions: null, pendingBoundaries: null, + aborts: null, + name: pendingProps.name, }; fiber.stateNode = tracingMarkerInstance; return fiber; diff --git a/packages/react-reconciler/src/ReactFiber.old.js b/packages/react-reconciler/src/ReactFiber.old.js index 572c1b770adaa..d8f1ef424bd13 100644 --- a/packages/react-reconciler/src/ReactFiber.old.js +++ b/packages/react-reconciler/src/ReactFiber.old.js @@ -774,6 +774,8 @@ export function createFiberFromTracingMarker( tag: TransitionTracingMarker, transitions: null, pendingBoundaries: null, + aborts: null, + name: pendingProps.name, }; fiber.stateNode = tracingMarkerInstance; return fiber; diff --git a/packages/react-reconciler/src/ReactFiberBeginWork.new.js b/packages/react-reconciler/src/ReactFiberBeginWork.new.js index 61edb46d32f2a..fa417e08114c9 100644 --- a/packages/react-reconciler/src/ReactFiberBeginWork.new.js +++ b/packages/react-reconciler/src/ReactFiberBeginWork.new.js @@ -89,6 +89,7 @@ import { StaticMask, ShouldCapture, ForceClientRender, + Passive, } from './ReactFiberFlags'; import ReactSharedInternals from 'shared/ReactSharedInternals'; import { @@ -979,10 +980,17 @@ function updateTracingMarkerComponent( const markerInstance: TracingMarkerInstance = { tag: TransitionTracingMarker, transitions: new Set(currentTransitions), - pendingBoundaries: new Map(), + pendingBoundaries: null, name: workInProgress.pendingProps.name, + aborts: null, }; workInProgress.stateNode = markerInstance; + + // We call the marker complete callback when all child suspense boundaries resolve. + // We do this in the commit phase on Offscreen. If the marker has no child suspense + // boundaries, we need to schedule a passive effect to make sure we call the marker + // complete callback. + workInProgress.flags |= Passive; } } else { if (__DEV__) { diff --git a/packages/react-reconciler/src/ReactFiberBeginWork.old.js b/packages/react-reconciler/src/ReactFiberBeginWork.old.js index 65403e6c8ad37..449f306e7ef89 100644 --- a/packages/react-reconciler/src/ReactFiberBeginWork.old.js +++ b/packages/react-reconciler/src/ReactFiberBeginWork.old.js @@ -89,6 +89,7 @@ import { StaticMask, ShouldCapture, ForceClientRender, + Passive, } from './ReactFiberFlags'; import ReactSharedInternals from 'shared/ReactSharedInternals'; import { @@ -979,10 +980,17 @@ function updateTracingMarkerComponent( const markerInstance: TracingMarkerInstance = { tag: TransitionTracingMarker, transitions: new Set(currentTransitions), - pendingBoundaries: new Map(), + pendingBoundaries: null, name: workInProgress.pendingProps.name, + aborts: null, }; workInProgress.stateNode = markerInstance; + + // We call the marker complete callback when all child suspense boundaries resolve. + // We do this in the commit phase on Offscreen. If the marker has no child suspense + // boundaries, we need to schedule a passive effect to make sure we call the marker + // complete callback. + workInProgress.flags |= Passive; } } else { if (__DEV__) { diff --git a/packages/react-reconciler/src/ReactFiberCommitWork.new.js b/packages/react-reconciler/src/ReactFiberCommitWork.new.js index 5d944a4d88d49..d2cd313a05ac8 100644 --- a/packages/react-reconciler/src/ReactFiberCommitWork.new.js +++ b/packages/react-reconciler/src/ReactFiberCommitWork.new.js @@ -30,7 +30,11 @@ import type { import type {HookFlags} from './ReactHookEffectTags'; import type {Cache} from './ReactFiberCacheComponent.new'; import type {RootState} from './ReactFiberRoot.new'; -import type {Transition} from './ReactFiberTracingMarkerComponent.new'; +import type { + Transition, + TracingMarkerInstance, + TransitionAbort, +} from './ReactFiberTracingMarkerComponent.new'; import { enableCreateEventHandleAPI, @@ -145,6 +149,7 @@ import { addTransitionProgressCallbackToPendingTransition, addTransitionCompleteCallbackToPendingTransition, addMarkerProgressCallbackToPendingTransition, + addMarkerIncompleteCallbackToPendingTransition, addMarkerCompleteCallbackToPendingTransition, setIsRunningInsertionEffect, getExecutionContext, @@ -1130,6 +1135,141 @@ function commitLayoutEffectOnFiber( } } +function abortRootTransitions( + root: FiberRoot, + abort: TransitionAbort, + deletedTransitions: Set, + deletedOffscreenInstance: OffscreenInstance | null, + isInDeletedTree: boolean, +) { + if (enableTransitionTracing) { + const rootTransitions = root.incompleteTransitions; + deletedTransitions.forEach(transition => { + if (rootTransitions.has(transition)) { + const transitionInstance: TracingMarkerInstance = (rootTransitions.get( + transition, + ): any); + if (transitionInstance.aborts === null) { + transitionInstance.aborts = []; + } + transitionInstance.aborts.push(abort); + + if (deletedOffscreenInstance !== null) { + if ( + transitionInstance.pendingBoundaries !== null && + transitionInstance.pendingBoundaries.has(deletedOffscreenInstance) + ) { + transitionInstance.pendingBoundaries.delete( + deletedOffscreenInstance, + ); + } + } + } + }); + } +} + +function abortTracingMarkerTransitions( + abortedFiber: Fiber, + abort: TransitionAbort, + deletedTransitions: Set, + deletedOffscreenInstance: OffscreenInstance | null, + isInDeletedTree: boolean, +) { + if (enableTransitionTracing) { + const markerInstance: TracingMarkerInstance = abortedFiber.stateNode; + const markerTransitions = markerInstance.transitions; + const pendingBoundaries = markerInstance.pendingBoundaries; + if (markerTransitions !== null) { + // TODO: Refactor this code. Is there a way to move this code to + // the deletions phase instead of calculating it here while making sure + // complete is called appropriately? + deletedTransitions.forEach(transition => { + // If one of the transitions on the tracing marker is a transition + // that was in an aborted subtree, we will abort that tracing marker + if ( + abortedFiber !== null && + markerTransitions.has(transition) && + (markerInstance.aborts === null || + !markerInstance.aborts.includes(abort)) + ) { + if (markerInstance.transitions !== null) { + if (markerInstance.aborts === null) { + markerInstance.aborts = [abort]; + addMarkerIncompleteCallbackToPendingTransition( + abortedFiber.memoizedProps.name, + markerInstance.transitions, + markerInstance.aborts, + ); + } else { + markerInstance.aborts.push(abort); + } + + // We only want to call onTransitionProgress when the marker hasn't been + // deleted + if ( + deletedOffscreenInstance !== null && + !isInDeletedTree && + pendingBoundaries !== null && + pendingBoundaries.has(deletedOffscreenInstance) + ) { + pendingBoundaries.delete(deletedOffscreenInstance); + + addMarkerProgressCallbackToPendingTransition( + abortedFiber.memoizedProps.name, + deletedTransitions, + pendingBoundaries, + ); + } + } + } + }); + } + } +} + +function abortParentMarkerTransitionsForDeletedFiber( + abortedFiber: Fiber, + abort: TransitionAbort, + deletedTransitions: Set, + deletedOffscreenInstance: OffscreenInstance | null, + isInDeletedTree: boolean, +) { + if (enableTransitionTracing) { + // Find all pending markers that are waiting on child suspense boundaries in the + // aborted subtree and cancels them + let fiber = abortedFiber; + while (fiber !== null) { + switch (fiber.tag) { + case TracingMarkerComponent: + abortTracingMarkerTransitions( + fiber, + abort, + deletedTransitions, + deletedOffscreenInstance, + isInDeletedTree, + ); + break; + case HostRoot: + const root = fiber.stateNode; + abortRootTransitions( + root, + abort, + deletedTransitions, + deletedOffscreenInstance, + isInDeletedTree, + ); + + break; + default: + break; + } + + fiber = fiber.return; + } + } +} + function commitTransitionProgress(offscreenFiber: Fiber) { if (enableTransitionTracing) { // This function adds suspense boundaries to the root @@ -1175,6 +1315,7 @@ function commitTransitionProgress(offscreenFiber: Fiber) { pendingMarkers.forEach(markerInstance => { const pendingBoundaries = markerInstance.pendingBoundaries; const transitions = markerInstance.transitions; + const markerName = markerInstance.name; if ( pendingBoundaries !== null && !pendingBoundaries.has(offscreenInstance) @@ -1185,10 +1326,10 @@ function commitTransitionProgress(offscreenFiber: Fiber) { if (transitions !== null) { if ( markerInstance.tag === TransitionTracingMarker && - markerInstance.name !== undefined + markerName !== null ) { addMarkerProgressCallbackToPendingTransition( - markerInstance.name, + markerName, transitions, pendingBoundaries, ); @@ -1212,6 +1353,7 @@ function commitTransitionProgress(offscreenFiber: Fiber) { pendingMarkers.forEach(markerInstance => { const pendingBoundaries = markerInstance.pendingBoundaries; const transitions = markerInstance.transitions; + const markerName = markerInstance.name; if ( pendingBoundaries !== null && pendingBoundaries.has(offscreenInstance) @@ -1220,13 +1362,27 @@ function commitTransitionProgress(offscreenFiber: Fiber) { if (transitions !== null) { if ( markerInstance.tag === TransitionTracingMarker && - markerInstance.name !== undefined + markerName !== null ) { addMarkerProgressCallbackToPendingTransition( - markerInstance.name, + markerName, transitions, pendingBoundaries, ); + + // If there are no more unresolved suspense boundaries, the interaction + // is considered finished + if (pendingBoundaries.size === 0) { + if (markerInstance.aborts === null) { + addMarkerCompleteCallbackToPendingTransition( + markerName, + transitions, + ); + } + markerInstance.transitions = null; + markerInstance.pendingBoundaries = null; + markerInstance.aborts = null; + } } else if (markerInstance.tag === TransitionRoot) { transitions.forEach(transition => { addTransitionProgressCallbackToPendingTransition( @@ -1737,6 +1893,7 @@ function commitDeletionEffects( 'a bug in React. Please file an issue.', ); } + commitDeletionEffectsOnFiber(root, returnFiber, deletedFiber); hostParent = null; hostParentIsContainer = false; @@ -1979,6 +2136,7 @@ function commitDeletionEffectsOnFiber( const prevOffscreenSubtreeWasHidden = offscreenSubtreeWasHidden; offscreenSubtreeWasHidden = prevOffscreenSubtreeWasHidden || deletedFiber.memoizedState !== null; + recursivelyTraverseDeletionEffects( finishedRoot, nearestMountedAncestor, @@ -2957,6 +3115,12 @@ function commitOffscreenPassiveMountEffects( } commitTransitionProgress(finishedWork); + + // TODO: Refactor this into an if/else branch + if (!isHidden) { + instance.transitions = null; + instance.pendingMarkers = null; + } } } @@ -2987,20 +3151,18 @@ function commitCachePassiveMountEffect( function commitTracingMarkerPassiveMountEffect(finishedWork: Fiber) { // Get the transitions that were initiatized during the render // and add a start transition callback for each of them + // We will only call this on initial mount of the tracing marker + // only if there are no suspense children const instance = finishedWork.stateNode; - if ( - instance.transitions !== null && - (instance.pendingBoundaries === null || - instance.pendingBoundaries.size === 0) - ) { - instance.transitions.forEach(transition => { - addMarkerCompleteCallbackToPendingTransition( - finishedWork.memoizedProps.name, - instance.transitions, - ); - }); + if (instance.transitions !== null && instance.pendingBoundaries === null) { + addMarkerCompleteCallbackToPendingTransition( + finishedWork.memoizedProps.name, + instance.transitions, + ); instance.transitions = null; instance.pendingBoundaries = null; + instance.aborts = null; + instance.name = null; } } @@ -3102,7 +3264,7 @@ function commitPassiveMountOnFiber( if (enableTransitionTracing) { // Get the transitions that were initiatized during the render // and add a start transition callback for each of them - const root = finishedWork.stateNode; + const root: FiberRoot = finishedWork.stateNode; const incompleteTransitions = root.incompleteTransitions; // Initial render if (committedTransitions !== null) { @@ -3116,7 +3278,9 @@ function commitPassiveMountOnFiber( incompleteTransitions.forEach((markerInstance, transition) => { const pendingBoundaries = markerInstance.pendingBoundaries; if (pendingBoundaries === null || pendingBoundaries.size === 0) { - addTransitionCompleteCallbackToPendingTransition(transition); + if (markerInstance.aborts === null) { + addTransitionCompleteCallbackToPendingTransition(transition); + } incompleteTransitions.delete(transition); } }); @@ -3489,21 +3653,6 @@ function commitAtomicPassiveEffects( } break; } - case TracingMarkerComponent: { - if (enableTransitionTracing) { - recursivelyTraverseAtomicPassiveEffects( - finishedRoot, - finishedWork, - committedLanes, - committedTransitions, - ); - if (flags & Passive) { - commitTracingMarkerPassiveMountEffect(finishedWork); - } - break; - } - // Intentional fallthrough to next branch - } // eslint-disable-next-line-no-fallthrough default: { recursivelyTraverseAtomicPassiveEffects( @@ -3827,6 +3976,43 @@ function commitPassiveUnmountInsideDeletedTreeOnFiber( } break; } + case SuspenseComponent: { + if (enableTransitionTracing) { + // We need to mark this fiber's parents as deleted + const offscreenFiber: Fiber = (current.child: any); + const instance: OffscreenInstance = offscreenFiber.stateNode; + const transitions = instance.transitions; + if (transitions !== null) { + const abortReason = { + reason: 'suspense', + name: current.memoizedProps.unstable_name || null, + }; + if ( + current.memoizedState === null || + current.memoizedState.dehydrated === null + ) { + abortParentMarkerTransitionsForDeletedFiber( + offscreenFiber, + abortReason, + transitions, + instance, + true, + ); + + if (nearestMountedAncestor !== null) { + abortParentMarkerTransitionsForDeletedFiber( + nearestMountedAncestor, + abortReason, + transitions, + instance, + false, + ); + } + } + } + } + break; + } case CacheComponent: { if (enableCache) { const cache = current.memoizedState.cache; @@ -3834,6 +4020,37 @@ function commitPassiveUnmountInsideDeletedTreeOnFiber( } break; } + case TracingMarkerComponent: { + if (enableTransitionTracing) { + // We need to mark this fiber's parents as deleted + const instance: TracingMarkerInstance = current.stateNode; + const transitions = instance.transitions; + if (transitions !== null) { + const abortReason = { + reason: 'marker', + name: current.memoizedProps.name, + }; + abortParentMarkerTransitionsForDeletedFiber( + current, + abortReason, + transitions, + null, + true, + ); + + if (nearestMountedAncestor !== null) { + abortParentMarkerTransitionsForDeletedFiber( + nearestMountedAncestor, + abortReason, + transitions, + null, + false, + ); + } + } + } + break; + } } } diff --git a/packages/react-reconciler/src/ReactFiberCommitWork.old.js b/packages/react-reconciler/src/ReactFiberCommitWork.old.js index 8afa97a5ab7b6..70a68617763c3 100644 --- a/packages/react-reconciler/src/ReactFiberCommitWork.old.js +++ b/packages/react-reconciler/src/ReactFiberCommitWork.old.js @@ -30,7 +30,11 @@ import type { import type {HookFlags} from './ReactHookEffectTags'; import type {Cache} from './ReactFiberCacheComponent.old'; import type {RootState} from './ReactFiberRoot.old'; -import type {Transition} from './ReactFiberTracingMarkerComponent.old'; +import type { + Transition, + TracingMarkerInstance, + TransitionAbort, +} from './ReactFiberTracingMarkerComponent.old'; import { enableCreateEventHandleAPI, @@ -145,6 +149,7 @@ import { addTransitionProgressCallbackToPendingTransition, addTransitionCompleteCallbackToPendingTransition, addMarkerProgressCallbackToPendingTransition, + addMarkerIncompleteCallbackToPendingTransition, addMarkerCompleteCallbackToPendingTransition, setIsRunningInsertionEffect, getExecutionContext, @@ -1130,6 +1135,141 @@ function commitLayoutEffectOnFiber( } } +function abortRootTransitions( + root: FiberRoot, + abort: TransitionAbort, + deletedTransitions: Set, + deletedOffscreenInstance: OffscreenInstance | null, + isInDeletedTree: boolean, +) { + if (enableTransitionTracing) { + const rootTransitions = root.incompleteTransitions; + deletedTransitions.forEach(transition => { + if (rootTransitions.has(transition)) { + const transitionInstance: TracingMarkerInstance = (rootTransitions.get( + transition, + ): any); + if (transitionInstance.aborts === null) { + transitionInstance.aborts = []; + } + transitionInstance.aborts.push(abort); + + if (deletedOffscreenInstance !== null) { + if ( + transitionInstance.pendingBoundaries !== null && + transitionInstance.pendingBoundaries.has(deletedOffscreenInstance) + ) { + transitionInstance.pendingBoundaries.delete( + deletedOffscreenInstance, + ); + } + } + } + }); + } +} + +function abortTracingMarkerTransitions( + abortedFiber: Fiber, + abort: TransitionAbort, + deletedTransitions: Set, + deletedOffscreenInstance: OffscreenInstance | null, + isInDeletedTree: boolean, +) { + if (enableTransitionTracing) { + const markerInstance: TracingMarkerInstance = abortedFiber.stateNode; + const markerTransitions = markerInstance.transitions; + const pendingBoundaries = markerInstance.pendingBoundaries; + if (markerTransitions !== null) { + // TODO: Refactor this code. Is there a way to move this code to + // the deletions phase instead of calculating it here while making sure + // complete is called appropriately? + deletedTransitions.forEach(transition => { + // If one of the transitions on the tracing marker is a transition + // that was in an aborted subtree, we will abort that tracing marker + if ( + abortedFiber !== null && + markerTransitions.has(transition) && + (markerInstance.aborts === null || + !markerInstance.aborts.includes(abort)) + ) { + if (markerInstance.transitions !== null) { + if (markerInstance.aborts === null) { + markerInstance.aborts = [abort]; + addMarkerIncompleteCallbackToPendingTransition( + abortedFiber.memoizedProps.name, + markerInstance.transitions, + markerInstance.aborts, + ); + } else { + markerInstance.aborts.push(abort); + } + + // We only want to call onTransitionProgress when the marker hasn't been + // deleted + if ( + deletedOffscreenInstance !== null && + !isInDeletedTree && + pendingBoundaries !== null && + pendingBoundaries.has(deletedOffscreenInstance) + ) { + pendingBoundaries.delete(deletedOffscreenInstance); + + addMarkerProgressCallbackToPendingTransition( + abortedFiber.memoizedProps.name, + deletedTransitions, + pendingBoundaries, + ); + } + } + } + }); + } + } +} + +function abortParentMarkerTransitionsForDeletedFiber( + abortedFiber: Fiber, + abort: TransitionAbort, + deletedTransitions: Set, + deletedOffscreenInstance: OffscreenInstance | null, + isInDeletedTree: boolean, +) { + if (enableTransitionTracing) { + // Find all pending markers that are waiting on child suspense boundaries in the + // aborted subtree and cancels them + let fiber = abortedFiber; + while (fiber !== null) { + switch (fiber.tag) { + case TracingMarkerComponent: + abortTracingMarkerTransitions( + fiber, + abort, + deletedTransitions, + deletedOffscreenInstance, + isInDeletedTree, + ); + break; + case HostRoot: + const root = fiber.stateNode; + abortRootTransitions( + root, + abort, + deletedTransitions, + deletedOffscreenInstance, + isInDeletedTree, + ); + + break; + default: + break; + } + + fiber = fiber.return; + } + } +} + function commitTransitionProgress(offscreenFiber: Fiber) { if (enableTransitionTracing) { // This function adds suspense boundaries to the root @@ -1175,6 +1315,7 @@ function commitTransitionProgress(offscreenFiber: Fiber) { pendingMarkers.forEach(markerInstance => { const pendingBoundaries = markerInstance.pendingBoundaries; const transitions = markerInstance.transitions; + const markerName = markerInstance.name; if ( pendingBoundaries !== null && !pendingBoundaries.has(offscreenInstance) @@ -1185,10 +1326,10 @@ function commitTransitionProgress(offscreenFiber: Fiber) { if (transitions !== null) { if ( markerInstance.tag === TransitionTracingMarker && - markerInstance.name !== undefined + markerName !== null ) { addMarkerProgressCallbackToPendingTransition( - markerInstance.name, + markerName, transitions, pendingBoundaries, ); @@ -1212,6 +1353,7 @@ function commitTransitionProgress(offscreenFiber: Fiber) { pendingMarkers.forEach(markerInstance => { const pendingBoundaries = markerInstance.pendingBoundaries; const transitions = markerInstance.transitions; + const markerName = markerInstance.name; if ( pendingBoundaries !== null && pendingBoundaries.has(offscreenInstance) @@ -1220,13 +1362,27 @@ function commitTransitionProgress(offscreenFiber: Fiber) { if (transitions !== null) { if ( markerInstance.tag === TransitionTracingMarker && - markerInstance.name !== undefined + markerName !== null ) { addMarkerProgressCallbackToPendingTransition( - markerInstance.name, + markerName, transitions, pendingBoundaries, ); + + // If there are no more unresolved suspense boundaries, the interaction + // is considered finished + if (pendingBoundaries.size === 0) { + if (markerInstance.aborts === null) { + addMarkerCompleteCallbackToPendingTransition( + markerName, + transitions, + ); + } + markerInstance.transitions = null; + markerInstance.pendingBoundaries = null; + markerInstance.aborts = null; + } } else if (markerInstance.tag === TransitionRoot) { transitions.forEach(transition => { addTransitionProgressCallbackToPendingTransition( @@ -1737,6 +1893,7 @@ function commitDeletionEffects( 'a bug in React. Please file an issue.', ); } + commitDeletionEffectsOnFiber(root, returnFiber, deletedFiber); hostParent = null; hostParentIsContainer = false; @@ -1979,6 +2136,7 @@ function commitDeletionEffectsOnFiber( const prevOffscreenSubtreeWasHidden = offscreenSubtreeWasHidden; offscreenSubtreeWasHidden = prevOffscreenSubtreeWasHidden || deletedFiber.memoizedState !== null; + recursivelyTraverseDeletionEffects( finishedRoot, nearestMountedAncestor, @@ -2957,6 +3115,12 @@ function commitOffscreenPassiveMountEffects( } commitTransitionProgress(finishedWork); + + // TODO: Refactor this into an if/else branch + if (!isHidden) { + instance.transitions = null; + instance.pendingMarkers = null; + } } } @@ -2987,20 +3151,18 @@ function commitCachePassiveMountEffect( function commitTracingMarkerPassiveMountEffect(finishedWork: Fiber) { // Get the transitions that were initiatized during the render // and add a start transition callback for each of them + // We will only call this on initial mount of the tracing marker + // only if there are no suspense children const instance = finishedWork.stateNode; - if ( - instance.transitions !== null && - (instance.pendingBoundaries === null || - instance.pendingBoundaries.size === 0) - ) { - instance.transitions.forEach(transition => { - addMarkerCompleteCallbackToPendingTransition( - finishedWork.memoizedProps.name, - instance.transitions, - ); - }); + if (instance.transitions !== null && instance.pendingBoundaries === null) { + addMarkerCompleteCallbackToPendingTransition( + finishedWork.memoizedProps.name, + instance.transitions, + ); instance.transitions = null; instance.pendingBoundaries = null; + instance.aborts = null; + instance.name = null; } } @@ -3102,7 +3264,7 @@ function commitPassiveMountOnFiber( if (enableTransitionTracing) { // Get the transitions that were initiatized during the render // and add a start transition callback for each of them - const root = finishedWork.stateNode; + const root: FiberRoot = finishedWork.stateNode; const incompleteTransitions = root.incompleteTransitions; // Initial render if (committedTransitions !== null) { @@ -3116,7 +3278,9 @@ function commitPassiveMountOnFiber( incompleteTransitions.forEach((markerInstance, transition) => { const pendingBoundaries = markerInstance.pendingBoundaries; if (pendingBoundaries === null || pendingBoundaries.size === 0) { - addTransitionCompleteCallbackToPendingTransition(transition); + if (markerInstance.aborts === null) { + addTransitionCompleteCallbackToPendingTransition(transition); + } incompleteTransitions.delete(transition); } }); @@ -3489,21 +3653,6 @@ function commitAtomicPassiveEffects( } break; } - case TracingMarkerComponent: { - if (enableTransitionTracing) { - recursivelyTraverseAtomicPassiveEffects( - finishedRoot, - finishedWork, - committedLanes, - committedTransitions, - ); - if (flags & Passive) { - commitTracingMarkerPassiveMountEffect(finishedWork); - } - break; - } - // Intentional fallthrough to next branch - } // eslint-disable-next-line-no-fallthrough default: { recursivelyTraverseAtomicPassiveEffects( @@ -3827,6 +3976,43 @@ function commitPassiveUnmountInsideDeletedTreeOnFiber( } break; } + case SuspenseComponent: { + if (enableTransitionTracing) { + // We need to mark this fiber's parents as deleted + const offscreenFiber: Fiber = (current.child: any); + const instance: OffscreenInstance = offscreenFiber.stateNode; + const transitions = instance.transitions; + if (transitions !== null) { + const abortReason = { + reason: 'suspense', + name: current.memoizedProps.unstable_name || null, + }; + if ( + current.memoizedState === null || + current.memoizedState.dehydrated === null + ) { + abortParentMarkerTransitionsForDeletedFiber( + offscreenFiber, + abortReason, + transitions, + instance, + true, + ); + + if (nearestMountedAncestor !== null) { + abortParentMarkerTransitionsForDeletedFiber( + nearestMountedAncestor, + abortReason, + transitions, + instance, + false, + ); + } + } + } + } + break; + } case CacheComponent: { if (enableCache) { const cache = current.memoizedState.cache; @@ -3834,6 +4020,37 @@ function commitPassiveUnmountInsideDeletedTreeOnFiber( } break; } + case TracingMarkerComponent: { + if (enableTransitionTracing) { + // We need to mark this fiber's parents as deleted + const instance: TracingMarkerInstance = current.stateNode; + const transitions = instance.transitions; + if (transitions !== null) { + const abortReason = { + reason: 'marker', + name: current.memoizedProps.name, + }; + abortParentMarkerTransitionsForDeletedFiber( + current, + abortReason, + transitions, + null, + true, + ); + + if (nearestMountedAncestor !== null) { + abortParentMarkerTransitionsForDeletedFiber( + nearestMountedAncestor, + abortReason, + transitions, + null, + false, + ); + } + } + } + break; + } } } diff --git a/packages/react-reconciler/src/ReactFiberCompleteWork.new.js b/packages/react-reconciler/src/ReactFiberCompleteWork.new.js index 7a8843941eee5..135e9290f549f 100644 --- a/packages/react-reconciler/src/ReactFiberCompleteWork.new.js +++ b/packages/react-reconciler/src/ReactFiberCompleteWork.new.js @@ -1589,16 +1589,6 @@ function completeWork( popMarkerInstance(workInProgress); } bubbleProperties(workInProgress); - - if ( - current === null || - (workInProgress.subtreeFlags & Visibility) !== NoFlags - ) { - // If any of our suspense children toggle visibility, this means that - // the pending boundaries array needs to be updated, which we only - // do in the passive phase. - workInProgress.flags |= Passive; - } } return null; } diff --git a/packages/react-reconciler/src/ReactFiberCompleteWork.old.js b/packages/react-reconciler/src/ReactFiberCompleteWork.old.js index 1ff1a5173ec10..3fd1bffbc3a99 100644 --- a/packages/react-reconciler/src/ReactFiberCompleteWork.old.js +++ b/packages/react-reconciler/src/ReactFiberCompleteWork.old.js @@ -1589,16 +1589,6 @@ function completeWork( popMarkerInstance(workInProgress); } bubbleProperties(workInProgress); - - if ( - current === null || - (workInProgress.subtreeFlags & Visibility) !== NoFlags - ) { - // If any of our suspense children toggle visibility, this means that - // the pending boundaries array needs to be updated, which we only - // do in the passive phase. - workInProgress.flags |= Passive; - } } return null; } diff --git a/packages/react-reconciler/src/ReactFiberTracingMarkerComponent.new.js b/packages/react-reconciler/src/ReactFiberTracingMarkerComponent.new.js index 31cc4c06841a4..1be1e7f930994 100644 --- a/packages/react-reconciler/src/ReactFiberTracingMarkerComponent.new.js +++ b/packages/react-reconciler/src/ReactFiberTracingMarkerComponent.new.js @@ -7,7 +7,11 @@ * @flow */ -import type {TransitionTracingCallbacks, Fiber} from './ReactInternalTypes'; +import type { + TransitionTracingCallbacks, + Fiber, + FiberRoot, +} from './ReactInternalTypes'; import type {OffscreenInstance} from './ReactFiberOffscreenComponent'; import type {StackCursor} from './ReactFiberStack.new'; @@ -21,7 +25,14 @@ export type PendingTransitionCallbacks = { transitionStart: Array | null, transitionProgress: Map | null, transitionComplete: Array | null, - markerProgress: Map | null, + markerProgress: Map< + string, + {pendingBoundaries: PendingBoundaries, transitions: Set}, + > | null, + markerIncomplete: Map< + string, + {aborts: Array, transitions: Set}, + > | null, markerComplete: Map> | null, }; @@ -36,11 +47,18 @@ export type BatchConfigTransition = { _updatedFibers?: Set, }; +// TODO: Is there a way to not include the tag or name here? export type TracingMarkerInstance = {| tag?: TracingMarkerTag, - pendingBoundaries: PendingBoundaries | null, transitions: Set | null, - name?: string, + pendingBoundaries: PendingBoundaries | null, + aborts: Array | null, + name: string | null, +|}; + +export type TransitionAbort = {| + reason: 'error' | 'unknown' | 'marker' | 'suspense', + name?: string | null, |}; export const TransitionRoot = 0; @@ -69,6 +87,7 @@ export function processTransitionCallbacks( if (onMarkerProgress != null && markerProgress !== null) { markerProgress.forEach((markerInstance, markerName) => { if (markerInstance.transitions !== null) { + // TODO: Clone the suspense object so users can't modify it const pending = markerInstance.pendingBoundaries !== null ? Array.from(markerInstance.pendingBoundaries.values()) @@ -101,6 +120,48 @@ export function processTransitionCallbacks( }); } + const markerIncomplete = pendingTransitions.markerIncomplete; + const onMarkerIncomplete = callbacks.onMarkerIncomplete; + if (onMarkerIncomplete != null && markerIncomplete !== null) { + markerIncomplete.forEach(({transitions, aborts}, markerName) => { + transitions.forEach(transition => { + const filteredAborts = []; + aborts.forEach(abort => { + switch (abort.reason) { + case 'marker': { + filteredAborts.push({ + type: 'marker', + name: abort.name, + endTime, + }); + break; + } + case 'suspense': { + filteredAborts.push({ + type: 'suspense', + name: abort.name, + endTime, + }); + break; + } + default: { + break; + } + } + }); + + if (filteredAborts.length > 0) { + onMarkerIncomplete( + transition.name, + markerName, + transition.startTime, + filteredAborts, + ); + } + }); + }); + } + const transitionProgress = pendingTransitions.transitionProgress; const onTransitionProgress = callbacks.onTransitionProgress; if (onTransitionProgress != null && transitionProgress !== null) { @@ -145,7 +206,7 @@ export function pushRootMarkerInstance(workInProgress: Fiber): void { // transitions map. Each entry in this map functions like a tracing // marker does, so we can push it onto the marker instance stack const transitions = getWorkInProgressTransitions(); - const root = workInProgress.stateNode; + const root: FiberRoot = workInProgress.stateNode; if (transitions !== null) { transitions.forEach(transition => { @@ -154,6 +215,8 @@ export function pushRootMarkerInstance(workInProgress: Fiber): void { tag: TransitionRoot, transitions: new Set([transition]), pendingBoundaries: null, + aborts: null, + name: null, }; root.incompleteTransitions.set(transition, markerInstance); } diff --git a/packages/react-reconciler/src/ReactFiberTracingMarkerComponent.old.js b/packages/react-reconciler/src/ReactFiberTracingMarkerComponent.old.js index 0e3c352d77287..e88930867851a 100644 --- a/packages/react-reconciler/src/ReactFiberTracingMarkerComponent.old.js +++ b/packages/react-reconciler/src/ReactFiberTracingMarkerComponent.old.js @@ -7,7 +7,11 @@ * @flow */ -import type {TransitionTracingCallbacks, Fiber} from './ReactInternalTypes'; +import type { + TransitionTracingCallbacks, + Fiber, + FiberRoot, +} from './ReactInternalTypes'; import type {OffscreenInstance} from './ReactFiberOffscreenComponent'; import type {StackCursor} from './ReactFiberStack.old'; @@ -21,7 +25,14 @@ export type PendingTransitionCallbacks = { transitionStart: Array | null, transitionProgress: Map | null, transitionComplete: Array | null, - markerProgress: Map | null, + markerProgress: Map< + string, + {pendingBoundaries: PendingBoundaries, transitions: Set}, + > | null, + markerIncomplete: Map< + string, + {aborts: Array, transitions: Set}, + > | null, markerComplete: Map> | null, }; @@ -36,11 +47,18 @@ export type BatchConfigTransition = { _updatedFibers?: Set, }; +// TODO: Is there a way to not include the tag or name here? export type TracingMarkerInstance = {| tag?: TracingMarkerTag, - pendingBoundaries: PendingBoundaries | null, transitions: Set | null, - name?: string, + pendingBoundaries: PendingBoundaries | null, + aborts: Array | null, + name: string | null, +|}; + +export type TransitionAbort = {| + reason: 'error' | 'unknown' | 'marker' | 'suspense', + name?: string | null, |}; export const TransitionRoot = 0; @@ -69,6 +87,7 @@ export function processTransitionCallbacks( if (onMarkerProgress != null && markerProgress !== null) { markerProgress.forEach((markerInstance, markerName) => { if (markerInstance.transitions !== null) { + // TODO: Clone the suspense object so users can't modify it const pending = markerInstance.pendingBoundaries !== null ? Array.from(markerInstance.pendingBoundaries.values()) @@ -101,6 +120,48 @@ export function processTransitionCallbacks( }); } + const markerIncomplete = pendingTransitions.markerIncomplete; + const onMarkerIncomplete = callbacks.onMarkerIncomplete; + if (onMarkerIncomplete != null && markerIncomplete !== null) { + markerIncomplete.forEach(({transitions, aborts}, markerName) => { + transitions.forEach(transition => { + const filteredAborts = []; + aborts.forEach(abort => { + switch (abort.reason) { + case 'marker': { + filteredAborts.push({ + type: 'marker', + name: abort.name, + endTime, + }); + break; + } + case 'suspense': { + filteredAborts.push({ + type: 'suspense', + name: abort.name, + endTime, + }); + break; + } + default: { + break; + } + } + }); + + if (filteredAborts.length > 0) { + onMarkerIncomplete( + transition.name, + markerName, + transition.startTime, + filteredAborts, + ); + } + }); + }); + } + const transitionProgress = pendingTransitions.transitionProgress; const onTransitionProgress = callbacks.onTransitionProgress; if (onTransitionProgress != null && transitionProgress !== null) { @@ -145,7 +206,7 @@ export function pushRootMarkerInstance(workInProgress: Fiber): void { // transitions map. Each entry in this map functions like a tracing // marker does, so we can push it onto the marker instance stack const transitions = getWorkInProgressTransitions(); - const root = workInProgress.stateNode; + const root: FiberRoot = workInProgress.stateNode; if (transitions !== null) { transitions.forEach(transition => { @@ -154,6 +215,8 @@ export function pushRootMarkerInstance(workInProgress: Fiber): void { tag: TransitionRoot, transitions: new Set([transition]), pendingBoundaries: null, + aborts: null, + name: null, }; root.incompleteTransitions.set(transition, markerInstance); } diff --git a/packages/react-reconciler/src/ReactFiberWorkLoop.new.js b/packages/react-reconciler/src/ReactFiberWorkLoop.new.js index a1416e6c4cf06..13a673b19aa2a 100644 --- a/packages/react-reconciler/src/ReactFiberWorkLoop.new.js +++ b/packages/react-reconciler/src/ReactFiberWorkLoop.new.js @@ -19,6 +19,7 @@ import type { PendingTransitionCallbacks, PendingBoundaries, Transition, + TransitionAbort, } from './ReactFiberTracingMarkerComponent.new'; import type {OffscreenInstance} from './ReactFiberOffscreenComponent'; @@ -361,6 +362,7 @@ export function addTransitionStartCallbackToPendingTransition( transitionProgress: null, transitionComplete: null, markerProgress: null, + markerIncomplete: null, markerComplete: null, }; } @@ -376,7 +378,7 @@ export function addTransitionStartCallbackToPendingTransition( export function addMarkerProgressCallbackToPendingTransition( markerName: string, transitions: Set, - pendingBoundaries: PendingBoundaries | null, + pendingBoundaries: PendingBoundaries, ) { if (enableTransitionTracing) { if (currentPendingTransitionCallbacks === null) { @@ -385,6 +387,7 @@ export function addMarkerProgressCallbackToPendingTransition( transitionProgress: null, transitionComplete: null, markerProgress: new Map(), + markerIncomplete: null, markerComplete: null, }; } @@ -400,6 +403,34 @@ export function addMarkerProgressCallbackToPendingTransition( } } +export function addMarkerIncompleteCallbackToPendingTransition( + markerName: string, + transitions: Set, + aborts: Array, +) { + if (enableTransitionTracing) { + if (currentPendingTransitionCallbacks === null) { + currentPendingTransitionCallbacks = { + transitionStart: null, + transitionProgress: null, + transitionComplete: null, + markerProgress: null, + markerIncomplete: new Map(), + markerComplete: null, + }; + } + + if (currentPendingTransitionCallbacks.markerIncomplete === null) { + currentPendingTransitionCallbacks.markerIncomplete = new Map(); + } + + currentPendingTransitionCallbacks.markerIncomplete.set(markerName, { + transitions, + aborts, + }); + } +} + export function addMarkerCompleteCallbackToPendingTransition( markerName: string, transitions: Set, @@ -411,6 +442,7 @@ export function addMarkerCompleteCallbackToPendingTransition( transitionProgress: null, transitionComplete: null, markerProgress: null, + markerIncomplete: null, markerComplete: new Map(), }; } @@ -437,6 +469,7 @@ export function addTransitionProgressCallbackToPendingTransition( transitionProgress: new Map(), transitionComplete: null, markerProgress: null, + markerIncomplete: null, markerComplete: null, }; } @@ -462,6 +495,7 @@ export function addTransitionCompleteCallbackToPendingTransition( transitionProgress: null, transitionComplete: [], markerProgress: null, + markerIncomplete: null, markerComplete: null, }; } diff --git a/packages/react-reconciler/src/ReactFiberWorkLoop.old.js b/packages/react-reconciler/src/ReactFiberWorkLoop.old.js index 8343bad2ab488..1d8d868a37a21 100644 --- a/packages/react-reconciler/src/ReactFiberWorkLoop.old.js +++ b/packages/react-reconciler/src/ReactFiberWorkLoop.old.js @@ -19,6 +19,7 @@ import type { PendingTransitionCallbacks, PendingBoundaries, Transition, + TransitionAbort, } from './ReactFiberTracingMarkerComponent.old'; import type {OffscreenInstance} from './ReactFiberOffscreenComponent'; @@ -361,6 +362,7 @@ export function addTransitionStartCallbackToPendingTransition( transitionProgress: null, transitionComplete: null, markerProgress: null, + markerIncomplete: null, markerComplete: null, }; } @@ -376,7 +378,7 @@ export function addTransitionStartCallbackToPendingTransition( export function addMarkerProgressCallbackToPendingTransition( markerName: string, transitions: Set, - pendingBoundaries: PendingBoundaries | null, + pendingBoundaries: PendingBoundaries, ) { if (enableTransitionTracing) { if (currentPendingTransitionCallbacks === null) { @@ -385,6 +387,7 @@ export function addMarkerProgressCallbackToPendingTransition( transitionProgress: null, transitionComplete: null, markerProgress: new Map(), + markerIncomplete: null, markerComplete: null, }; } @@ -400,6 +403,34 @@ export function addMarkerProgressCallbackToPendingTransition( } } +export function addMarkerIncompleteCallbackToPendingTransition( + markerName: string, + transitions: Set, + aborts: Array, +) { + if (enableTransitionTracing) { + if (currentPendingTransitionCallbacks === null) { + currentPendingTransitionCallbacks = { + transitionStart: null, + transitionProgress: null, + transitionComplete: null, + markerProgress: null, + markerIncomplete: new Map(), + markerComplete: null, + }; + } + + if (currentPendingTransitionCallbacks.markerIncomplete === null) { + currentPendingTransitionCallbacks.markerIncomplete = new Map(); + } + + currentPendingTransitionCallbacks.markerIncomplete.set(markerName, { + transitions, + aborts, + }); + } +} + export function addMarkerCompleteCallbackToPendingTransition( markerName: string, transitions: Set, @@ -411,6 +442,7 @@ export function addMarkerCompleteCallbackToPendingTransition( transitionProgress: null, transitionComplete: null, markerProgress: null, + markerIncomplete: null, markerComplete: new Map(), }; } @@ -437,6 +469,7 @@ export function addTransitionProgressCallbackToPendingTransition( transitionProgress: new Map(), transitionComplete: null, markerProgress: null, + markerIncomplete: null, markerComplete: null, }; } @@ -462,6 +495,7 @@ export function addTransitionCompleteCallbackToPendingTransition( transitionProgress: null, transitionComplete: [], markerProgress: null, + markerIncomplete: null, markerComplete: null, }; } diff --git a/packages/react-reconciler/src/ReactInternalTypes.js b/packages/react-reconciler/src/ReactInternalTypes.js index e9a4b61039f66..b03a715701368 100644 --- a/packages/react-reconciler/src/ReactInternalTypes.js +++ b/packages/react-reconciler/src/ReactInternalTypes.js @@ -291,8 +291,7 @@ export type TransitionTracingCallbacks = { startTime: number, deletions: Array<{ type: string, - name?: string, - newName?: string, + name?: string | null, endTime: number, }>, ) => void, @@ -314,8 +313,7 @@ export type TransitionTracingCallbacks = { startTime: number, deletions: Array<{ type: string, - name?: string, - newName?: string, + name?: string | null, endTime: number, }>, ) => void, @@ -337,7 +335,7 @@ type TransitionTracingOnlyFiberRootProperties = {| // are considered complete when the pending suspense boundaries set is // empty. We can represent this as a Map of transitions to suspense // boundary sets - incompleteTransitions: Map, TracingMarkerInstance>, + incompleteTransitions: Map, |}; // Exported FiberRoot type includes all properties, diff --git a/packages/react-reconciler/src/__tests__/ReactTransitionTracing-test.js b/packages/react-reconciler/src/__tests__/ReactTransitionTracing-test.js index ae4812afa8ec2..a6637b55977c9 100644 --- a/packages/react-reconciler/src/__tests__/ReactTransitionTracing-test.js +++ b/packages/react-reconciler/src/__tests__/ReactTransitionTracing-test.js @@ -23,6 +23,17 @@ let caches; let seededCache; describe('ReactInteractionTracing', () => { + function stringifyDeletions(deletions) { + return deletions + .map( + d => + `{${Object.keys(d) + .map(key => `${key}: ${d[key]}`) + .sort() + .join(', ')}}`, + ) + .join(', '); + } beforeEach(() => { jest.resetModules(); @@ -1284,18 +1295,846 @@ describe('ReactInteractionTracing', () => { }); // @gate enableTransitionTracing - it('warns when marker name changes', async () => { + it.skip('warn and calls marker incomplete if name changes before transition completes', 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, markerName}) { + return ( +
+ {navigate ? ( + + }> + + + + ) : ( + + )} +
+ ); + } + + 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 one', + }, + ); + ReactNoop.expire(1000); + await advanceTimers(1000); + + expect(Scheduler).toFlushAndYield([ + 'Suspend [Page Two]', + 'Loading...', + 'onTransitionStart(transition one, 1000)', + 'onMarkerProgress(transition one, marker one, 1000, 2000, [])', + 'onTransitionProgress(transition one, 1000, 2000, [])', + ]); + + root.render(); + ReactNoop.expire(1000); + await advanceTimers(1000); + expect(() => + expect(Scheduler).toFlushAndYield([ + 'Suspend [Page Two]', + 'Loading...', + 'onMarkerIncomplete(transition one, marker one, 1000, [{endTime: 3000, name: marker one, newName: marker two, type: marker}])', + ]), + ).toErrorDev(''); + + resolveText('Page Two'); + ReactNoop.expire(1000); + await advanceTimers(1000); + expect(Scheduler).toFlushAndYield([ + 'Page Two', + 'onMarkerProgress(transition one, marker one, 1000, 4000, [])', + 'onTransitionProgress(transition one, 1000, 4000, [])', + 'onTransitionComplete(transition one, 1000, 4000)', + ]); + }); + }); + + // @gate enableTransitionTracing + it('marker incomplete for tree with parent and sibling tracing markers', 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, showMarker}) { + return ( +
+ {navigate ? ( + + {showMarker ? ( + + }> + + + + ) : ( + }> + + + )} + + }> + + + + + ) : ( + + )} +
+ ); + } + + 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 one', + }, + ); + ReactNoop.expire(1000); + await advanceTimers(1000); + expect(Scheduler).toFlushAndYield([ + 'Suspend [Page Two]', + 'Loading...', + 'Suspend [Sibling Text]', + 'Sibling Loading...', + 'onTransitionStart(transition one, 1000)', + 'onMarkerProgress(transition one, parent, 1000, 2000, [suspense page, suspense sibling])', + 'onMarkerProgress(transition one, marker one, 1000, 2000, [suspense page])', + 'onMarkerProgress(transition one, sibling, 1000, 2000, [suspense sibling])', + 'onTransitionProgress(transition one, 1000, 2000, [suspense page, suspense sibling])', + ]); + root.render(); + + ReactNoop.expire(1000); + await advanceTimers(1000); + expect(Scheduler).toFlushAndYield([ + 'Suspend [Page Two]', + 'Loading...', + 'Suspend [Sibling Text]', + 'Sibling Loading...', + '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(); + ReactNoop.expire(1000); + await advanceTimers(1000); + expect(Scheduler).toFlushAndYield([ + 'Suspend [Page Two]', + 'Loading...', + 'Suspend [Sibling Text]', + 'Sibling Loading...', + ]); + }); + + resolveText('Page Two'); + ReactNoop.expire(1000); + await advanceTimers(1000); + expect(Scheduler).toFlushAndYield(['Page Two']); + + resolveText('Sibling Text'); + ReactNoop.expire(1000); + await advanceTimers(1000); + expect(Scheduler).toFlushAndYield([ + 'Sibling Text', + 'onMarkerProgress(transition one, parent, 1000, 6000, [])', + 'onMarkerProgress(transition one, sibling, 1000, 6000, [])', + // Calls markerComplete and transitionComplete for all parents + 'onMarkerComplete(transition one, sibling, 1000, 6000)', + 'onTransitionProgress(transition one, 1000, 6000, [])', + ]); + }); + + // @gate enableTransitionTracing + it('marker gets 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]', + '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])', + ]); + + root.render(); + ReactNoop.expire(1000); + await advanceTimers(1000); + expect(Scheduler).toFlushAndYield([ + 'Suspend [Page Two]', + 'Loading Two...', + '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'); + ReactNoop.expire(1000); + await advanceTimers(1000); + expect(Scheduler).toFlushAndYield([ + 'Page Two', + // Marker progress will still get called after incomplete but not marker complete + 'onMarkerProgress(transition, parent, 1000, 4000, [])', + 'onMarkerProgress(transition, two, 1000, 4000, [])', + 'onMarkerComplete(transition, two, 1000, 4000)', + // Transition progress will still get called after incomplete but not transition complete + 'onTransitionProgress(transition, 1000, 4000, [])', + ]); + }); + }); + + // @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 = { + onTransitionStart: (name, startTime) => { + Scheduler.unstable_yieldValue( + `onTransitionStart(${name}, ${startTime})`, + ); + }, + onTransitionComplete: (name, startTime, endTime) => { + Scheduler.unstable_yieldValue( + `onTransitionComplete(${name}, ${startTime}, ${endTime})`, + ); + }, + 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})`,