Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 11 additions & 0 deletions fixtures/view-transition/src/components/Page.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import React, {
unstable_ViewTransition as ViewTransition,
unstable_Activity as Activity,
unstable_useSwipeTransition as useSwipeTransition,
useLayoutEffect,
useEffect,
useState,
useId,
Expand Down Expand Up @@ -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>
Expand Down
10 changes: 6 additions & 4 deletions packages/react-art/src/ReactFiberConfigART.js
Original file line number Diff line number Diff line change
Expand Up @@ -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, ...};

Expand Down
18 changes: 12 additions & 6 deletions packages/react-dom-bindings/src/client/ReactFiberConfigDOM.js
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -1764,19 +1764,25 @@ 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
// or if it doesn't accept the object form. Other errors are async.
// 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,
...
};
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -2072,7 +2078,7 @@ export function startGestureTransition(
}
}

export function stopGestureTransition(transition: RunningGestureTransition) {
export function stopViewTransition(transition: RunningViewTransition) {
transition.skipTransition();
}

Expand Down
15 changes: 10 additions & 5 deletions packages/react-native-renderer/src/ReactFiberConfigNative.js
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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, ...};

Expand Down
18 changes: 12 additions & 6 deletions packages/react-noop-renderer/src/createReactNoop.js
Original file line number Diff line number Diff line change
Expand Up @@ -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, ...};

Expand Down Expand Up @@ -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(
Expand All @@ -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;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
13 changes: 5 additions & 8 deletions packages/react-reconciler/src/ReactFiberGestureScheduler.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,10 +8,7 @@
*/

import type {FiberRoot} from './ReactInternalTypes';
import type {
GestureTimeline,
RunningGestureTransition,
} from './ReactFiberConfig';
import type {GestureTimeline, RunningViewTransition} from './ReactFiberConfig';

import {
GestureLane,
Expand All @@ -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.
Expand All @@ -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.
};
Expand Down Expand Up @@ -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);
}
}
}
Expand Down Expand Up @@ -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;
Expand Down
7 changes: 6 additions & 1 deletion packages/react-reconciler/src/ReactFiberRootScheduler.js
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand Down
41 changes: 34 additions & 7 deletions packages/react-reconciler/src/ReactFiberWorkLoop.js
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -102,6 +106,7 @@ import {
trackSchedulerEvent,
startViewTransition,
startGestureTransition,
stopViewTransition,
createViewTransitionInstance,
} from './ReactFiberConfig';

Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -3503,10 +3509,8 @@ function commitRoot(
}

pendingEffectsStatus = PENDING_MUTATION_PHASE;
const startedViewTransition =
enableViewTransition &&
willStartViewTransition &&
startViewTransition(
if (enableViewTransition && willStartViewTransition) {
pendingViewTransition = startViewTransition(
root.containerInfo,
pendingTransitionTypes,
flushMutationEffects,
Expand All @@ -3516,7 +3520,7 @@ function commitRoot(
flushPassiveEffects,
reportViewTransitionError,
);
if (!startedViewTransition) {
} else {
// Flush synchronously.
flushMutationEffects();
flushLayoutEffects();
Expand Down Expand Up @@ -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();
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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();
Expand Down Expand Up @@ -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();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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;
Expand Down
Loading
Loading