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 {