diff --git a/fixtures/view-transition/src/components/Page.js b/fixtures/view-transition/src/components/Page.js index fbaa9017171..6227e9ebc5f 100644 --- a/fixtures/view-transition/src/components/Page.js +++ b/fixtures/view-transition/src/components/Page.js @@ -171,17 +171,20 @@ export default function Page({url, navigate}) { }}>

{!show ? 'A' + counter : 'B'}

- {show ? ( -
- {a} - {b} -
- ) : ( -
- {b} - {a} -
- )} + { + // Using url instead of renderedUrl here lets us only update this on commit. + url === '/?b' ? ( +
+ {a} + {b} +
+ ) : ( +
+ {b} + {a} +
+ ) + } {show ? (
hello{exclamation}
diff --git a/fixtures/view-transition/src/components/SwipeRecognizer.js b/fixtures/view-transition/src/components/SwipeRecognizer.js index df4d743e1ba..6d628110237 100644 --- a/fixtures/view-transition/src/components/SwipeRecognizer.js +++ b/fixtures/view-transition/src/components/SwipeRecognizer.js @@ -114,16 +114,17 @@ export default function SwipeRecognizer({ ); } function onGestureEnd(changed) { - // Reset scroll - if (changed) { - // Trigger side-effects - startTransition(action); - } + // We cancel the gesture before invoking side-effects to allow the gesture lane to fully commit + // before scheduling new updates. if (activeGesture.current !== null) { const cancelGesture = activeGesture.current; activeGesture.current = null; cancelGesture(); } + if (changed) { + // Trigger side-effects + startTransition(action); + } } function onScrollEnd() { if (touchTimeline.current) { diff --git a/packages/react-dom/src/__tests__/ReactDOMFizzServer-test.js b/packages/react-dom/src/__tests__/ReactDOMFizzServer-test.js index 32745e4b109..7d4220460d9 100644 --- a/packages/react-dom/src/__tests__/ReactDOMFizzServer-test.js +++ b/packages/react-dom/src/__tests__/ReactDOMFizzServer-test.js @@ -9478,4 +9478,146 @@ Unfortunately that previous paragraph wasn't quite long enough so I'll continue , ); }); + + it('useId is consistent for siblings when component suspends with nested lazy', async () => { + // Inner component uses useId + function InnerComponent() { + const id = React.useId(); + Scheduler.log('InnerComponent id: ' + id); + return inner; + } + + // Outer component uses useId and renders a lazy inner + function OuterComponent({innerElement}) { + const id = React.useId(); + Scheduler.log('OuterComponent id: ' + id); + return
{innerElement}
; + } + + // This sibling also has useId - its ID must be consistent with server + function Sibling() { + const id = React.useId(); + Scheduler.log('Sibling id: ' + id); + return sibling; + } + + // Create fresh lazy components for SERVER (resolve immediately) + const serverLazyInner = React.lazy(async () => { + Scheduler.log('server lazy inner initializer'); + return {default: }; + }); + + const serverLazyOuter = React.lazy(async () => { + Scheduler.log('server lazy outer initializer'); + return { + default: , + }; + }); + + // Server render with lazy (resolves immediately) + await act(() => { + const {pipe} = renderToPipeableStream( + + + <>{serverLazyOuter} + <> + + + + , + ); + pipe(writable); + }); + + expect(getVisibleChildren(document)).toEqual( + + + +
+ inner +
+ sibling + + , + ); + + assertLog([ + 'server lazy outer initializer', + 'Sibling id: _R_2_', + 'OuterComponent id: _R_1_', + 'server lazy inner initializer', + 'InnerComponent id: _R_5_', + ]); + + // Create fresh lazy components for CLIENT + let resolveClientInner; + const clientLazyInner = React.lazy(async () => { + Scheduler.log('client lazy inner initializer'); + return new Promise(r => { + resolveClientInner = () => r({default: }); + }); + }); + + let resolveClientOuter; + const clientLazyOuter = React.lazy(async () => { + Scheduler.log('client lazy outer initializer'); + return new Promise(r => { + resolveClientOuter = () => + r({default: }); + }); + }); + + const hydrationErrors = []; + + // Client hydrates with nested lazy components + let root; + React.startTransition(() => { + root = ReactDOMClient.hydrateRoot( + document, + + + <>{clientLazyOuter} + <> + + + + , + { + onRecoverableError(error) { + hydrationErrors.push(error.message); + }, + }, + ); + }); + + // First suspension on outer lazy + await waitFor(['client lazy outer initializer']); + resolveClientOuter(); + + // Second suspension on inner lazy + await waitFor([ + 'OuterComponent id: _R_1_', + 'client lazy inner initializer', + ]); + resolveClientInner(); + + await waitForAll(['InnerComponent id: _R_5_', 'Sibling id: _R_2_']); + + // The IDs should match the server-generated IDs + expect(hydrationErrors).toEqual([]); + + expect(getVisibleChildren(document)).toEqual( + + + +
+ inner +
+ sibling + + , + ); + + root.unmount(); + }); }); diff --git a/packages/react-reconciler/src/ReactFiberFlags.js b/packages/react-reconciler/src/ReactFiberFlags.js index cc43edc66b6..7fa1dcb8173 100644 --- a/packages/react-reconciler/src/ReactFiberFlags.js +++ b/packages/react-reconciler/src/ReactFiberFlags.js @@ -62,13 +62,13 @@ export const ShouldCapture = /* */ 0b0000000000000010000000000000 export const ForceUpdateForLegacySuspense = /* */ 0b0000000000000100000000000000000; export const DidPropagateContext = /* */ 0b0000000000001000000000000000000; export const NeedsPropagation = /* */ 0b0000000000010000000000000000000; -export const Forked = /* */ 0b0000000000100000000000000000000; // Static tags describe aspects of a fiber that are not specific to a render, // e.g. a fiber uses a passive effect (even if there are no updates on this particular render). // This enables us to defer more work in the unmount case, // since we can defer traversing the tree during layout to look for Passive effects, // and instead rely on the static flag as a signal that there may be cleanup work. +export const Forked = /* */ 0b0000000000100000000000000000000; export const SnapshotStatic = /* */ 0b0000000001000000000000000000000; export const LayoutStatic = /* */ 0b0000000010000000000000000000000; export const RefStatic = LayoutStatic; @@ -142,4 +142,5 @@ export const StaticMask = MaySuspendCommit | ViewTransitionStatic | ViewTransitionNamedStatic | - PortalStatic; + PortalStatic | + Forked; diff --git a/packages/react-reconciler/src/ReactFiberGestureScheduler.js b/packages/react-reconciler/src/ReactFiberGestureScheduler.js index ed61c59b05a..373e8167cab 100644 --- a/packages/react-reconciler/src/ReactFiberGestureScheduler.js +++ b/packages/react-reconciler/src/ReactFiberGestureScheduler.js @@ -11,14 +11,21 @@ import type {FiberRoot} from './ReactInternalTypes'; import type {GestureOptions} from 'shared/ReactTypes'; import type {GestureTimeline, RunningViewTransition} from './ReactFiberConfig'; import type {TransitionTypes} from 'react/src/ReactTransitionType'; +import type {Lane} from './ReactFiberLane'; import { GestureLane, - includesBlockingLane, - includesTransitionLane, + markRootEntangled, + markRootFinished, + NoLane, + NoLanes, } from './ReactFiberLane'; -import {ensureRootIsScheduled} from './ReactFiberRootScheduler'; +import { + ensureRootIsScheduled, + requestTransitionLane, +} from './ReactFiberRootScheduler'; import {getCurrentGestureOffset, stopViewTransition} from './ReactFiberConfig'; +import {pingGestureRoot, restartGestureRoot} from './ReactFiberWorkLoop'; // This type keeps track of any scheduled or active gestures. export type ScheduledGesture = { @@ -28,6 +35,9 @@ export type ScheduledGesture = { rangeEnd: number, // The percentage along the timeline where the "destination" state is reached. types: null | TransitionTypes, // Any addTransitionType call made during startGestureTransition. running: null | RunningViewTransition, // Used to cancel the running transition after we're done. + commit: null | (() => void), // Callback to run to commit if there's a pending commit. + committing: boolean, // If the gesture was released in a committed state and should actually commit. + revertLane: Lane, // The Lane that we'll use to schedule the revert. prev: null | ScheduledGesture, // The previous scheduled gesture in the queue for this root. next: null | ScheduledGesture, // The next scheduled gesture in the queue for this root. }; @@ -55,6 +65,9 @@ export function scheduleGesture( rangeEnd: 100, // Uninitialized types: null, running: null, + commit: null, + committing: false, + revertLane: NoLane, // Starts uninitialized. prev: prev, next: null, }; @@ -120,79 +133,125 @@ export function cancelScheduledGesture( root: FiberRoot, gesture: ScheduledGesture, ): void { + // Entangle any Transitions started in this event with the revertLane of the gesture + // so that we commit them all together. + if (gesture.revertLane !== NoLane) { + const entangledLanes = gesture.revertLane | requestTransitionLane(null); + markRootEntangled(root, entangledLanes); + } + gesture.count--; if (gesture.count === 0) { - // Delete the scheduled gesture from the pending queue. - deleteScheduledGesture(root, gesture); + // If the end state is closer to the end than the beginning then we commit into the + // end state before reverting back (or applying a new Transition). + // Otherwise we just revert back and don't commit. + let shouldCommit: boolean; + const finalOffset = getCurrentGestureOffset(gesture.provider); + const rangeStart = gesture.rangeStart; + const rangeEnd = gesture.rangeEnd; + if (rangeStart < rangeEnd) { + shouldCommit = finalOffset > rangeStart + (rangeEnd - rangeStart) / 2; + } else { + shouldCommit = finalOffset < rangeEnd + (rangeStart - rangeEnd) / 2; + } // TODO: If we're currently rendering this gesture, we need to restart the render // on a different gesture or cancel the render.. // TODO: We might want to pause the View Transition at this point since you should // no longer be able to update the position of anything but it might be better to // just commit the gesture state. const runningTransition = gesture.running; - if (runningTransition !== null) { - const pendingLanesExcludingGestureLane = root.pendingLanes & ~GestureLane; - if ( - includesBlockingLane(pendingLanesExcludingGestureLane) || - includesTransitionLane(pendingLanesExcludingGestureLane) - ) { - // If we have pending work we schedule the gesture to be stopped at the next commit. - // This ensures that we don't snap back to the previous state until we have - // had a chance to commit any resulting updates. - const existing = root.stoppingGestures; - if (existing !== null) { - gesture.next = existing; - existing.prev = gesture; + if (runningTransition !== null && shouldCommit) { + // If we are going to commit this gesture in its to state, we need to wait to + // stop it until it commits. We should now schedule a render at the gesture + // lane to actually commit it. + gesture.committing = true; + if (root.pendingGestures === gesture) { + const commitCallback = gesture.commit; + if (commitCallback !== null) { + gesture.commit = null; + // If we already have a commit prepared we can immediately commit the tree + // without rerendering. + // TODO: Consider scheduling this in a task instead of synchronously inside the last cancellation.s + commitCallback(); + } else { + // Ping the root given the new state. This is similar to pingSuspendedRoot. + pingGestureRoot(root); + } + } + } else { + // If we're not going to commit this gesture we can stop the View Transition + // right away and delete the scheduled gesture from the pending queue. + if (gesture.prev === null) { + if (root.pendingGestures === gesture) { + // This was the currently rendering gesture. + root.pendingGestures = gesture.next; + let remainingLanes = root.pendingLanes; + if (root.pendingGestures === null) { + // Gestures don't clear their lanes while the gesture is still active but it + // might not be scheduled to do any more renders and so we shouldn't schedule + // any more gesture lane work until a new gesture is scheduled. + remainingLanes &= ~GestureLane; + } + markRootFinished( + root, + GestureLane, + remainingLanes, + NoLane, + NoLane, + NoLanes, + ); + // If we had a currently rendering gesture we need to now reset the gesture lane to + // now render the next gesture or cancel if there's no more gestures in the queue. + restartGestureRoot(root); } - root.stoppingGestures = gesture; - } else { gesture.running = null; - // If there's no work scheduled so we can stop the View Transition right away. - stopViewTransition(runningTransition); + if (runningTransition !== null) { + stopViewTransition(runningTransition); + } + } else { + // This was not the current gesture so it doesn't affect the current render. + gesture.prev.next = gesture.next; + if (gesture.next !== null) { + gesture.next.prev = gesture.prev; + } + gesture.prev = null; + gesture.next = null; } } } } -export function deleteScheduledGesture( - root: FiberRoot, - gesture: ScheduledGesture, -): void { - if (gesture.prev === null) { - if (root.pendingGestures === gesture) { - root.pendingGestures = gesture.next; - if (root.pendingGestures === null) { - // Gestures don't clear their lanes while the gesture is still active but it - // might not be scheduled to do any more renders and so we shouldn't schedule - // any more gesture lane work until a new gesture is scheduled. - root.pendingLanes &= ~GestureLane; - } - } - if (root.stoppingGestures === gesture) { - // This should not really happen the way we use it now but just in case we start. - root.stoppingGestures = gesture.next; +export function stopCommittedGesture(root: FiberRoot) { + // The top was just committed. We can delete it from the queue + // and stop its View Transition now. + const committedGesture = root.pendingGestures; + if (committedGesture !== null) { + // Mark it as no longer committing and should no longer be included in rerenders. + committedGesture.committing = false; + const nextGesture = committedGesture.next; + if (nextGesture === null) { + // Gestures don't clear their lanes while the gesture is still active but it + // might not be scheduled to do any more renders and so we shouldn't schedule + // any more gesture lane work until a new gesture is scheduled. + root.pendingLanes &= ~GestureLane; + } else { + nextGesture.prev = null; } - } else { - gesture.prev.next = gesture.next; - if (gesture.next !== null) { - gesture.next.prev = gesture.prev; + root.pendingGestures = nextGesture; + const runningTransition = committedGesture.running; + if (runningTransition !== null) { + committedGesture.running = null; + stopViewTransition(runningTransition); } - gesture.prev = null; - gesture.next = null; } } -export function stopCompletedGestures(root: FiberRoot) { - let gesture = root.stoppingGestures; - root.stoppingGestures = null; - while (gesture !== null) { - if (gesture.running !== null) { - stopViewTransition(gesture.running); - gesture.running = null; - } - const nextGesture = gesture.next; - gesture.next = null; - gesture.prev = null; - gesture = nextGesture; - } +export function scheduleGestureCommit( + gesture: ScheduledGesture, + callback: () => void, +): () => void { + gesture.commit = callback; + return function () { + gesture.commit = null; + }; } diff --git a/packages/react-reconciler/src/ReactFiberHooks.js b/packages/react-reconciler/src/ReactFiberHooks.js index 66a390ebb81..422667c6a12 100644 --- a/packages/react-reconciler/src/ReactFiberHooks.js +++ b/packages/react-reconciler/src/ReactFiberHooks.js @@ -1384,7 +1384,7 @@ function updateReducerImpl( // ScheduledGesture. const scheduledGesture = update.gesture; if (scheduledGesture !== null) { - if (scheduledGesture.count === 0) { + if (scheduledGesture.count === 0 && !scheduledGesture.committing) { // This gesture has already been cancelled. We can clean up this update. update = update.next; continue; @@ -3792,7 +3792,14 @@ function dispatchOptimisticSetState( if (provider !== null) { // If this was a gesture, ensure we have a scheduled gesture and that // we associate this update with this specific gesture instance. - update.gesture = scheduleGesture(root, provider); + const gesture = (update.gesture = scheduleGesture(root, provider)); + // Ensure the gesture always uses the same revert lane. This can happen for + // two startGestureTransition calls to the same provider in different events. + if (gesture.revertLane === NoLane) { + gesture.revertLane = update.revertLane; + } else { + update.revertLane = gesture.revertLane; + } } } } diff --git a/packages/react-reconciler/src/ReactFiberLane.js b/packages/react-reconciler/src/ReactFiberLane.js index 7ec53c096a5..b54047cb4aa 100644 --- a/packages/react-reconciler/src/ReactFiberLane.js +++ b/packages/react-reconciler/src/ReactFiberLane.js @@ -621,7 +621,8 @@ export function includesOnlyRetries(lanes: Lanes): boolean { export function includesOnlyNonUrgentLanes(lanes: Lanes): boolean { // TODO: Should hydration lanes be included here? This function is only // used in `updateDeferredValueImpl`. - const UrgentLanes = SyncLane | InputContinuousLane | DefaultLane; + const UrgentLanes = + SyncLane | InputContinuousLane | DefaultLane | GestureLane; return (lanes & UrgentLanes) === NoLanes; } export function includesOnlyTransitions(lanes: Lanes): boolean { diff --git a/packages/react-reconciler/src/ReactFiberPerformanceTrack.js b/packages/react-reconciler/src/ReactFiberPerformanceTrack.js index f926bd6085b..a1a2d76cecc 100644 --- a/packages/react-reconciler/src/ReactFiberPerformanceTrack.js +++ b/packages/react-reconciler/src/ReactFiberPerformanceTrack.js @@ -1569,6 +1569,41 @@ export function logPaintYieldPhase( } } +export function logApplyGesturePhase( + startTime: number, + endTime: number, + debugTask: null | ConsoleTask, +): void { + if (supportsUserTiming) { + if (endTime <= startTime) { + return; + } + if (__DEV__ && debugTask) { + debugTask.run( + // $FlowFixMe[method-unbinding] + console.timeStamp.bind( + console, + 'Create Ghost Tree', + startTime, + endTime, + currentTrack, + LANES_TRACK_GROUP, + 'secondary-dark', + ), + ); + } else { + console.timeStamp( + 'Create Ghost Tree', + startTime, + endTime, + currentTrack, + LANES_TRACK_GROUP, + 'secondary-dark', + ); + } + } +} + export function logStartViewTransitionYieldPhase( startTime: number, endTime: number, diff --git a/packages/react-reconciler/src/ReactFiberRoot.js b/packages/react-reconciler/src/ReactFiberRoot.js index 908893db948..26386597ee4 100644 --- a/packages/react-reconciler/src/ReactFiberRoot.js +++ b/packages/react-reconciler/src/ReactFiberRoot.js @@ -115,7 +115,6 @@ function FiberRootNode( if (enableGestureTransition) { this.pendingGestures = null; - this.stoppingGestures = null; this.gestureClone = null; } diff --git a/packages/react-reconciler/src/ReactFiberWorkLoop.js b/packages/react-reconciler/src/ReactFiberWorkLoop.js index 9f21041e6df..5bc334364ad 100644 --- a/packages/react-reconciler/src/ReactFiberWorkLoop.js +++ b/packages/react-reconciler/src/ReactFiberWorkLoop.js @@ -92,6 +92,7 @@ import { logSuspendedYieldTime, setCurrentTrackFromLanes, markAllLanesInOrder, + logApplyGesturePhase, } from './ReactFiberPerformanceTrack'; import { @@ -399,8 +400,8 @@ import { import {getMaskedContext, getUnmaskedContext} from './ReactFiberLegacyContext'; import {logUncaughtError} from './ReactFiberErrorLogger'; import { - deleteScheduledGesture, - stopCompletedGestures, + scheduleGestureCommit, + stopCommittedGesture, } from './ReactFiberGestureScheduler'; import {claimQueuedTransitionTypes} from './ReactFiberTransitionTypes'; @@ -1407,7 +1408,7 @@ function finishConcurrentRender( if (shouldForceFlushFallbacksInDEV()) { // We're inside an `act` scope. Commit immediately. - commitRoot( + completeRoot( root, finishedWork, lanes, @@ -1417,6 +1418,7 @@ function finishConcurrentRender( workInProgressDeferredLane, workInProgressRootInterleavedUpdatedLanes, workInProgressSuspendedRetryLanes, + workInProgressRootDidSkipSuspendedSiblings, exitStatus, null, null, @@ -1458,7 +1460,7 @@ function finishConcurrentRender( // run one after the other. pendingEffectsLanes = lanes; root.timeoutHandle = scheduleTimeout( - commitRootWhenReady.bind( + completeRootWhenReady.bind( null, root, finishedWork, @@ -1480,7 +1482,7 @@ function finishConcurrentRender( return; } } - commitRootWhenReady( + completeRootWhenReady( root, finishedWork, workInProgressRootRecoverableErrors, @@ -1499,7 +1501,7 @@ function finishConcurrentRender( } } -function commitRootWhenReady( +function completeRootWhenReady( root: FiberRoot, finishedWork: Fiber, recoverableErrors: Array> | null, @@ -1540,12 +1542,17 @@ function commitRootWhenReady( // This will also track any newly added or appearing ViewTransition // components for the purposes of forming pairs. accumulateSuspenseyCommit(finishedWork, lanes, suspendedState); - if (isViewTransitionEligible || isGestureTransition) { - // If we're stopping gestures we don't have to wait for any pending - // view transition. We'll stop it when we commit. - if (!enableGestureTransition || root.stoppingGestures === null) { - suspendOnActiveViewTransition(suspendedState, root.containerInfo); - } + if ( + isViewTransitionEligible || + (isGestureTransition && + root.pendingGestures !== null && + // If this gesture already has a View Transition running then we don't + // have to wait on that one before proceeding. We may hold the commit + // on the gesture committing later on in completeRoot. + root.pendingGestures.running === null) + ) { + // Wait for any pending View Transition (including gestures) to finish. + suspendOnActiveViewTransition(suspendedState, root.containerInfo); } // For timeouts we use the previous fallback commit for retries and // the start time of the transition for transitions. This offset @@ -1572,7 +1579,7 @@ function commitRootWhenReady( // root again. pendingEffectsLanes = lanes; root.cancelPendingCommit = schedulePendingCommit( - commitRoot.bind( + completeRoot.bind( null, root, finishedWork, @@ -1583,6 +1590,7 @@ function commitRootWhenReady( spawnedLane, updatedLanes, suspendedRetryLanes, + didSkipSuspendedSiblings, exitStatus, suspendedState, enableProfilerTimer @@ -1599,7 +1607,7 @@ function commitRootWhenReady( } // Otherwise, commit immediately.; - commitRoot( + completeRoot( root, finishedWork, lanes, @@ -1609,6 +1617,7 @@ function commitRootWhenReady( spawnedLane, updatedLanes, suspendedRetryLanes, + didSkipSuspendedSiblings, exitStatus, suspendedState, suspendedCommitReason, @@ -3420,7 +3429,7 @@ function unwindUnitOfWork(unitOfWork: Fiber, skipSiblings: boolean): void { workInProgress = null; } -function commitRoot( +function completeRoot( root: FiberRoot, finishedWork: null | Fiber, lanes: Lanes, @@ -3430,6 +3439,7 @@ function commitRoot( spawnedLane: Lane, updatedLanes: Lanes, suspendedRetryLanes: Lanes, + didSkipSuspendedSiblings: boolean, exitStatus: RootExitStatus, suspendedState: null | SuspendedState, suspendedCommitReason: SuspendedCommitReason, // Profiling-only @@ -3456,6 +3466,16 @@ function commitRoot( if (enableProfilerTimer && enableComponentPerformanceTrack) { // Log the previous render phase once we commit. I.e. we weren't interrupted. setCurrentTrackFromLanes(lanes); + if (isGestureRender(lanes)) { + // Clamp the render start time in case if something else on this lane was committed + // (such as this same tree before). + if (completedRenderStartTime < gestureClampTime) { + completedRenderStartTime = gestureClampTime; + } + if (completedRenderEndTime < gestureClampTime) { + completedRenderEndTime = gestureClampTime; + } + } if (exitStatus === RootErrored) { logErroredRenderPhase( completedRenderStartTime, @@ -3496,9 +3516,9 @@ function commitRoot( markCommitStopped(); } if (enableGestureTransition) { - // Stop any gestures that were completed and is now being reverted. - if (root.stoppingGestures !== null) { - stopCompletedGestures(root); + // Stop any gestures that were committed. + if (isGestureRender(lanes)) { + stopCommittedGesture(root); } } return; @@ -3520,10 +3540,126 @@ function commitRoot( ); } + if (root === workInProgressRoot) { + // We can reset these now that they are finished. + workInProgressRoot = null; + workInProgress = null; + workInProgressRootRenderLanes = NoLanes; + } else { + // This indicates that the last root we worked on is not the same one that + // we're committing now. This most commonly happens when a suspended root + // times out. + } + + // workInProgressX might be overwritten, so we want + // to store it in pendingPassiveX until they get processed + // We need to pass this through as an argument to completeRoot + // because workInProgressX might have changed between + // the previous render and commit if we throttle the commit + // with setTimeout + pendingFinishedWork = finishedWork; + pendingEffectsRoot = root; + pendingEffectsLanes = lanes; + pendingPassiveTransitions = transitions; + pendingRecoverableErrors = recoverableErrors; + pendingDidIncludeRenderPhaseUpdate = didIncludeRenderPhaseUpdate; + if (enableProfilerTimer) { + pendingEffectsRenderEndTime = completedRenderEndTime; + pendingSuspendedCommitReason = suspendedCommitReason; + pendingDelayedCommitReason = IMMEDIATE_COMMIT; + pendingSuspendedViewTransitionReason = null; + } + + if (enableGestureTransition && isGestureRender(lanes)) { + const committingGesture = root.pendingGestures; + if (committingGesture !== null && !committingGesture.committing) { + // This gesture is not ready to commit yet. We'll mark it as suspended and + // start a gesture transition which isn't really a side-effect. Then later + // we might come back around to actually committing the root. + const didAttemptEntireTree = !didSkipSuspendedSiblings; + markRootSuspended(root, lanes, spawnedLane, didAttemptEntireTree); + if (committingGesture.running === null) { + applyGestureOnRoot( + root, + finishedWork, + recoverableErrors, + suspendedState, + enableProfilerTimer + ? suspendedCommitReason === null + ? completedRenderEndTime + : commitStartTime + : 0, + ); + } else { + // If we already have a gesture running, we don't update it in place + // even if we have a new tree. Instead we wait until we can commit. + if (enableProfilerTimer && enableComponentPerformanceTrack) { + // Clamp at the render time since we're not going to finish the rest + // of this commit or apply yet. + finalizeRender(lanes, completedRenderEndTime); + } + // We are no longer committing. + pendingEffectsRoot = (null: any); // Clear for GC purposes. + pendingFinishedWork = (null: any); // Clear for GC purposes. + pendingEffectsLanes = NoLanes; + } + // Schedule the root to be committed when the gesture completes. + root.cancelPendingCommit = scheduleGestureCommit( + committingGesture, + completeRoot.bind( + null, + root, + finishedWork, + lanes, + recoverableErrors, + transitions, + didIncludeRenderPhaseUpdate, + spawnedLane, + updatedLanes, + suspendedRetryLanes, + didSkipSuspendedSiblings, + exitStatus, + suspendedState, + 'Waiting for the Gesture to finish' /* suspendedCommitReason */, + completedRenderStartTime, + completedRenderEndTime, + ), + ); + return; + } + } + + // If we're not starting a gesture we now actually commit the root. + commitRoot( + root, + finishedWork, + lanes, + spawnedLane, + updatedLanes, + suspendedRetryLanes, + suspendedState, + suspendedCommitReason, + completedRenderEndTime, + ); +} + +function commitRoot( + root: FiberRoot, + finishedWork: Fiber, + lanes: Lanes, + spawnedLane: Lane, + updatedLanes: Lanes, + suspendedRetryLanes: Lanes, + suspendedState: null | SuspendedState, + suspendedCommitReason: SuspendedCommitReason, // Profiling-only + completedRenderEndTime: number, // Profiling-only +) { // Check which lanes no longer have any work scheduled on them, and mark // those as finished. let remainingLanes = mergeLanes(finishedWork.lanes, finishedWork.childLanes); + pendingEffectsRemainingLanes = remainingLanes; + // Make sure to account for lanes that were updated by a concurrent event // during the render phase; don't mark them as finished. const concurrentlyUpdatedLanes = getConcurrentlyUpdatedLanes(); @@ -3553,53 +3689,6 @@ function commitRoot( // Reset this before firing side effects so we can detect recursive updates. didIncludeCommitPhaseUpdate = false; - if (root === workInProgressRoot) { - // We can reset these now that they are finished. - workInProgressRoot = null; - workInProgress = null; - workInProgressRootRenderLanes = NoLanes; - } else { - // This indicates that the last root we worked on is not the same one that - // we're committing now. This most commonly happens when a suspended root - // times out. - } - - // workInProgressX might be overwritten, so we want - // to store it in pendingPassiveX until they get processed - // We need to pass this through as an argument to commitRoot - // because workInProgressX might have changed between - // the previous render and commit if we throttle the commit - // with setTimeout - pendingFinishedWork = finishedWork; - pendingEffectsRoot = root; - pendingEffectsLanes = lanes; - pendingEffectsRemainingLanes = remainingLanes; - pendingPassiveTransitions = transitions; - pendingRecoverableErrors = recoverableErrors; - pendingDidIncludeRenderPhaseUpdate = didIncludeRenderPhaseUpdate; - if (enableProfilerTimer) { - pendingEffectsRenderEndTime = completedRenderEndTime; - pendingSuspendedCommitReason = suspendedCommitReason; - pendingDelayedCommitReason = IMMEDIATE_COMMIT; - pendingSuspendedViewTransitionReason = null; - } - - if (enableGestureTransition && isGestureRender(lanes)) { - // This is a special kind of render that doesn't commit regular effects. - commitGestureOnRoot( - root, - finishedWork, - recoverableErrors, - suspendedState, - enableProfilerTimer - ? suspendedCommitReason === null - ? completedRenderEndTime - : commitStartTime - : 0, - ); - return; - } - // If there are pending passive effects, schedule a callback to process them. // Do this as early as possible, so it is queued before anything else that // might get scheduled in the commit phase. (See #16714.) @@ -3712,20 +3801,19 @@ function commitRoot( } } - let willStartViewTransition = shouldStartViewTransition; if (enableGestureTransition) { - // Stop any gestures that were completed and is now being committed. - if (root.stoppingGestures !== null) { - stopCompletedGestures(root); - // If we are in the process of stopping some gesture we shouldn't start - // a View Transition because that would start from the previous state to - // the next state. - willStartViewTransition = false; + // Stop any gestures that were committed. + if (isGestureRender(lanes)) { + stopCommittedGesture(root); + // Note that shouldStartViewTransition should always be false here because + // committing a gesture never starts a new View Transition itself since it's + // not a View Transition eligible lane. Only follow up Transition commits can + // cause animate. } } pendingEffectsStatus = PENDING_MUTATION_PHASE; - if (enableViewTransition && willStartViewTransition) { + if (enableViewTransition && shouldStartViewTransition) { if (enableProfilerTimer && enableComponentPerformanceTrack) { startAnimating(lanes); } @@ -4155,7 +4243,7 @@ function flushSpawnedWork(): void { flushPendingEffects(); } - // Always call this before exiting `commitRoot`, to ensure that any + // Always call this before exiting `completeRoot`, to ensure that any // additional work on this root is scheduled. ensureRootIsScheduled(root); @@ -4244,7 +4332,7 @@ function flushSpawnedWork(): void { } } -function commitGestureOnRoot( +function applyGestureOnRoot( root: FiberRoot, finishedWork: Fiber, recoverableErrors: null | Array>, @@ -4259,7 +4347,6 @@ function commitGestureOnRoot( ensureRootIsScheduled(root); return; } - deleteScheduledGesture(root, finishedGesture); if (enableProfilerTimer && enableComponentPerformanceTrack) { startAnimating(pendingEffectsLanes); @@ -4325,6 +4412,15 @@ function flushGestureMutations(): void { ReactSharedInternals.T = prevTransition; } + if (enableProfilerTimer && enableComponentPerformanceTrack) { + recordCommitEndTime(); + logApplyGesturePhase( + pendingEffectsRenderEndTime, + commitEndTime, + animatingTask, + ); + } + pendingEffectsStatus = PENDING_GESTURE_ANIMATION_PHASE; } @@ -4342,10 +4438,11 @@ function flushGestureAnimations(): void { const lanes = pendingEffectsLanes; if (enableProfilerTimer && enableComponentPerformanceTrack) { + const startViewTransitionStartTime = commitEndTime; // Update the new commitEndTime to when we started the animation. recordCommitEndTime(); logStartViewTransitionYieldPhase( - pendingEffectsRenderEndTime, + startViewTransitionStartTime, commitEndTime, pendingDelayedCommitReason === ABORTED_VIEW_TRANSITION_COMMIT, animatingTask, @@ -4468,7 +4565,7 @@ function flushPassiveEffects(): boolean { // flushPassiveEffectsImpl const root = pendingEffectsRoot; // Cache and clear the remaining lanes flag; it must be reset since this - // method can be called from various places, not always from commitRoot + // method can be called from various places, not always from completeRoot // where the remaining lanes are known const remainingLanes = pendingEffectsRemainingLanes; pendingEffectsRemainingLanes = NoLanes; @@ -4856,6 +4953,45 @@ function pingSuspendedRoot( ensureRootIsScheduled(root); } +export function pingGestureRoot(root: FiberRoot): void { + const gesture = root.pendingGestures; + if (gesture === null) { + return; + } + // Ping it for rerender and commit. + markRootPinged(root, GestureLane); + ensureRootIsScheduled(root); +} + +export function restartGestureRoot(root: FiberRoot): void { + if ( + workInProgressRoot === root && + isGestureRender(workInProgressRootRenderLanes) + ) { + // The current render was a gesture but it's now defunct. We need to restart the render. + if ((executionContext & RenderContext) === NoContext) { + prepareFreshStack(root, NoLanes); + } else { + // TODO: Throw interruption when supported again. + } + } else if ( + root.cancelPendingCommit !== null && + isGestureRender(pendingEffectsLanes) + ) { + // We have a suspended commit which we'll discard. + const cancelPendingCommit = root.cancelPendingCommit; + if (cancelPendingCommit !== null) { + root.cancelPendingCommit = null; + cancelPendingCommit(); + } + } + if (root.pendingGestures !== null) { + // We still have gestures to work on. Let's schedule a restart. + markRootPinged(root, GestureLane); + } + ensureRootIsScheduled(root); +} + function retryTimedOutBoundary(boundaryFiber: Fiber, retryLane: Lane) { // The boundary fiber (a Suspense component or SuspenseList component) // previously was rendered in its fallback state. One of the promises that diff --git a/packages/react-reconciler/src/ReactInternalTypes.js b/packages/react-reconciler/src/ReactInternalTypes.js index 775b69d211f..ce22050123c 100644 --- a/packages/react-reconciler/src/ReactInternalTypes.js +++ b/packages/react-reconciler/src/ReactInternalTypes.js @@ -292,7 +292,6 @@ type BaseFiberRootProperties = { transitionTypes: null | TransitionTypes, // TODO: Make this a LaneMap. // enableGestureTransition only pendingGestures: null | ScheduledGesture, - stoppingGestures: null | ScheduledGesture, gestureClone: null | Instance, }; diff --git a/packages/react/src/ReactLazy.js b/packages/react/src/ReactLazy.js index 47f37afbae2..b380750ca10 100644 --- a/packages/react/src/ReactLazy.js +++ b/packages/react/src/ReactLazy.js @@ -117,13 +117,15 @@ function lazyInitializer(payload: Payload): T { // $FlowFixMe ioInfo.value.value = debugValue; } - // Make the thenable introspectable - if (thenable.status === undefined) { - const fulfilledThenable: FulfilledThenable<{default: T, ...}> = - (thenable: any); - fulfilledThenable.status = 'fulfilled'; - fulfilledThenable.value = moduleObject; - } + } + // Make the thenable introspectable + // TODO we should move the lazy introspection into the resolveLazy + // impl or make suspendedThenable be able to be a lazy itself + if (thenable.status === undefined) { + const fulfilledThenable: FulfilledThenable<{default: T, ...}> = + (thenable: any); + fulfilledThenable.status = 'fulfilled'; + fulfilledThenable.value = moduleObject; } } }, @@ -151,13 +153,15 @@ function lazyInitializer(payload: Payload): T { // $FlowFixMe ioInfo.value.reason = error; } - // Make the thenable introspectable - if (thenable.status === undefined) { - const rejectedThenable: RejectedThenable<{default: T, ...}> = - (thenable: any); - rejectedThenable.status = 'rejected'; - rejectedThenable.reason = error; - } + } + // Make the thenable introspectable + // TODO we should move the lazy introspection into the resolveLazy + // impl or make suspendedThenable be able to be a lazy itself + if (thenable.status === undefined) { + const rejectedThenable: RejectedThenable<{default: T, ...}> = + (thenable: any); + rejectedThenable.status = 'rejected'; + rejectedThenable.reason = error; } } },