@samitouri / QOS-React / commits / 42a57ea802

Merge ViewTransition layout/onLayout props into update/onUpdate (#32723)

We currently have the ability to have a separate animation for a ViewTransition that relayouts but doesn't actually have any internal mutations. This can be useful if you want to separate just a move from for example flashing an update. However, we're concerned that this might be more confusion than its worth because subtle differences in mutations can cause it to trigger the other case. The existence of the property name might also make you start looking for it to solve something that it's not meant for. We already fallback to using the "update" property if it exists but layout doesn't. So if we ever decide to add this back it would backwards compatible. We've also shown in implementation that it can work.

Sebastian Markbåge committed Mar 24, 2025 at 14:04 UTC 42a57ea8027de8af55e6f4483c3b9a8f4cba31fb
4 files changed +16 -114
packages/react-reconciler/src/ReactFiberApplyGesture.js
+4 -8
@@ -308,7 +308,7 @@ function applyNestedViewTransition(child: Fiber): void {
308 const name = getViewTransitionName(props, state);
309 const className: ?string = getViewTransitionClassName(
310 props.className,
311 - props.layout,
311 + props.update,
312 );
313 if (className !== 'none') {
314 const clones = state.clones;
@@ -335,17 +335,13 @@ function applyUpdateViewTransition(current: Fiber, finishedWork: Fiber): void {
335 // we would use. However, since this animation is going in reverse we actually
336 // want the props from "current" since that's the class that would've won if
337 // it was the normal direction. To preserve the same effect in either direction.
338 - let className: ?string = getViewTransitionClassName(
338 + const className: ?string = getViewTransitionClassName(
339 newProps.className,
340 newProps.update,
341 );
342 if (className === 'none') {
343 - className = getViewTransitionClassName(newProps.className, newProps.layout);
344 - if (className === 'none') {
345 - // If both update and layout are both "none" then we don't have to
346 - // apply a name. Since we won't animate this boundary.
347 - return;
348 - }
343 + // If update is "none" then we don't have to apply a name. Since we won't animate this boundary.
344 + return;
345 }
346 const clones = state.clones;
347 // If there are no clones at this point, that should mean that there are no
packages/react-reconciler/src/ReactFiberCommitViewTransitions.js
+11 -96
@@ -469,17 +469,13 @@ export function commitBeforeUpdateViewTransition(
469 // For example, if update="foo" layout="none" and it turns out this was
470 // a layout only change, then the "foo" class will be applied even though
471 // it was not actually an update. Which is a bug.
472 - let className: ?string = getViewTransitionClassName(
472 + const className: ?string = getViewTransitionClassName(
473 newProps.className,
474 newProps.update,
475 );
476 if (className === 'none') {
477 - className = getViewTransitionClassName(newProps.className, newProps.layout);
478 - if (className === 'none') {
479 - // If both update and layout are both "none" then we don't have to
480 - // apply a name. Since we won't animate this boundary.
481 - return;
482 - }
477 + // If update is "none" then we don't have to apply a name. Since we won't animate this boundary.
478 + return;
479 }
480 applyViewTransitionToHostInstances(
481 current.child,
@@ -500,7 +496,7 @@ export function commitNestedViewTransitions(changedParent: Fiber): void {
496 const name = getViewTransitionName(props, child.stateNode);
497 const className: ?string = getViewTransitionClassName(
498 props.className,
503 - props.layout,
499 + props.update,
500 );
501 if (className !== 'none') {
502 applyViewTransitionToHostInstances(
@@ -590,61 +586,6 @@ export function restoreNestedViewTransitions(changedParent: Fiber): void {
586 }
587 }
588
593 -export function cancelViewTransitionHostInstances(
594 - child: null | Fiber,
595 - oldName: string,
596 - stopAtNestedViewTransitions: boolean,
597 -): void {
598 - viewTransitionHostInstanceIdx = 0;
599 - cancelViewTransitionHostInstancesRecursive(
600 - child,
601 - oldName,
602 - stopAtNestedViewTransitions,
603 - );
604 -}
605 -
606 -function cancelViewTransitionHostInstancesRecursive(
607 - child: null | Fiber,
608 - oldName: string,
609 - stopAtNestedViewTransitions: boolean,
610 -): void {
611 - if (!supportsMutation) {
612 - return;
613 - }
614 - while (child !== null) {
615 - if (child.tag === HostComponent) {
616 - const instance: Instance = child.stateNode;
617 - if (viewTransitionCancelableChildren === null) {
618 - viewTransitionCancelableChildren = [];
619 - }
620 - viewTransitionCancelableChildren.push(
621 - instance,
622 - oldName,
623 - child.memoizedProps,
624 - );
625 - viewTransitionHostInstanceIdx++;
626 - } else if (
627 - child.tag === OffscreenComponent &&
628 - child.memoizedState !== null
629 - ) {
630 - // Skip any hidden subtrees. They were or are effectively not there.
631 - } else if (
632 - child.tag === ViewTransitionComponent &&
633 - stopAtNestedViewTransitions
634 - ) {
635 - // Skip any nested view transitions for updates since in that case the
636 - // inner most one is the one that handles the update.
637 - } else {
638 - cancelViewTransitionHostInstancesRecursive(
639 - child.child,
640 - oldName,
641 - stopAtNestedViewTransitions,
642 - );
643 - }
644 - child = child.sibling;
645 - }
646 -}
647 -
589 export function measureViewTransitionHostInstances(
590 parentViewTransition: Fiber,
591 child: null | Fiber,
@@ -792,40 +733,14 @@ export function measureUpdateViewTransition(
733 const state: ViewTransitionState = newFiber.stateNode;
734 const newName = getViewTransitionName(props, state);
735 const oldName = getViewTransitionName(oldFiber.memoizedProps, state);
795 - const updateClassName: ?string = getViewTransitionClassName(
736 + // Whether it ends up having been updated or relayout we apply the update class name.
737 + const className: ?string = getViewTransitionClassName(
738 props.className,
739 props.update,
740 );
799 - const layoutClassName: ?string = getViewTransitionClassName(
800 - props.className,
801 - props.layout,
802 - );
803 - let className: ?string;
804 - if (updateClassName === 'none') {
805 - if (layoutClassName === 'none') {
806 - // If both update and layout class name were none, then we didn't apply any
807 - // names in the before update phase so we shouldn't now neither.
808 - return false;
809 - }
810 - // We don't care if this is mutated or children layout changed, but we still
811 - // measure each instance to see if it moved and therefore should apply layout.
812 - finishedWork.flags &= ~Update;
813 - className = layoutClassName;
814 - } else if ((finishedWork.flags & Update) !== NoFlags) {
815 - // It was updated and we have an appropriate class name to apply.
816 - className = updateClassName;
817 - } else {
818 - if (layoutClassName === 'none') {
819 - // If we did not update, then all changes are considered a layout. We'll
820 - // attempt to cancel.
821 - // This should use the Fiber that got names applied in the snapshot phase
822 - // since those are the ones we're trying to cancel.
823 - cancelViewTransitionHostInstances(oldFiber.child, oldName, true);
824 - return false;
825 - }
826 - // We didn't update but we might still apply layout so we measure each
827 - // instance to see if it moved or resized.
828 - className = layoutClassName;
741 + if (className === 'none') {
742 + // If update is "none" then we don't have to apply a name. Since we won't animate this boundary.
743 + return false;
744 }
745 // If nothing changed due to a mutation, or children changing size
746 // and the measurements end up unchanged, we should restore it to not animate.
@@ -873,7 +788,7 @@ export function measureNestedViewTransitions(
788 const name = getViewTransitionName(props, state);
789 const className: ?string = getViewTransitionClassName(
790 props.className,
876 - props.layout,
791 + props.update,
792 );
793 let previousMeasurements: null | Array<InstanceMeasurement>;
794 if (gesture) {
@@ -902,7 +817,7 @@ export function measureNestedViewTransitions(
817 if (gesture) {
818 // TODO: Schedule gesture events.
819 } else {
905 - scheduleViewTransitionEvent(child, props.onLayout);
820 + scheduleViewTransitionEvent(child, props.onUpdate);
821 }
822 }
823 } else if ((child.subtreeFlags & ViewTransitionStatic) !== NoFlags) {
packages/react-reconciler/src/ReactFiberCommitWork.js
+1 -8
@@ -2440,8 +2440,6 @@ function commitAfterMutationEffectsOnFiber(
2440 break;
2441 }
2442 case ViewTransitionComponent: {
2443 - const wasMutated = (finishedWork.flags & Update) !== NoFlags;
2444 -
2443 const prevContextChanged = viewTransitionContextChanged;
2444 const prevCancelableChildren = pushViewTransitionCancelableScope();
2445 viewTransitionContextChanged = false;
@@ -2477,12 +2475,7 @@ function commitAfterMutationEffectsOnFiber(
2475 // then we should probably issue an event since this instance is part of it.
2476 } else {
2477 const props: ViewTransitionProps = finishedWork.memoizedProps;
2480 - scheduleViewTransitionEvent(
2481 - finishedWork,
2482 - wasMutated || viewTransitionContextChanged
2483 - ? props.onUpdate
2484 - : props.onLayout,
2485 - );
2478 + scheduleViewTransitionEvent(finishedWork, props.onUpdate);
2479
2480 // If this boundary did update, we cannot cancel its children so those are dropped.
2481 popViewTransitionCancelableScope(prevCancelableChildren);
packages/react-reconciler/src/ReactFiberViewTransitionComponent.js
-2
@@ -32,12 +32,10 @@ export type ViewTransitionProps = {
32 className?: ViewTransitionClass,
33 enter?: ViewTransitionClass,
34 exit?: ViewTransitionClass,
35 - layout?: ViewTransitionClass,
35 share?: ViewTransitionClass,
36 update?: ViewTransitionClass,
37 onEnter?: (instance: ViewTransitionInstance, types: Array<string>) => void,
38 onExit?: (instance: ViewTransitionInstance, types: Array<string>) => void,
40 - onLayout?: (instance: ViewTransitionInstance, types: Array<string>) => void,
39 onShare?: (instance: ViewTransitionInstance, types: Array<string>) => void,
40 onUpdate?: (instance: ViewTransitionInstance, types: Array<string>) => void,
41 };