Skip to content

Commit 1ecd99c

Browse files
authored
Temporarily Mount useInsertionEffect while a Gesture snapshot is being computed (#35565)
`useInsertionEffect` is meant to be used to insert `<style>` tags that affect the layout. It allows precomputing a layout before it mounts. Since we're not normally firing any effects during the "apply gesture" phase where we create the clones, it's possible for the target snapshot to be missing styles. This makes it so that `useInsertionEffect` for a new tree are mounted before the snapshot is taken and then unmounted before the animation starts. Note that because we are mounting a clone of the DOM tree and the previous DOM tree remains mounted during the snapshot, we can't unmount any previous insertion effects. This can lead to conflicts but that is similar to what can happen with conflicts for two mounted Activity boundaries since insertion effects can remain mounted inside those. A revealed Activity will have already had their insertion effects fired while offscreen. However, one thing this doesn't yet do is handle the case where a `useInsertionEffect` is *updated* as part of a gesture being applied. This means it's still possible for it to miss some styles in that case. The interesting thing there is that since the old state and the new state will both be applicable to the global DOM in this phase, what should really happen is that we should mount the new updated state without unmounting the old state and then unmount the updated state. Meaning you can have the same hook in the mounted state twice at the same time.
1 parent c55ffb5 commit 1ecd99c

File tree

3 files changed

+112
-25
lines changed

3 files changed

+112
-25
lines changed

fixtures/view-transition/src/components/Page.css

Lines changed: 0 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,3 @@
1-
.roboto-font {
2-
font-family: "Roboto", serif;
3-
font-optical-sizing: auto;
4-
font-weight: 100;
5-
font-style: normal;
6-
font-variation-settings:
7-
"wdth" 100;
8-
}
9-
101
.swipe-recognizer {
112
width: 300px;
123
background: #eee;

fixtures/view-transition/src/components/Page.js

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import React, {
44
Activity,
55
useLayoutEffect,
66
useEffect,
7+
useInsertionEffect,
78
useState,
89
useId,
910
useOptimistic,
@@ -41,6 +42,26 @@ const b = (
4142
);
4243

4344
function Component() {
45+
// Test inserting fonts with style tags using useInsertionEffect. This is not recommended but
46+
// used to test that gestures etc works with useInsertionEffect so that stylesheet based
47+
// libraries can be properly supported.
48+
useInsertionEffect(() => {
49+
const style = document.createElement('style');
50+
style.textContent = `
51+
.roboto-font {
52+
font-family: "Roboto", serif;
53+
font-optical-sizing: auto;
54+
font-weight: 100;
55+
font-style: normal;
56+
font-variation-settings:
57+
"wdth" 100;
58+
}
59+
`;
60+
document.head.appendChild(style);
61+
return () => {
62+
document.head.removeChild(style);
63+
};
64+
}, []);
4465
return (
4566
<ViewTransition
4667
default={

packages/react-reconciler/src/ReactFiberApplyGesture.js

Lines changed: 91 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,7 @@ import {
4343
} from './ReactFiberMutationTracking';
4444
import {
4545
MutationMask,
46+
Placement,
4647
Update,
4748
ContentReset,
4849
NoFlags,
@@ -52,6 +53,14 @@ import {
5253
AffectedParentLayout,
5354
} from './ReactFiberFlags';
5455
import {
56+
HasEffect as HookHasEffect,
57+
Insertion as HookInsertion,
58+
} from './ReactHookEffectTags';
59+
import {
60+
FunctionComponent,
61+
ForwardRef,
62+
MemoComponent,
63+
SimpleMemoComponent,
5564
HostComponent,
5665
HostHoistable,
5766
HostSingleton,
@@ -72,6 +81,10 @@ import {
7281
pushViewTransitionCancelableScope,
7382
popViewTransitionCancelableScope,
7483
} from './ReactFiberCommitViewTransitions';
84+
import {
85+
commitHookEffectListMount,
86+
commitHookEffectListUnmount,
87+
} from './ReactFiberCommitEffects';
7588
import {
7689
getViewTransitionName,
7790
getViewTransitionClassName,
@@ -378,9 +391,10 @@ function recursivelyInsertNew(
378391
if (
379392
visitPhase === INSERT_APPEARING_PAIR &&
380393
parentViewTransition === null &&
381-
(parentFiber.subtreeFlags & ViewTransitionNamedStatic) === NoFlags
394+
(parentFiber.subtreeFlags & (ViewTransitionNamedStatic | Placement)) ===
395+
NoFlags
382396
) {
383-
// We're just searching for pairs but we have reached the end.
397+
// We're just searching for pairs or insertion effects but we have reached the end.
384398
return;
385399
}
386400
let child = parentFiber.child;
@@ -402,6 +416,28 @@ function recursivelyInsertNewFiber(
402416
visitPhase: VisitPhase,
403417
): void {
404418
switch (finishedWork.tag) {
419+
case FunctionComponent:
420+
case ForwardRef:
421+
case MemoComponent:
422+
case SimpleMemoComponent: {
423+
recursivelyInsertNew(
424+
finishedWork,
425+
hostParentClone,
426+
parentViewTransition,
427+
visitPhase,
428+
);
429+
if (finishedWork.flags & Update) {
430+
// Insertion Effects are mounted temporarily during the rendering of the snapshot.
431+
// This does not affect cloned Offscreen content since those would've been mounted
432+
// while inside the offscreen tree already.
433+
// Note that because we are mounting a clone of the DOM tree and the previous DOM
434+
// tree remains mounted during the snapshot, we can't unmount any previous insertion
435+
// effects. This can lead to conflicts but that is similar to what can happen with
436+
// conflicts for two mounted Activity boundaries.
437+
commitHookEffectListMount(HookInsertion | HookHasEffect, finishedWork);
438+
}
439+
break;
440+
}
405441
case HostHoistable: {
406442
if (supportsResources) {
407443
// TODO: Hoistables should get optimistically inserted and then removed.
@@ -1039,6 +1075,40 @@ function measureExitViewTransitions(placement: Fiber): void {
10391075
}
10401076
}
10411077

1078+
function recursivelyRestoreNew(
1079+
finishedWork: Fiber,
1080+
nearestMountedAncestor: Fiber,
1081+
): void {
1082+
// There has to be move a Placement AND an Update flag somewhere below for this
1083+
// pass to be relevant since we only apply insertion effects for new components here.
1084+
if (((Placement | Update) & finishedWork.subtreeFlags) !== NoFlags) {
1085+
let child = finishedWork.child;
1086+
while (child !== null) {
1087+
recursivelyRestoreNew(child, nearestMountedAncestor);
1088+
child = child.sibling;
1089+
}
1090+
}
1091+
switch (finishedWork.tag) {
1092+
case FunctionComponent:
1093+
case ForwardRef:
1094+
case MemoComponent:
1095+
case SimpleMemoComponent: {
1096+
const current = finishedWork.alternate;
1097+
if (current === null && finishedWork.flags & Update) {
1098+
// Insertion Effects are mounted temporarily during the rendering of the snapshot.
1099+
// We have now already takes a snapshot of the inserted state so we can now unmount
1100+
// them to get back into the original state before starting the animation.
1101+
commitHookEffectListUnmount(
1102+
HookInsertion | HookHasEffect,
1103+
finishedWork,
1104+
nearestMountedAncestor,
1105+
);
1106+
}
1107+
break;
1108+
}
1109+
}
1110+
}
1111+
10421112
function recursivelyApplyViewTransitions(parentFiber: Fiber) {
10431113
const deletions = parentFiber.deletions;
10441114
if (deletions !== null) {
@@ -1055,7 +1125,13 @@ function recursivelyApplyViewTransitions(parentFiber: Fiber) {
10551125
// If we have mutations or if this is a newly inserted tree, clone as we go.
10561126
let child = parentFiber.child;
10571127
while (child !== null) {
1058-
applyViewTransitionsOnFiber(child);
1128+
const current = child.alternate;
1129+
if (current === null) {
1130+
measureExitViewTransitions(child);
1131+
recursivelyRestoreNew(child, parentFiber);
1132+
} else {
1133+
applyViewTransitionsOnFiber(child, current);
1134+
}
10591135
child = child.sibling;
10601136
}
10611137
} else {
@@ -1066,14 +1142,7 @@ function recursivelyApplyViewTransitions(parentFiber: Fiber) {
10661142
}
10671143
}
10681144

1069-
function applyViewTransitionsOnFiber(finishedWork: Fiber) {
1070-
const current = finishedWork.alternate;
1071-
if (current === null) {
1072-
measureExitViewTransitions(finishedWork);
1073-
return;
1074-
}
1075-
1076-
const flags = finishedWork.flags;
1145+
function applyViewTransitionsOnFiber(finishedWork: Fiber, current: Fiber) {
10771146
// The effect flag should be checked *after* we refine the type of fiber,
10781147
// because the fiber tag is more specific. An exception is any flag related
10791148
// to reconciliation, because those can be set on all fiber types.
@@ -1083,12 +1152,18 @@ function applyViewTransitionsOnFiber(finishedWork: Fiber) {
10831152
break;
10841153
}
10851154
case OffscreenComponent: {
1086-
if (flags & Visibility) {
1087-
const newState: OffscreenState | null = finishedWork.memoizedState;
1088-
const isHidden = newState !== null;
1089-
if (!isHidden) {
1155+
const newState: OffscreenState | null = finishedWork.memoizedState;
1156+
const isHidden = newState !== null;
1157+
const wasHidden = current.memoizedState !== null;
1158+
if (!isHidden) {
1159+
if (wasHidden) {
10901160
measureExitViewTransitions(finishedWork);
1091-
} else if (current !== null && current.memoizedState === null) {
1161+
recursivelyRestoreNew(finishedWork, finishedWork);
1162+
} else {
1163+
recursivelyApplyViewTransitions(finishedWork);
1164+
}
1165+
} else {
1166+
if (!wasHidden) {
10921167
// Was previously mounted as visible but is now hidden.
10931168
commitEnterViewTransitions(current, true);
10941169
}

0 commit comments

Comments
 (0)