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;
}
}
},