mirror of
https://github.com/zebrajr/react.git
synced 2025-12-06 00:20:04 +01:00
Don't flush synchronous work if we're in the middle of a ViewTransition async sequence (#32760)
Starting a View Transition is an async sequence. Since React can get a sync update in the middle of sequence we sometimes interrupt that sequence. Currently, we don't actually cancel the View Transition so it can just run as a partial. This ensures that we fully skip it when that happens, as well as warn. However, it's very easy to trigger this with just a setState in useLayoutEffect right now. Therefore if we're inside the preparing sequence of a startViewTransition, this delays work that would've normally flushed in a microtask. ~Maybe we want to do the same for Default work already scheduled through a scheduler Task.~ Edit: This was already done. `flushSync` currently will still lead to an interrupted View Transition (with a warning). There's a tradeoff here whether we want to try our best to preserve the guarantees of `flushSync` or favor the animation. It's already possible to suspend at the root with `flushSync` which means it's not always 100% guaranteed to commit anyway. We could treat it as suspended. But let's see how much this is a problem in practice.
This commit is contained in:
parent
254114616a
commit
a5297ece62
|
|
@ -2,6 +2,7 @@ import React, {
|
|||
unstable_ViewTransition as ViewTransition,
|
||||
unstable_Activity as Activity,
|
||||
unstable_useSwipeTransition as useSwipeTransition,
|
||||
useLayoutEffect,
|
||||
useEffect,
|
||||
useState,
|
||||
useId,
|
||||
|
|
@ -68,6 +69,16 @@ export default function Page({url, navigate}) {
|
|||
return () => clearInterval(timer);
|
||||
}, []);
|
||||
|
||||
useLayoutEffect(() => {
|
||||
// Calling a default update should not interrupt ViewTransitions but
|
||||
// a flushSync will.
|
||||
// Promise.resolve().then(() => {
|
||||
// flushSync(() => {
|
||||
setCounter(c => c + 10);
|
||||
// });
|
||||
// });
|
||||
}, [show]);
|
||||
|
||||
const exclamation = (
|
||||
<ViewTransition name="exclamation" onShare={onTransition}>
|
||||
<span>!</span>
|
||||
|
|
|
|||
10
packages/react-art/src/ReactFiberConfigART.js
vendored
10
packages/react-art/src/ReactFiberConfigART.js
vendored
|
|
@ -538,14 +538,16 @@ export function hasInstanceAffectedParent(
|
|||
}
|
||||
|
||||
export function startViewTransition() {
|
||||
return false;
|
||||
return null;
|
||||
}
|
||||
|
||||
export type RunningGestureTransition = null;
|
||||
export type RunningViewTransition = null;
|
||||
|
||||
export function startGestureTransition() {}
|
||||
export function startGestureTransition() {
|
||||
return null;
|
||||
}
|
||||
|
||||
export function stopGestureTransition(transition: RunningGestureTransition) {}
|
||||
export function stopViewTransition(transition: RunningViewTransition) {}
|
||||
|
||||
export type ViewTransitionInstance = null | {name: string, ...};
|
||||
|
||||
|
|
|
|||
|
|
@ -1687,7 +1687,7 @@ export function startViewTransition(
|
|||
spawnedWorkCallback: () => void,
|
||||
passiveCallback: () => mixed,
|
||||
errorCallback: mixed => void,
|
||||
): boolean {
|
||||
): null | RunningViewTransition {
|
||||
const ownerDocument: Document =
|
||||
rootContainer.nodeType === DOCUMENT_NODE
|
||||
? (rootContainer: any)
|
||||
|
|
@ -1764,7 +1764,7 @@ export function startViewTransition(
|
|||
}
|
||||
passiveCallback();
|
||||
});
|
||||
return true;
|
||||
return transition;
|
||||
} catch (x) {
|
||||
// We use the error as feature detection.
|
||||
// The only thing that should throw is if startViewTransition is missing
|
||||
|
|
@ -1772,11 +1772,17 @@ export function startViewTransition(
|
|||
// I.e. it's before the View Transitions v2 spec. We only support View
|
||||
// Transitions v2 otherwise we fallback to not animating to ensure that
|
||||
// we're not animating with the wrong animation mapped.
|
||||
return false;
|
||||
// Flush remaining work synchronously.
|
||||
mutationCallback();
|
||||
layoutCallback();
|
||||
// Skip afterMutationCallback(). We don't need it since we're not animating.
|
||||
spawnedWorkCallback();
|
||||
// Skip passiveCallback(). Spawned work will schedule a task.
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
export type RunningGestureTransition = {
|
||||
export type RunningViewTransition = {
|
||||
skipTransition(): void,
|
||||
...
|
||||
};
|
||||
|
|
@ -1900,7 +1906,7 @@ export function startGestureTransition(
|
|||
mutationCallback: () => void,
|
||||
animateCallback: () => void,
|
||||
errorCallback: mixed => void,
|
||||
): null | RunningGestureTransition {
|
||||
): null | RunningViewTransition {
|
||||
const ownerDocument: Document =
|
||||
rootContainer.nodeType === DOCUMENT_NODE
|
||||
? (rootContainer: any)
|
||||
|
|
@ -2072,7 +2078,7 @@ export function startGestureTransition(
|
|||
}
|
||||
}
|
||||
|
||||
export function stopGestureTransition(transition: RunningGestureTransition) {
|
||||
export function stopViewTransition(transition: RunningViewTransition) {
|
||||
transition.skipTransition();
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -653,11 +653,16 @@ export function startViewTransition(
|
|||
spawnedWorkCallback: () => void,
|
||||
passiveCallback: () => mixed,
|
||||
errorCallback: mixed => void,
|
||||
): boolean {
|
||||
return false;
|
||||
): null | RunningViewTransition {
|
||||
mutationCallback();
|
||||
layoutCallback();
|
||||
// Skip afterMutationCallback(). We don't need it since we're not animating.
|
||||
spawnedWorkCallback();
|
||||
// Skip passiveCallback(). Spawned work will schedule a task.
|
||||
return null;
|
||||
}
|
||||
|
||||
export type RunningGestureTransition = null;
|
||||
export type RunningViewTransition = null;
|
||||
|
||||
export function startGestureTransition(
|
||||
rootContainer: Container,
|
||||
|
|
@ -668,13 +673,13 @@ export function startGestureTransition(
|
|||
mutationCallback: () => void,
|
||||
animateCallback: () => void,
|
||||
errorCallback: mixed => void,
|
||||
): RunningGestureTransition {
|
||||
): null | RunningViewTransition {
|
||||
mutationCallback();
|
||||
animateCallback();
|
||||
return null;
|
||||
}
|
||||
|
||||
export function stopGestureTransition(transition: RunningGestureTransition) {}
|
||||
export function stopViewTransition(transition: RunningViewTransition) {}
|
||||
|
||||
export type ViewTransitionInstance = null | {name: string, ...};
|
||||
|
||||
|
|
|
|||
|
|
@ -93,7 +93,7 @@ export type TransitionStatus = mixed;
|
|||
|
||||
export type FormInstance = Instance;
|
||||
|
||||
export type RunningGestureTransition = null;
|
||||
export type RunningViewTransition = null;
|
||||
|
||||
export type ViewTransitionInstance = null | {name: string, ...};
|
||||
|
||||
|
|
@ -826,12 +826,18 @@ function createReactNoop(reconciler: Function, useMutation: boolean) {
|
|||
rootContainer: Container,
|
||||
transitionTypes: null | TransitionTypes,
|
||||
mutationCallback: () => void,
|
||||
afterMutationCallback: () => void,
|
||||
layoutCallback: () => void,
|
||||
afterMutationCallback: () => void,
|
||||
spawnedWorkCallback: () => void,
|
||||
passiveCallback: () => mixed,
|
||||
errorCallback: mixed => void,
|
||||
): boolean {
|
||||
return false;
|
||||
): null | RunningViewTransition {
|
||||
mutationCallback();
|
||||
layoutCallback();
|
||||
// Skip afterMutationCallback(). We don't need it since we're not animating.
|
||||
spawnedWorkCallback();
|
||||
// Skip passiveCallback(). Spawned work will schedule a task.
|
||||
return null;
|
||||
},
|
||||
|
||||
startGestureTransition(
|
||||
|
|
@ -843,13 +849,13 @@ function createReactNoop(reconciler: Function, useMutation: boolean) {
|
|||
mutationCallback: () => void,
|
||||
animateCallback: () => void,
|
||||
errorCallback: mixed => void,
|
||||
): RunningGestureTransition {
|
||||
): null | RunningViewTransition {
|
||||
mutationCallback();
|
||||
animateCallback();
|
||||
return null;
|
||||
},
|
||||
|
||||
stopGestureTransition(transition: RunningGestureTransition) {},
|
||||
stopViewTransition(transition: RunningViewTransition) {},
|
||||
|
||||
createViewTransitionInstance(name: string): ViewTransitionInstance {
|
||||
return null;
|
||||
|
|
|
|||
|
|
@ -51,9 +51,9 @@ export const wasInstanceInViewport = shim;
|
|||
export const hasInstanceChanged = shim;
|
||||
export const hasInstanceAffectedParent = shim;
|
||||
export const startViewTransition = shim;
|
||||
export type RunningGestureTransition = null;
|
||||
export type RunningViewTransition = null;
|
||||
export const startGestureTransition = shim;
|
||||
export const stopGestureTransition = shim;
|
||||
export const stopViewTransition = shim;
|
||||
export type ViewTransitionInstance = null | {name: string, ...};
|
||||
export const createViewTransitionInstance = shim;
|
||||
export type GestureTimeline = any;
|
||||
|
|
|
|||
|
|
@ -8,10 +8,7 @@
|
|||
*/
|
||||
|
||||
import type {FiberRoot} from './ReactInternalTypes';
|
||||
import type {
|
||||
GestureTimeline,
|
||||
RunningGestureTransition,
|
||||
} from './ReactFiberConfig';
|
||||
import type {GestureTimeline, RunningViewTransition} from './ReactFiberConfig';
|
||||
|
||||
import {
|
||||
GestureLane,
|
||||
|
|
@ -21,7 +18,7 @@ import {
|
|||
import {ensureRootIsScheduled} from './ReactFiberRootScheduler';
|
||||
import {
|
||||
subscribeToGestureDirection,
|
||||
stopGestureTransition,
|
||||
stopViewTransition,
|
||||
} from './ReactFiberConfig';
|
||||
|
||||
// This type keeps track of any scheduled or active gestures.
|
||||
|
|
@ -33,7 +30,7 @@ export type ScheduledGesture = {
|
|||
rangeCurrent: number, // The starting offset along the timeline.
|
||||
rangeNext: number, // The end along the timeline where the next state is reached.
|
||||
cancel: () => void, // Cancel the subscription to direction change.
|
||||
running: null | RunningGestureTransition, // Used to cancel the running transition after we're done.
|
||||
running: null | RunningViewTransition, // Used to cancel the running transition after we're done.
|
||||
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.
|
||||
};
|
||||
|
|
@ -144,7 +141,7 @@ export function cancelScheduledGesture(
|
|||
} else {
|
||||
gesture.running = null;
|
||||
// If there's no work scheduled so we can stop the View Transition right away.
|
||||
stopGestureTransition(runningTransition);
|
||||
stopViewTransition(runningTransition);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -183,7 +180,7 @@ export function stopCompletedGestures(root: FiberRoot) {
|
|||
root.stoppingGestures = null;
|
||||
while (gesture !== null) {
|
||||
if (gesture.running !== null) {
|
||||
stopGestureTransition(gesture.running);
|
||||
stopViewTransition(gesture.running);
|
||||
gesture.running = null;
|
||||
}
|
||||
const nextGesture = gesture.next;
|
||||
|
|
|
|||
|
|
@ -310,7 +310,12 @@ function processRootScheduleInMicrotask() {
|
|||
|
||||
// At the end of the microtask, flush any pending synchronous work. This has
|
||||
// to come at the end, because it does actual rendering work that might throw.
|
||||
flushSyncWorkAcrossRoots_impl(syncTransitionLanes, false);
|
||||
// If we're in the middle of a View Transition async sequence, we don't want to
|
||||
// interrupt that sequence. Instead, we'll flush any remaining work when it
|
||||
// completes.
|
||||
if (!hasPendingCommitEffects()) {
|
||||
flushSyncWorkAcrossRoots_impl(syncTransitionLanes, false);
|
||||
}
|
||||
}
|
||||
|
||||
function scheduleTaskForRootDuringMicrotask(
|
||||
|
|
|
|||
|
|
@ -21,7 +21,11 @@ import type {
|
|||
TransitionAbort,
|
||||
} from './ReactFiberTracingMarkerComponent';
|
||||
import type {OffscreenInstance} from './ReactFiberActivityComponent';
|
||||
import type {Resource, ViewTransitionInstance} from './ReactFiberConfig';
|
||||
import type {
|
||||
Resource,
|
||||
ViewTransitionInstance,
|
||||
RunningViewTransition,
|
||||
} from './ReactFiberConfig';
|
||||
import type {RootState} from './ReactFiberRoot';
|
||||
import {
|
||||
getViewTransitionName,
|
||||
|
|
@ -102,6 +106,7 @@ import {
|
|||
trackSchedulerEvent,
|
||||
startViewTransition,
|
||||
startGestureTransition,
|
||||
stopViewTransition,
|
||||
createViewTransitionInstance,
|
||||
} from './ReactFiberConfig';
|
||||
|
||||
|
|
@ -665,6 +670,7 @@ let pendingEffectsRemainingLanes: Lanes = NoLanes;
|
|||
let pendingEffectsRenderEndTime: number = -0; // Profiling-only
|
||||
let pendingPassiveTransitions: Array<Transition> | null = null;
|
||||
let pendingRecoverableErrors: null | Array<CapturedValue<mixed>> = null;
|
||||
let pendingViewTransition: null | RunningViewTransition = null;
|
||||
let pendingViewTransitionEvents: Array<(types: Array<string>) => void> | null =
|
||||
null;
|
||||
let pendingTransitionTypes: null | TransitionTypes = null;
|
||||
|
|
@ -3503,10 +3509,8 @@ function commitRoot(
|
|||
}
|
||||
|
||||
pendingEffectsStatus = PENDING_MUTATION_PHASE;
|
||||
const startedViewTransition =
|
||||
enableViewTransition &&
|
||||
willStartViewTransition &&
|
||||
startViewTransition(
|
||||
if (enableViewTransition && willStartViewTransition) {
|
||||
pendingViewTransition = startViewTransition(
|
||||
root.containerInfo,
|
||||
pendingTransitionTypes,
|
||||
flushMutationEffects,
|
||||
|
|
@ -3516,7 +3520,7 @@ function commitRoot(
|
|||
flushPassiveEffects,
|
||||
reportViewTransitionError,
|
||||
);
|
||||
if (!startedViewTransition) {
|
||||
} else {
|
||||
// Flush synchronously.
|
||||
flushMutationEffects();
|
||||
flushLayoutEffects();
|
||||
|
|
@ -3646,6 +3650,8 @@ function flushSpawnedWork(): void {
|
|||
}
|
||||
pendingEffectsStatus = NO_PENDING_EFFECTS;
|
||||
|
||||
pendingViewTransition = null; // The view transition has now fully started.
|
||||
|
||||
// Tell Scheduler to yield at the end of the frame, so the browser has an
|
||||
// opportunity to paint.
|
||||
requestPaint();
|
||||
|
|
@ -3915,7 +3921,7 @@ function commitGestureOnRoot(
|
|||
pendingTransitionTypes = null;
|
||||
pendingEffectsStatus = PENDING_GESTURE_MUTATION_PHASE;
|
||||
|
||||
finishedGesture.running = startGestureTransition(
|
||||
pendingViewTransition = finishedGesture.running = startGestureTransition(
|
||||
root.containerInfo,
|
||||
finishedGesture.provider,
|
||||
finishedGesture.rangeCurrent,
|
||||
|
|
@ -3975,6 +3981,8 @@ function flushGestureAnimations(): void {
|
|||
pendingFinishedWork = (null: any); // Clear for GC purposes.
|
||||
pendingEffectsLanes = NoLanes;
|
||||
|
||||
pendingViewTransition = null; // The view transition has now fully started.
|
||||
|
||||
const prevTransition = ReactSharedInternals.T;
|
||||
ReactSharedInternals.T = null;
|
||||
const previousPriority = getCurrentUpdatePriority();
|
||||
|
|
@ -4025,8 +4033,27 @@ function releaseRootPooledCache(root: FiberRoot, remainingLanes: Lanes) {
|
|||
}
|
||||
}
|
||||
|
||||
let didWarnAboutInterruptedViewTransitions = false;
|
||||
|
||||
export function flushPendingEffects(wasDelayedCommit?: boolean): boolean {
|
||||
// Returns whether passive effects were flushed.
|
||||
if (enableViewTransition && pendingViewTransition !== null) {
|
||||
// If we forced a flush before the View Transition full started then we skip it.
|
||||
// This ensures that we're not running a partial animation.
|
||||
stopViewTransition(pendingViewTransition);
|
||||
if (__DEV__) {
|
||||
if (!didWarnAboutInterruptedViewTransitions) {
|
||||
didWarnAboutInterruptedViewTransitions = true;
|
||||
console.warn(
|
||||
'A flushSync update cancelled a View Transition because it was called ' +
|
||||
'while the View Transition was still preparing. To preserve the synchronous ' +
|
||||
'semantics, React had to skip the View Transition. If you can, try to avoid ' +
|
||||
"flushSync() in a scenario that's likely to interfere.",
|
||||
);
|
||||
}
|
||||
}
|
||||
pendingViewTransition = null;
|
||||
}
|
||||
flushGestureMutations();
|
||||
flushGestureAnimations();
|
||||
flushMutationEffects();
|
||||
|
|
|
|||
|
|
@ -40,7 +40,7 @@ export opaque type NoTimeout = mixed;
|
|||
export opaque type RendererInspectionConfig = mixed;
|
||||
export opaque type TransitionStatus = mixed;
|
||||
export opaque type FormInstance = mixed;
|
||||
export type RunningGestureTransition = mixed;
|
||||
export type RunningViewTransition = mixed;
|
||||
export type ViewTransitionInstance = null | {name: string, ...};
|
||||
export opaque type InstanceMeasurement = mixed;
|
||||
export type EventResponder = any;
|
||||
|
|
@ -155,7 +155,7 @@ export const hasInstanceChanged = $$$config.hasInstanceChanged;
|
|||
export const hasInstanceAffectedParent = $$$config.hasInstanceAffectedParent;
|
||||
export const startViewTransition = $$$config.startViewTransition;
|
||||
export const startGestureTransition = $$$config.startGestureTransition;
|
||||
export const stopGestureTransition = $$$config.stopGestureTransition;
|
||||
export const stopViewTransition = $$$config.stopViewTransition;
|
||||
export const getCurrentGestureOffset = $$$config.getCurrentGestureOffset;
|
||||
export const subscribeToGestureDirection =
|
||||
$$$config.subscribeToGestureDirection;
|
||||
|
|
|
|||
|
|
@ -422,11 +422,16 @@ export function startViewTransition(
|
|||
spawnedWorkCallback: () => void,
|
||||
passiveCallback: () => mixed,
|
||||
errorCallback: mixed => void,
|
||||
): boolean {
|
||||
return false;
|
||||
): null | RunningViewTransition {
|
||||
mutationCallback();
|
||||
layoutCallback();
|
||||
// Skip afterMutationCallback(). We don't need it since we're not animating.
|
||||
spawnedWorkCallback();
|
||||
// Skip passiveCallback(). Spawned work will schedule a task.
|
||||
return null;
|
||||
}
|
||||
|
||||
export type RunningGestureTransition = null;
|
||||
export type RunningViewTransition = null;
|
||||
|
||||
export function startGestureTransition(
|
||||
rootContainer: Container,
|
||||
|
|
@ -437,13 +442,13 @@ export function startGestureTransition(
|
|||
mutationCallback: () => void,
|
||||
animateCallback: () => void,
|
||||
errorCallback: mixed => void,
|
||||
): RunningGestureTransition {
|
||||
): null | RunningViewTransition {
|
||||
mutationCallback();
|
||||
animateCallback();
|
||||
return null;
|
||||
}
|
||||
|
||||
export function stopGestureTransition(transition: RunningGestureTransition) {}
|
||||
export function stopViewTransition(transition: RunningViewTransition) {}
|
||||
|
||||
export type ViewTransitionInstance = null | {name: string, ...};
|
||||
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user