@samitouri / QOS-React / commits / 2c560374d6

Measure and apply names for the "new" phase (#32612)

Stacked on #32599 and #32611. This is able to reuse the code from CommitViewTransitions for "enter", "shared" and "layout". The difference is that for "enter"/"shared" in the "new" phase we pass in the deletions. For "layout" of nested boundaries we just need to measure the clones at the same time we measure the original nodes since we haven't measured them in a previous phase in the current approach. With these updates, things move around more like expected in the fixture because we're now applying the appropriate pairs to trigger individual animations instead of just the full document cross-fade. The "update" phase is a little more complicated and is coming soon.

Sebastian Markbåge committed Mar 14, 2025 at 14:26 UTC 2c560374d6276c51cfeeff7af39628058c155a13
3 files changed +44 -79
packages/react-reconciler/src/ReactFiberApplyGesture.js
+5 -68
@@ -62,6 +62,8 @@ import {
62 restoreEnterOrExitViewTransitions,
63 restoreNestedViewTransitions,
64 appearingViewTransitions,
65 + commitEnterViewTransitions,
66 + measureNestedViewTransitions,
67 } from './ReactFiberCommitViewTransitions';
68 import {
69 getViewTransitionName,
@@ -968,53 +970,6 @@ export function insertDestinationClones(
970 }
971 }
972
971 -function applyDeletedPairViewTransitions(deletion: Fiber): void {
972 - if ((deletion.subtreeFlags & ViewTransitionNamedStatic) === NoFlags) {
973 - // This has no named view transitions in its subtree.
974 - return;
975 - }
976 - let child = deletion.child;
977 - while (child !== null) {
978 - if (child.tag === OffscreenComponent && child.memoizedState === null) {
979 - // This tree was already hidden so we skip it.
980 - } else {
981 - if (
982 - child.tag === ViewTransitionComponent &&
983 - (child.flags & ViewTransitionNamedStatic) !== NoFlags
984 - ) {
985 - const props: ViewTransitionProps = child.memoizedProps;
986 - const name = props.name;
987 - if (name != null && name !== 'auto') {
988 - // TODO: Find a pair
989 - }
990 - }
991 - applyDeletedPairViewTransitions(child);
992 - }
993 - child = child.sibling;
994 - }
995 -}
996 -
997 -function applyEnterViewTransitions(deletion: Fiber): void {
998 - if (deletion.tag === ViewTransitionComponent) {
999 - const props: ViewTransitionProps = deletion.memoizedProps;
1000 - const name = props.name;
1001 - if (name != null && name !== 'auto') {
1002 - // TODO: Find a pair
1003 - }
1004 - // Look for more pairs deeper in the tree.
1005 - applyDeletedPairViewTransitions(deletion);
1006 - } else if ((deletion.subtreeFlags & ViewTransitionStatic) !== NoFlags) {
1007 - // TODO: Check if this is a hidden Offscreen or a Portal.
1008 - let child = deletion.child;
1009 - while (child !== null) {
1010 - applyEnterViewTransitions(child);
1011 - child = child.sibling;
1012 - }
1013 - } else {
1014 - applyDeletedPairViewTransitions(deletion);
1015 - }
1016 -}
1017 -
973 function measureExitViewTransitions(placement: Fiber): void {
974 if (placement.tag === ViewTransitionComponent) {
975 // const state: ViewTransitionState = placement.stateNode;
@@ -1036,24 +991,6 @@ function measureExitViewTransitions(placement: Fiber): void {
991 }
992 }
993
1039 -function measureNestedViewTransitions(changedParent: Fiber): void {
1040 - let child = changedParent.child;
1041 - while (child !== null) {
1042 - if (child.tag === ViewTransitionComponent) {
1043 - const current = child.alternate;
1044 - if (current !== null) {
1045 - // const props: ViewTransitionProps = child.memoizedProps;
1046 - // const name = getViewTransitionName(props, child.stateNode);
1047 - // TODO: Measure both the old and new state and see if they're different.
1048 - }
1049 - } else if ((child.subtreeFlags & ViewTransitionStatic) !== NoFlags) {
1050 - // TODO: Check if this is a hidden Offscreen or a Portal.
1051 - measureNestedViewTransitions(child);
1052 - }
1053 - child = child.sibling;
1054 - }
1055 -}
1056 -
994 function measureUpdateViewTransition(
995 current: Fiber,
996 finishedWork: Fiber,
@@ -1066,7 +1003,7 @@ function recursivelyApplyViewTransitions(parentFiber: Fiber) {
1003 if (deletions !== null) {
1004 for (let i = 0; i < deletions.length; i++) {
1005 const childToDelete = deletions[i];
1069 - applyEnterViewTransitions(childToDelete);
1006 + commitEnterViewTransitions(childToDelete, true);
1007 }
1008 }
1009
@@ -1084,7 +1021,7 @@ function recursivelyApplyViewTransitions(parentFiber: Fiber) {
1021 // Nothing has changed in this subtree, but the parent may have still affected
1022 // its size and position. We need to measure the old and new state to see if
1023 // we should animate its size and position.
1087 - measureNestedViewTransitions(parentFiber);
1024 + measureNestedViewTransitions(parentFiber, true);
1025 }
1026 }
1027
@@ -1121,7 +1058,7 @@ function applyViewTransitionsOnFiber(finishedWork: Fiber) {
1058 measureExitViewTransitions(finishedWork);
1059 } else if (current !== null && current.memoizedState === null) {
1060 // Was previously mounted as visible but is now hidden.
1124 - applyEnterViewTransitions(current);
1061 + commitEnterViewTransitions(current, true);
1062 }
1063 }
1064 break;
packages/react-reconciler/src/ReactFiberCommitViewTransitions.js
+36 -8
@@ -257,7 +257,10 @@ function commitAppearingPairViewTransitions(placement: Fiber): void {
257 }
258 }
259
260 -export function commitEnterViewTransitions(placement: Fiber): void {
260 +export function commitEnterViewTransitions(
261 + placement: Fiber,
262 + gesture: boolean,
263 +): void {
264 if (placement.tag === ViewTransitionComponent) {
265 const state: ViewTransitionState = placement.stateNode;
266 const props: ViewTransitionProps = placement.memoizedProps;
@@ -284,7 +287,11 @@ export function commitEnterViewTransitions(placement: Fiber): void {
287 commitAppearingPairViewTransitions(placement);
288
289 if (!state.paired) {
287 - scheduleViewTransitionEvent(placement, props.onEnter);
290 + if (gesture) {
291 + // TODO: Schedule gesture events.
292 + } else {
293 + scheduleViewTransitionEvent(placement, props.onEnter);
294 + }
295 }
296 }
297 } else {
@@ -293,7 +300,7 @@ export function commitEnterViewTransitions(placement: Fiber): void {
300 } else if ((placement.subtreeFlags & ViewTransitionStatic) !== NoFlags) {
301 let child = placement.child;
302 while (child !== null) {
296 - commitEnterViewTransitions(child);
303 + commitEnterViewTransitions(child, gesture);
304 child = child.sibling;
305 }
306 } else {
@@ -703,6 +710,8 @@ function measureViewTransitionHostInstancesRecursive(
710 }
711 if ((parentViewTransition.flags & Update) !== NoFlags) {
712 // We might update this node so we need to apply its new name for the new state.
713 + // Additionally in the ApplyGesture case we also need to do this because the clone
714 + // will have the name but this one won't.
715 applyViewTransitionName(
716 instance,
717 viewTransitionHostInstanceIdx === 0
@@ -828,32 +837,51 @@ export function measureUpdateViewTransition(
837 return inViewport;
838 }
839
831 -export function measureNestedViewTransitions(changedParent: Fiber): void {
840 +export function measureNestedViewTransitions(
841 + changedParent: Fiber,
842 + gesture: boolean,
843 +): void {
844 let child = changedParent.child;
845 while (child !== null) {
846 if (child.tag === ViewTransitionComponent) {
847 const props: ViewTransitionProps = child.memoizedProps;
836 - const name = getViewTransitionName(props, child.stateNode);
848 + const state: ViewTransitionState = child.stateNode;
849 + const name = getViewTransitionName(props, state);
850 const className: ?string = getViewTransitionClassName(
851 props.className,
852 props.layout,
853 );
854 + let previousMeasurements: null | Array<InstanceMeasurement>;
855 + if (gesture) {
856 + const clones = state.clones;
857 + if (clones === null) {
858 + previousMeasurements = null;
859 + } else {
860 + previousMeasurements = clones.map(measureInstance);
861 + }
862 + } else {
863 + previousMeasurements = child.memoizedState;
864 + }
865 const inViewport = measureViewTransitionHostInstances(
866 child,
867 child.child,
868 name,
869 name, // Since this is unchanged, new and old name is the same.
870 className,
847 - child.memoizedState,
871 + previousMeasurements,
872 false,
873 );
874 if ((child.flags & Update) === NoFlags || !inViewport) {
875 // Nothing changed.
876 } else {
853 - scheduleViewTransitionEvent(child, props.onLayout);
877 + if (gesture) {
878 + // TODO: Schedule gesture events.
879 + } else {
880 + scheduleViewTransitionEvent(child, props.onLayout);
881 + }
882 }
883 } else if ((child.subtreeFlags & ViewTransitionStatic) !== NoFlags) {
856 - measureNestedViewTransitions(child);
884 + measureNestedViewTransitions(child, gesture);
885 }
886 child = child.sibling;
887 }
packages/react-reconciler/src/ReactFiberCommitWork.js
+3 -3
@@ -2450,7 +2450,7 @@ function recursivelyTraverseAfterMutationEffects(
2450 // Nothing has changed in this subtree, but the parent may have still affected
2451 // its size and position. We need to measure this and if not, restore it to
2452 // not animate.
2453 - measureNestedViewTransitions(parentFiber);
2453 + measureNestedViewTransitions(parentFiber, false);
2454 }
2455 }
2456
@@ -2468,7 +2468,7 @@ function commitAfterMutationEffectsOnFiber(
2468 // we can just bail after we're done with the first one.
2469 // The first ViewTransition inside a newly mounted tree runs an enter transition
2470 // but other nested ones don't unless they have a named pair.
2471 - commitEnterViewTransitions(finishedWork);
2471 + commitEnterViewTransitions(finishedWork, false);
2472 return;
2473 }
2474
@@ -2512,7 +2512,7 @@ function commitAfterMutationEffectsOnFiber(
2512 // The Offscreen tree is visible.
2513 const wasHidden = current.memoizedState !== null;
2514 if (wasHidden) {
2515 - commitEnterViewTransitions(finishedWork);
2515 + commitEnterViewTransitions(finishedWork, false);
2516 // If it was previous hidden then the children are treated as enter
2517 // not updates so we don't need to visit these children.
2518 } else {