Skip to content

Commit 4b3728f

Browse files
authored
[Fiber] Track Appearing Named ViewTransition in the accumulateSuspenseyCommit Phase (#32254)
When a named ViewTransition component unmounts in one place and mounts in a different place we need to match these up so we know a pair has been created. Since the unmounts are tracked in the snapshot phase we need some way to track the mounts before that. Originally the way I did that is by reusing the render phase since there was no other phase in the commit before that. However, that's not quite correct. Just because something is visited in render doesn't mean it'll commit. E.g. if that tree ends up suspending or erroring. Which would lead to a false positive on match. The unmount shouldn't animate in that case. (Un)fortunately we have already added a traversal before the snapshot phase for tracking suspensey CSS. The `accumulateSuspenseyCommit` phase. This needs to find new mounts of Suspensey CSS or if there was a reappearing Offscreen boundary it needs to find any Suspensey CSS already inside that tree. This is exactly the same traversal we need to find newly appearing View Transition components. So we can just reuse that.
1 parent f02ba2f commit 4b3728f

File tree

5 files changed

+66
-122
lines changed

5 files changed

+66
-122
lines changed

packages/react-reconciler/src/ReactFiberBeginWork.js

Lines changed: 5 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -97,6 +97,7 @@ import {
9797
Passive,
9898
DidDefer,
9999
ViewTransitionNamedStatic,
100+
ViewTransitionNamedMount,
100101
LayoutStatic,
101102
} from './ReactFiberFlags';
102103
import {
@@ -266,7 +267,6 @@ import {
266267
markSkippedUpdateLanes,
267268
getWorkInProgressRoot,
268269
peekDeferredLane,
269-
trackAppearingViewTransition,
270270
} from './ReactFiberWorkLoop';
271271
import {enqueueConcurrentRenderForLane} from './ReactFiberConcurrentUpdates';
272272
import {pushCacheProvider, CacheContext} from './ReactFiberCacheComponent';
@@ -3243,12 +3243,10 @@ function updateViewTransition(
32433243
if (pendingProps.name != null && pendingProps.name !== 'auto') {
32443244
// Explicitly named boundary. We track it so that we can pair it up with another explicit
32453245
// boundary if we get deleted.
3246-
workInProgress.flags |= ViewTransitionNamedStatic;
3247-
if (current === null) {
3248-
// This is a new mount. We track it in case we end up having a deletion with the same name.
3249-
// TODO: A problem with this strategy is that this subtree might not actually end up mounted.
3250-
trackAppearingViewTransition(instance, pendingProps.name);
3251-
}
3246+
workInProgress.flags |=
3247+
current === null
3248+
? ViewTransitionNamedMount | ViewTransitionNamedStatic
3249+
: ViewTransitionNamedStatic;
32523250
} else {
32533251
// Assign an auto generated name using the useId algorthim if an explicit one is not provided.
32543252
// We don't need the name yet but we do it here to allow hydration state to be used.

packages/react-reconciler/src/ReactFiberCommitWork.js

Lines changed: 52 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -276,6 +276,10 @@ export let shouldFireAfterActiveInstanceBlur: boolean = false;
276276

277277
export let shouldStartViewTransition: boolean = false;
278278

279+
// This tracks named ViewTransition components found in the accumulateSuspenseyCommit
280+
// phase that might need to find deleted pairs in the beforeMutation phase.
281+
let appearingViewTransitions: Map<string, ViewTransitionState> | null = null;
282+
279283
// Used during the commit phase to track whether a parent ViewTransition component
280284
// might have been affected by any mutations / relayouts below.
281285
let viewTransitionContextChanged: boolean = false;
@@ -288,7 +292,6 @@ export function commitBeforeMutationEffects(
288292
root: FiberRoot,
289293
firstChild: Fiber,
290294
committedLanes: Lanes,
291-
appearingViewTransitions: Map<string, ViewTransitionState> | null,
292295
): void {
293296
focusedInstanceHandle = prepareForCommit(root.containerInfo);
294297
shouldFireAfterActiveInstanceBlur = false;
@@ -299,19 +302,15 @@ export function commitBeforeMutationEffects(
299302
includesOnlyViewTransitionEligibleLanes(committedLanes);
300303

301304
nextEffect = firstChild;
302-
commitBeforeMutationEffects_begin(
303-
isViewTransitionEligible,
304-
appearingViewTransitions,
305-
);
305+
commitBeforeMutationEffects_begin(isViewTransitionEligible);
306306

307307
// We no longer need to track the active instance fiber
308308
focusedInstanceHandle = null;
309+
// We've found any matched pairs and can now reset.
310+
appearingViewTransitions = null;
309311
}
310312

311-
function commitBeforeMutationEffects_begin(
312-
isViewTransitionEligible: boolean,
313-
appearingViewTransitions: Map<string, ViewTransitionState> | null,
314-
) {
313+
function commitBeforeMutationEffects_begin(isViewTransitionEligible: boolean) {
315314
// If this commit is eligible for a View Transition we look into all mutated subtrees.
316315
// TODO: We could optimize this by marking these with the Snapshot subtree flag in the render phase.
317316
const subtreeMask = isViewTransitionEligible
@@ -331,7 +330,6 @@ function commitBeforeMutationEffects_begin(
331330
commitBeforeMutationEffectsDeletion(
332331
deletion,
333332
isViewTransitionEligible,
334-
appearingViewTransitions,
335333
);
336334
}
337335
}
@@ -364,7 +362,7 @@ function commitBeforeMutationEffects_begin(
364362
isViewTransitionEligible
365363
) {
366364
// Was previously mounted as visible but is now hidden.
367-
commitExitViewTransitions(current, appearingViewTransitions);
365+
commitExitViewTransitions(current);
368366
}
369367
// Skip before mutation effects of the children because they're hidden.
370368
commitBeforeMutationEffects_complete(isViewTransitionEligible);
@@ -528,7 +526,6 @@ function commitBeforeMutationEffectsOnFiber(
528526
function commitBeforeMutationEffectsDeletion(
529527
deletion: Fiber,
530528
isViewTransitionEligible: boolean,
531-
appearingViewTransitions: Map<string, ViewTransitionState> | null,
532529
) {
533530
if (enableCreateEventHandleAPI) {
534531
// TODO (effects) It would be nice to avoid calling doesFiberContain()
@@ -541,7 +538,7 @@ function commitBeforeMutationEffectsDeletion(
541538
}
542539
}
543540
if (isViewTransitionEligible) {
544-
commitExitViewTransitions(deletion, appearingViewTransitions);
541+
commitExitViewTransitions(deletion);
545542
}
546543
}
547544

@@ -745,14 +742,15 @@ function commitEnterViewTransitions(placement: Fiber): void {
745742
}
746743
}
747744

748-
function commitDeletedPairViewTransitions(
749-
deletion: Fiber,
750-
appearingViewTransitions: Map<string, ViewTransitionState>,
751-
): void {
752-
if (appearingViewTransitions.size === 0) {
745+
function commitDeletedPairViewTransitions(deletion: Fiber): void {
746+
if (
747+
appearingViewTransitions === null ||
748+
appearingViewTransitions.size === 0
749+
) {
753750
// We've found all.
754751
return;
755752
}
753+
const pairs = appearingViewTransitions;
756754
if ((deletion.subtreeFlags & ViewTransitionNamedStatic) === NoFlags) {
757755
// This has no named view transitions in its subtree.
758756
return;
@@ -769,7 +767,7 @@ function commitDeletedPairViewTransitions(
769767
const props: ViewTransitionProps = child.memoizedProps;
770768
const name = props.name;
771769
if (name != null && name !== 'auto') {
772-
const pair = appearingViewTransitions.get(name);
770+
const pair = pairs.get(name);
773771
if (pair !== undefined) {
774772
const className: ?string = getViewTransitionClassName(
775773
props.className,
@@ -802,23 +800,20 @@ function commitDeletedPairViewTransitions(
802800
}
803801
// Delete the entry so that we know when we've found all of them
804802
// and can stop searching (size reaches zero).
805-
appearingViewTransitions.delete(name);
806-
if (appearingViewTransitions.size === 0) {
803+
pairs.delete(name);
804+
if (pairs.size === 0) {
807805
break;
808806
}
809807
}
810808
}
811809
}
812-
commitDeletedPairViewTransitions(child, appearingViewTransitions);
810+
commitDeletedPairViewTransitions(child);
813811
}
814812
child = child.sibling;
815813
}
816814
}
817815

818-
function commitExitViewTransitions(
819-
deletion: Fiber,
820-
appearingViewTransitions: Map<string, ViewTransitionState> | null,
821-
): void {
816+
function commitExitViewTransitions(deletion: Fiber): void {
822817
if (deletion.tag === ViewTransitionComponent) {
823818
const props: ViewTransitionProps = deletion.memoizedProps;
824819
const name = getViewTransitionName(props, deletion.stateNode);
@@ -863,17 +858,17 @@ function commitExitViewTransitions(
863858
}
864859
if (appearingViewTransitions !== null) {
865860
// Look for more pairs deeper in the tree.
866-
commitDeletedPairViewTransitions(deletion, appearingViewTransitions);
861+
commitDeletedPairViewTransitions(deletion);
867862
}
868863
} else if ((deletion.subtreeFlags & ViewTransitionStatic) !== NoFlags) {
869864
let child = deletion.child;
870865
while (child !== null) {
871-
commitExitViewTransitions(child, appearingViewTransitions);
866+
commitExitViewTransitions(child);
872867
child = child.sibling;
873868
}
874869
} else {
875870
if (appearingViewTransitions !== null) {
876-
commitDeletedPairViewTransitions(deletion, appearingViewTransitions);
871+
commitDeletedPairViewTransitions(deletion);
877872
}
878873
}
879874
}
@@ -4813,8 +4808,13 @@ export function commitPassiveUnmountEffects(finishedWork: Fiber): void {
48134808
// already in the "current" tree. Because their visibility has changed, the
48144809
// browser may not have prerendered them yet. So we check the MaySuspendCommit
48154810
// flag instead.
4811+
//
4812+
// Note that MaySuspendCommit and ShouldSuspendCommit also includes named
4813+
// ViewTransitions so that we know to also visit those to collect appearing
4814+
// pairs.
48164815
let suspenseyCommitFlag = ShouldSuspendCommit;
48174816
export function accumulateSuspenseyCommit(finishedWork: Fiber): void {
4817+
appearingViewTransitions = null;
48184818
accumulateSuspenseyCommitOnFiber(finishedWork);
48194819
}
48204820

@@ -4893,6 +4893,29 @@ function accumulateSuspenseyCommitOnFiber(fiber: Fiber) {
48934893
}
48944894
break;
48954895
}
4896+
case ViewTransitionComponent: {
4897+
if (enableViewTransition) {
4898+
if ((fiber.flags & suspenseyCommitFlag) !== NoFlags) {
4899+
const props: ViewTransitionProps = fiber.memoizedProps;
4900+
const name: ?string | 'auto' = props.name;
4901+
if (name != null && name !== 'auto') {
4902+
// This is a named ViewTransition being mounted or reappearing. Let's add it to
4903+
// the map so we can match it with deletions later.
4904+
if (appearingViewTransitions === null) {
4905+
appearingViewTransitions = new Map();
4906+
}
4907+
// Reset the pair in case we didn't end up restoring the instance in previous commits.
4908+
// This shouldn't really happen anymore but just in case. We could maybe add an invariant.
4909+
const instance: ViewTransitionState = fiber.stateNode;
4910+
instance.paired = null;
4911+
appearingViewTransitions.set(name, instance);
4912+
}
4913+
}
4914+
recursivelyAccumulateSuspenseyCommit(fiber);
4915+
break;
4916+
}
4917+
// Fallthrough
4918+
}
48964919
default: {
48974920
recursivelyAccumulateSuspenseyCommit(fiber);
48984921
}

packages/react-reconciler/src/ReactFiberCompleteWork.js

Lines changed: 0 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -28,10 +28,6 @@ import type {
2828
OffscreenState,
2929
OffscreenQueue,
3030
} from './ReactFiberActivityComponent';
31-
import type {
32-
ViewTransitionProps,
33-
ViewTransitionState,
34-
} from './ReactFiberViewTransitionComponent';
3531
import {isOffscreenManual} from './ReactFiberActivityComponent';
3632
import type {TracingMarkerInstance} from './ReactFiberTracingMarkerComponent';
3733
import type {Cache} from './ReactFiberCacheComponent';
@@ -99,7 +95,6 @@ import {
9995
ShouldSuspendCommit,
10096
Cloned,
10197
ViewTransitionStatic,
102-
ViewTransitionNamedStatic,
10398
} from './ReactFiberFlags';
10499

105100
import {
@@ -164,7 +159,6 @@ import {
164159
getWorkInProgressTransitions,
165160
shouldRemainOnPreviousScreen,
166161
markSpawnedRetryLane,
167-
trackAppearingViewTransition,
168162
} from './ReactFiberWorkLoop';
169163
import {
170164
OffscreenLane,
@@ -947,34 +941,6 @@ function completeDehydratedSuspenseBoundary(
947941
}
948942
}
949943

950-
function trackReappearingViewTransitions(workInProgress: Fiber): void {
951-
if ((workInProgress.subtreeFlags & ViewTransitionNamedStatic) === NoFlags) {
952-
// This has no named view transitions in its subtree.
953-
return;
954-
}
955-
// This needs to search for any explicitly named reappearing View Transitions,
956-
// whether they were updated in this transition or unchanged from before.
957-
let child = workInProgress.child;
958-
while (child !== null) {
959-
if (child.tag === OffscreenComponent && child.memoizedState === null) {
960-
// This tree is currently hidden so we skip it.
961-
} else {
962-
if (
963-
child.tag === ViewTransitionComponent &&
964-
(child.flags & ViewTransitionNamedStatic) !== NoFlags
965-
) {
966-
const props: ViewTransitionProps = child.memoizedProps;
967-
if (props.name != null && props.name !== 'auto') {
968-
const instance: ViewTransitionState = child.stateNode;
969-
trackAppearingViewTransition(instance, props.name);
970-
}
971-
}
972-
trackReappearingViewTransitions(child);
973-
}
974-
child = child.sibling;
975-
}
976-
}
977-
978944
function completeWork(
979945
current: Fiber | null,
980946
workInProgress: Fiber,
@@ -1796,14 +1762,6 @@ function completeWork(
17961762
const prevIsHidden = prevState !== null;
17971763
if (prevIsHidden !== nextIsHidden) {
17981764
workInProgress.flags |= Visibility;
1799-
if (enableViewTransition && !nextIsHidden) {
1800-
// If we're revealing a new tree, we need to find any named
1801-
// ViewTransitions inside it that might have a deleted pair.
1802-
// We do this in the complete phase in case the tree has
1803-
// changed during the reveal but we have to do it before we
1804-
// find the first deleted pair in the before mutation phase.
1805-
trackReappearingViewTransitions(workInProgress);
1806-
}
18071765
}
18081766
} else {
18091767
// On initial mount, we only need a Visibility effect if the tree

packages/react-reconciler/src/ReactFiberFlags.js

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,7 @@ export const StoreConsistency = /* */ 0b0000000000000000100000000000
4444
// possible, because we're about to run out of bits.
4545
export const ScheduleRetry = StoreConsistency;
4646
export const ShouldSuspendCommit = Visibility;
47+
export const ViewTransitionNamedMount = ShouldSuspendCommit;
4748
export const DidDefer = ContentReset;
4849
export const FormReset = Snapshot;
4950
export const AffectedParentLayout = ContentReset;
@@ -74,8 +75,10 @@ export const PassiveStatic = /* */ 0b0000000100000000000000000000
7475
export const MaySuspendCommit = /* */ 0b0000001000000000000000000000000;
7576
// ViewTransitionNamedStatic tracks explicitly name ViewTransition components deeply
7677
// that might need to be visited during clean up. This is similar to SnapshotStatic
77-
// if there was any other use for it.
78-
export const ViewTransitionNamedStatic = /* */ SnapshotStatic;
78+
// if there was any other use for it. It also needs to run in the same phase as
79+
// MaySuspendCommit tracking.
80+
export const ViewTransitionNamedStatic =
81+
/* */ SnapshotStatic | MaySuspendCommit;
7982
// ViewTransitionStatic tracks whether there are an ViewTransition components from
8083
// the nearest HostComponent down. It resets at every HostComponent level.
8184
export const ViewTransitionStatic = /* */ 0b0000010000000000000000000000000;

0 commit comments

Comments
 (0)