@samitouri / QOS-React / commits / c4a3b92e09

Add more phases to the ReactFiberApplyGesture (#32578)

Stacked on #32585 and #32605. This adds more loops for the phases of "Apply Gesture". It doesn't implement the interesting bit yet like adding view-transition-names and measurements. I'll do that in a separate PR to keep reviewing easier. The three phases of this approach is roughly: - Clone and apply names to the "old" state. - Inside startViewTransition: Apply names to the "new" state. Measure both the "old" and "new" state to know whether to cancel some of them. Delete the clones which will include all the "old" names. - After startViewTransition: Restore "new" names back to no view-transition-name. Since we don't have any other Effects in these phases we have a bit more flexibility and we can avoid extra phases that traverse the tree. I've tried to avoid any additional passes. An interesting consequence of this approach is that we could measure both the "old" and "new" state before `startViewTransition`. This would be more efficient because we wouldn't need to take View Transition snapshots of parts of the tree that won't actually animate. However, that would require an extra pass and force layout earlier. It would also have different semantics from the fire-and-forget View Transitions because we could optimize better which can be visible. It would also not account for any late mutations. So I decided to instead let the layout be computed by painting as usual and then measure both "old" and "new" inside the startViewTransition instead. Then canceling anything that doesn't animate to keep it consistent. Unfortunately, though there's not a lot of code sharing possible in these phases because the strategy is so different with the cloning and because the animation is performed in reverse. The "finishedWork" Fiber represents the "old" state and the "current" Fiber represents the "new" state. The most complicated phase is the cloning. I actually ended up having to make a very different pattern from the other phases and CommitWork in general. Because we have to clone as we go and also do other things like apply names and finding pairs, it has more phases. I ended up with an approach that uses three different loops. The outer one for updated trees, one for inserted trees that don't need cloning (doesn't include reappearing offscreen) and one for not updated trees that still need cloning. Inside each loop it can also be in different phases which I track with the `visitPhase` enum - this pattern is kind of new. Additionally, we need to measure the cloned nodes after we've applied mutations to them and we have to wait until the whole tree is inserted. We don't have a reference to these DOM elements in the Fiber tree since that still refers to the original ones. We need to store the cloned elements somewhere. So I added a temporary field on the ViewTransitionState to keep track of any clones owned by that ViewTransition. When we deep clone an unchanged subtree we don't have DOM element instances. It wouldn't be quite safe to try to find them from the tree structure. So we need to avoid the deep clones if we might need DOM elements. Therefore we keep traversing in the case where we need to find nested ViewTransition boundaries that are either potentially affected by layout or a "pair". For the other two phases the pattern there's a lot of code duplication since it's slightly different from the commit ones but they at least follow the same pattern. For the restore phase I was actually able to reuse most of the code. I don't love how much code this is.

Sebastian Markbåge committed Mar 14, 2025 at 13:20 UTC c4a3b92e098cf1896939758e7419cbdb0e2f0cf4
3 files changed +722 -113
packages/react-reconciler/src/ReactFiber.js
+1
@@ -878,6 +878,7 @@ export function createFiberFromViewTransition(
878 const instance: ViewTransitionState = {
879 autoName: null,
880 paired: null,
881 + clones: null,
882 ref: null,
883 };
884 fiber.stateNode = instance;
packages/react-reconciler/src/ReactFiberApplyGesture.js
+719 -112
@@ -13,6 +13,11 @@ import type {Instance, TextInstance} from './ReactFiberConfig';
13
14 import type {OffscreenState} from './ReactFiberActivityComponent';
15
16 +import type {
17 + ViewTransitionState,
18 + ViewTransitionProps,
19 +} from './ReactFiberViewTransitionComponent';
20 +
21 import {
22 cloneMutableInstance,
23 cloneMutableTextInstance,
@@ -40,6 +45,8 @@ import {
45 ContentReset,
46 NoFlags,
47 Visibility,
48 + ViewTransitionNamedStatic,
49 + ViewTransitionStatic,
50 } from './ReactFiberFlags';
51 import {
52 HostComponent,
@@ -50,6 +57,10 @@ import {
57 OffscreenComponent,
58 ViewTransitionComponent,
59 } from './ReactWorkTags';
60 +import {
61 + restoreEnterOrExitViewTransitions,
62 + restoreNestedViewTransitions,
63 +} from './ReactFiberCommitViewTransitions';
64
65 let didWarnForRootClone = false;
66
@@ -57,26 +68,284 @@ function detectMutationOrInsertClones(finishedWork: Fiber): boolean {
68 return true;
69 }
70
60 -let unhideHostChildren = false;
71 +const CLONE_UPDATE = 0; // Mutations in this subtree or potentially affected by layout.
72 +const CLONE_EXIT = 1; // Inside a reappearing offscreen before the next ViewTransition or HostComponent.
73 +const CLONE_UNHIDE = 2; // Inside a reappearing offscreen before the next HostComponent.
74 +const CLONE_APPEARING_PAIR = 3; // Like UNHIDE but we're already inside the first Host Component only finding pairs.
75 +const CLONE_UNCHANGED = 4; // Nothing in this tree was changed but we're still walking to clone it.
76 +const INSERT_EXIT = 5; // Inside a newly mounted tree before the next ViewTransition or HostComponent.
77 +const INSERT_APPEND = 6; // Inside a newly mounted tree before the next HostComponent.
78 +const INSERT_APPEARING_PAIR = 7; // Inside a newly mounted tree only finding pairs.
79 +type VisitPhase = 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7;
80 +
81 +function trackDeletedPairViewTransitions(deletion: Fiber): void {
82 + if ((deletion.subtreeFlags & ViewTransitionNamedStatic) === NoFlags) {
83 + // This has no named view transitions in its subtree.
84 + return;
85 + }
86 + let child = deletion.child;
87 + while (child !== null) {
88 + if (child.tag === OffscreenComponent && child.memoizedState === null) {
89 + // This tree was already hidden so we skip it.
90 + } else {
91 + if (
92 + child.tag === ViewTransitionComponent &&
93 + (child.flags & ViewTransitionNamedStatic) !== NoFlags
94 + ) {
95 + const props: ViewTransitionProps = child.memoizedProps;
96 + const name = props.name;
97 + if (name != null && name !== 'auto') {
98 + // TODO: Find a pair
99 + }
100 + }
101 + trackDeletedPairViewTransitions(child);
102 + }
103 + child = child.sibling;
104 + }
105 +}
106 +
107 +function trackEnterViewTransitions(deletion: Fiber): void {
108 + if (deletion.tag === ViewTransitionComponent) {
109 + const props: ViewTransitionProps = deletion.memoizedProps;
110 + const name = props.name;
111 + if (name != null && name !== 'auto') {
112 + // TODO: Find a pair
113 + }
114 + // Look for more pairs deeper in the tree.
115 + trackDeletedPairViewTransitions(deletion);
116 + } else if ((deletion.subtreeFlags & ViewTransitionStatic) !== NoFlags) {
117 + let child = deletion.child;
118 + while (child !== null) {
119 + trackEnterViewTransitions(child);
120 + child = child.sibling;
121 + }
122 + } else {
123 + trackDeletedPairViewTransitions(deletion);
124 + }
125 +}
126 +
127 +function recursivelyInsertNew(
128 + parentFiber: Fiber,
129 + hostParentClone: Instance,
130 + parentViewTransition: null | ViewTransitionState,
131 + visitPhase: VisitPhase,
132 +): void {
133 + if (
134 + visitPhase === INSERT_APPEARING_PAIR &&
135 + parentViewTransition === null &&
136 + (parentFiber.subtreeFlags & ViewTransitionNamedStatic) === NoFlags
137 + ) {
138 + // We're just searching for pairs but we have reached the end.
139 + return;
140 + }
141 + let child = parentFiber.child;
142 + while (child !== null) {
143 + recursivelyInsertNewFiber(
144 + child,
145 + hostParentClone,
146 + parentViewTransition,
147 + visitPhase,
148 + );
149 + child = child.sibling;
150 + }
151 +}
152 +
153 +function recursivelyInsertNewFiber(
154 + finishedWork: Fiber,
155 + hostParentClone: Instance,
156 + parentViewTransition: null | ViewTransitionState,
157 + visitPhase: VisitPhase,
158 +): void {
159 + switch (finishedWork.tag) {
160 + case HostHoistable: {
161 + if (supportsResources) {
162 + // TODO: Hoistables should get optimistically inserted and then removed.
163 + recursivelyInsertNew(
164 + finishedWork,
165 + hostParentClone,
166 + parentViewTransition,
167 + visitPhase,
168 + );
169 + break;
170 + }
171 + // Fall through
172 + }
173 + case HostSingleton: {
174 + if (supportsSingletons) {
175 + recursivelyInsertNew(
176 + finishedWork,
177 + hostParentClone,
178 + parentViewTransition,
179 + visitPhase,
180 + );
181 +
182 + if (__DEV__) {
183 + // We cannot apply mutations to Host Singletons since by definition
184 + // they cannot be cloned. Therefore we warn in DEV if this commit
185 + // had any effect.
186 + if (finishedWork.flags & Update) {
187 + console.error(
188 + 'useSwipeTransition() caused something to render a new <%s>. ' +
189 + 'This is not possible in the current implementation. ' +
190 + "Make sure that the swipe doesn't mount any new <%s> elements.",
191 + finishedWork.type,
192 + finishedWork.type,
193 + );
194 + }
195 + }
196 + break;
197 + }
198 + // Fall through
199 + }
200 + case HostComponent: {
201 + const instance: Instance = finishedWork.stateNode;
202 + // For insertions we don't need to clone. It's already new state node.
203 + if (visitPhase !== INSERT_APPEARING_PAIR) {
204 + appendChild(hostParentClone, instance);
205 + recursivelyInsertNew(
206 + finishedWork,
207 + instance,
208 + null,
209 + INSERT_APPEARING_PAIR,
210 + );
211 + } else {
212 + recursivelyInsertNew(finishedWork, instance, null, visitPhase);
213 + }
214 + if (parentViewTransition !== null) {
215 + if (parentViewTransition.clones === null) {
216 + parentViewTransition.clones = [instance];
217 + } else {
218 + parentViewTransition.clones.push(instance);
219 + }
220 + }
221 + break;
222 + }
223 + case HostText: {
224 + const textInstance: TextInstance = finishedWork.stateNode;
225 + if (textInstance === null) {
226 + throw new Error(
227 + 'This should have a text node initialized. This error is likely ' +
228 + 'caused by a bug in React. Please file an issue.',
229 + );
230 + }
231 + // For insertions we don't need to clone. It's already new state node.
232 + if (visitPhase !== INSERT_APPEARING_PAIR) {
233 + appendChild(hostParentClone, textInstance);
234 + }
235 + break;
236 + }
237 + case HostPortal: {
238 + // TODO: Consider what should happen to Portals. For now we exclude them.
239 + break;
240 + }
241 + case OffscreenComponent: {
242 + const newState: OffscreenState | null = finishedWork.memoizedState;
243 + const isHidden = newState !== null;
244 + if (!isHidden) {
245 + // Only insert nodes if this tree is going to be visible. No need to
246 + // insert invisible content.
247 + // Since there was no mutation to this node, it couldn't have changed
248 + // visibility so we don't need to update visitPhase here.
249 + recursivelyInsertNew(
250 + finishedWork,
251 + hostParentClone,
252 + parentViewTransition,
253 + visitPhase,
254 + );
255 + }
256 + break;
257 + }
258 + case ViewTransitionComponent:
259 + const prevMutationContext = pushMutationContext();
260 + const viewTransitionState: ViewTransitionState = finishedWork.stateNode;
261 + // TODO: If this was already cloned by a previous pass we can reuse those clones.
262 + viewTransitionState.clones = null;
263 + let nextPhase;
264 + if (visitPhase === INSERT_EXIT) {
265 + // This was an Enter of a ViewTransition. We now move onto inserting the inner
266 + // HostComponents and finding inner pairs.
267 + nextPhase = INSERT_APPEND;
268 + // TODO: Mark the name and find a pair.
269 + } else {
270 + nextPhase = visitPhase;
271 + }
272 + recursivelyInsertNew(
273 + finishedWork,
274 + hostParentClone,
275 + viewTransitionState,
276 + nextPhase,
277 + );
278 + popMutationContext(prevMutationContext);
279 + break;
280 + default: {
281 + recursivelyInsertNew(
282 + finishedWork,
283 + hostParentClone,
284 + parentViewTransition,
285 + visitPhase,
286 + );
287 + break;
288 + }
289 + }
290 +}
291
292 function recursivelyInsertClonesFromExistingTree(
293 parentFiber: Fiber,
294 hostParentClone: Instance,
295 + parentViewTransition: null | ViewTransitionState,
296 + visitPhase: VisitPhase,
297 ): void {
298 let child = parentFiber.child;
299 while (child !== null) {
300 switch (child.tag) {
301 case HostComponent: {
302 const instance: Instance = child.stateNode;
71 - // If we have no mutations in this subtree, we just need to make a deep clone.
72 - const clone: Instance = cloneMutableInstance(instance, true);
303 + let nextPhase: VisitPhase;
304 + switch (visitPhase) {
305 + case CLONE_EXIT:
306 + case CLONE_UNHIDE:
307 + case CLONE_APPEARING_PAIR:
308 + // If this was an unhide, we need to keep going if there are any named
309 + // pairs in this subtree, since they might need to be marked.
310 + nextPhase =
311 + (child.subtreeFlags & ViewTransitionNamedStatic) !== NoFlags
312 + ? CLONE_APPEARING_PAIR
313 + : CLONE_UNCHANGED;
314 + break;
315 + default:
316 + // We've found any "layout" View Transitions at this point so we can bail.
317 + nextPhase = CLONE_UNCHANGED;
318 + }
319 + let clone: Instance;
320 + if (nextPhase !== CLONE_UNCHANGED) {
321 + // We might need a handle on these clones, so we need to do a shallow clone
322 + // and keep going.
323 + clone = cloneMutableInstance(instance, false);
324 + recursivelyInsertClonesFromExistingTree(
325 + child,
326 + clone,
327 + null,
328 + nextPhase,
329 + );
330 + } else {
331 + // If we have no mutations in this subtree, and we don't need a handle on the
332 + // clones, then we can do a deep clone instead and bailout.
333 + clone = cloneMutableInstance(instance, true);
334 + // TODO: We may need to transfer some DOM state such as scroll position
335 + // for the deep clones.
336 + // TODO: If there's a manual view-transition-name inside the clone we
337 + // should ideally remove it from the original and then restore it in mutation
338 + // phase. Otherwise it leads to duplicate names.
339 + }
340 appendChild(hostParentClone, clone);
74 - // TODO: We may need to transfer some DOM state such as scroll position
75 - // for the deep clones.
76 - // TODO: If there's a manual view-transition-name inside the clone we
77 - // should ideally remove it from the original and then restore it in mutation
78 - // phase. Otherwise it leads to duplicate names.
79 - if (unhideHostChildren) {
341 + if (parentViewTransition !== null) {
342 + if (parentViewTransition.clones === null) {
343 + parentViewTransition.clones = [clone];
344 + } else {
345 + parentViewTransition.clones.push(clone);
346 + }
347 + }
348 + if (visitPhase === CLONE_EXIT || visitPhase === CLONE_UNHIDE) {
349 unhideInstance(clone, child.memoizedProps);
350 }
351 break;
@@ -91,7 +360,7 @@ function recursivelyInsertClonesFromExistingTree(
360 }
361 const clone = cloneMutableTextInstance(textInstance);
362 appendChild(hostParentClone, clone);
94 - if (unhideHostChildren) {
363 + if (visitPhase === CLONE_EXIT || visitPhase === CLONE_UNHIDE) {
364 unhideTextInstance(clone, child.memoizedProps);
365 }
366 break;
@@ -108,21 +377,52 @@ function recursivelyInsertClonesFromExistingTree(
377 // clone invisible content.
378 // TODO: If this is visible but detached it should still be cloned.
379 // Since there was no mutation to this node, it couldn't have changed
111 - // visibility so we don't need to update unhideHostChildren here.
112 - recursivelyInsertClonesFromExistingTree(child, hostParentClone);
380 + // visibility so we don't need to update visitPhase here.
381 + recursivelyInsertClonesFromExistingTree(
382 + child,
383 + hostParentClone,
384 + parentViewTransition,
385 + visitPhase,
386 + );
387 }
388 break;
389 }
390 case ViewTransitionComponent:
391 const prevMutationContext = pushMutationContext();
392 + const viewTransitionState: ViewTransitionState = child.stateNode;
393 // TODO: If this was already cloned by a previous pass we can reuse those clones.
119 - recursivelyInsertClonesFromExistingTree(child, hostParentClone);
120 - // TODO: Do we need to track whether this should have a name applied?
394 + viewTransitionState.clones = null;
395 + let nextPhase;
396 + if (visitPhase === CLONE_EXIT) {
397 + // This was an Enter of a ViewTransition. We now move onto unhiding the inner
398 + // HostComponents and finding inner pairs.
399 + nextPhase = CLONE_UNHIDE;
400 + // TODO: Mark the name and find a pair.
401 + } else if (visitPhase === CLONE_UPDATE) {
402 + // If the tree had no mutations and we've found the top most ViewTransition
403 + // then this is the one we might apply the "layout" state too if it has changed
404 + // position. After we've found its HostComponents we can bail out.
405 + nextPhase = CLONE_UNCHANGED;
406 + } else {
407 + nextPhase = visitPhase;
408 + }
409 + recursivelyInsertClonesFromExistingTree(
410 + child,
411 + hostParentClone,
412 + viewTransitionState,
413 + nextPhase,
414 + );
415 + // TODO: Only the first level should track if this was s
416 // child.flags |= Update;
417 popMutationContext(prevMutationContext);
418 break;
419 default: {
125 - recursivelyInsertClonesFromExistingTree(child, hostParentClone);
420 + recursivelyInsertClonesFromExistingTree(
421 + child,
422 + hostParentClone,
423 + parentViewTransition,
424 + visitPhase,
425 + );
426 break;
427 }
428 }
@@ -133,12 +433,14 @@ function recursivelyInsertClonesFromExistingTree(
433 function recursivelyInsertClones(
434 parentFiber: Fiber,
435 hostParentClone: Instance,
436 + parentViewTransition: null | ViewTransitionState,
437 + visitPhase: VisitPhase,
438 ) {
439 const deletions = parentFiber.deletions;
440 if (deletions !== null) {
441 for (let i = 0; i < deletions.length; i++) {
140 - // const childToDelete = deletions[i];
141 - // TODO
442 + const childToDelete = deletions[i];
443 + trackEnterViewTransitions(childToDelete);
444 }
445 }
446
@@ -149,21 +451,45 @@ function recursivelyInsertClones(
451 // If we have mutations or if this is a newly inserted tree, clone as we go.
452 let child = parentFiber.child;
453 while (child !== null) {
152 - insertDestinationClonesOfFiber(child, hostParentClone);
454 + insertDestinationClonesOfFiber(
455 + child,
456 + hostParentClone,
457 + parentViewTransition,
458 + visitPhase,
459 + );
460 child = child.sibling;
461 }
462 } else {
463 // Once we reach a subtree with no more mutations we can bail out.
464 // However, we must still insert deep clones of the HostComponents.
158 - recursivelyInsertClonesFromExistingTree(parentFiber, hostParentClone);
465 + recursivelyInsertClonesFromExistingTree(
466 + parentFiber,
467 + hostParentClone,
468 + parentViewTransition,
469 + visitPhase,
470 + );
471 }
472 }
473
474 function insertDestinationClonesOfFiber(
475 finishedWork: Fiber,
476 hostParentClone: Instance,
477 + parentViewTransition: null | ViewTransitionState,
478 + visitPhase: VisitPhase,
479 ) {
480 const current = finishedWork.alternate;
481 + if (current === null) {
482 + // This is a newly mounted subtree. Insert any HostComponents and trigger
483 + // Enter transitions.
484 + recursivelyInsertNewFiber(
485 + finishedWork,
486 + hostParentClone,
487 + parentViewTransition,
488 + INSERT_EXIT,
489 + );
490 + return;
491 + }
492 +
493 const flags = finishedWork.flags;
494 // The effect flag should be checked *after* we refine the type of fiber,
495 // because the fiber tag is more specific. An exception is any flag related
@@ -172,55 +498,55 @@ function insertDestinationClonesOfFiber(
498 case HostHoistable: {
499 if (supportsResources) {
500 // TODO: Hoistables should get optimistically inserted and then removed.
175 - recursivelyInsertClones(finishedWork, hostParentClone);
501 + recursivelyInsertClones(
502 + finishedWork,
503 + hostParentClone,
504 + parentViewTransition,
505 + visitPhase,
506 + );
507 break;
508 }
509 // Fall through
510 }
511 case HostSingleton: {
512 if (supportsSingletons) {
182 - recursivelyInsertClones(finishedWork, hostParentClone);
513 + recursivelyInsertClones(
514 + finishedWork,
515 + hostParentClone,
516 + parentViewTransition,
517 + visitPhase,
518 + );
519 if (__DEV__) {
520 // We cannot apply mutations to Host Singletons since by definition
521 // they cannot be cloned. Therefore we warn in DEV if this commit
522 // had any effect.
523 if (flags & Update) {
188 - if (current === null) {
189 - console.error(
190 - 'useSwipeTransition() caused something to render a new <%s>. ' +
191 - 'This is not possible in the current implementation. ' +
192 - "Make sure that the swipe doesn't mount any new <%s> elements.",
193 - finishedWork.type,
194 - finishedWork.type,
195 - );
196 - } else {
197 - const newProps = finishedWork.memoizedProps;
198 - const oldProps = current.memoizedProps;
199 - const instance = finishedWork.stateNode;
200 - const type = finishedWork.type;
201 - const prev = pushMutationContext();
202 -
203 - try {
204 - // Since we currently don't have a separate diffing algorithm for
205 - // individual properties, the Update flag can be a false positive.
206 - // We have to apply the new props first o detect any mutations and
207 - // then revert them.
208 - commitUpdate(instance, type, oldProps, newProps, finishedWork);
209 - if (viewTransitionMutationContext) {
210 - console.error(
211 - 'useSwipeTransition() caused something to mutate <%s>. ' +
212 - 'This is not possible in the current implementation. ' +
213 - "Make sure that the swipe doesn't update any state which " +
214 - 'causes <%s> to change.',
215 - finishedWork.type,
216 - finishedWork.type,
217 - );
218 - }
219 - // Revert
220 - commitUpdate(instance, type, newProps, oldProps, finishedWork);
221 - } finally {
222 - popMutationContext(prev);
524 + const newProps = finishedWork.memoizedProps;
525 + const oldProps = current.memoizedProps;
526 + const instance = finishedWork.stateNode;
527 + const type = finishedWork.type;
528 + const prev = pushMutationContext();
529 +
530 + try {
531 + // Since we currently don't have a separate diffing algorithm for
532 + // individual properties, the Update flag can be a false positive.
533 + // We have to apply the new props first o detect any mutations and
534 + // then revert them.
535 + commitUpdate(instance, type, oldProps, newProps, finishedWork);
536 + if (viewTransitionMutationContext) {
537 + console.error(
538 + 'useSwipeTransition() caused something to mutate <%s>. ' +
539 + 'This is not possible in the current implementation. ' +
540 + "Make sure that the swipe doesn't update any state which " +
541 + 'causes <%s> to change.',
542 + finishedWork.type,
543 + finishedWork.type,
544 + );
545 }
546 + // Revert
547 + commitUpdate(instance, type, newProps, oldProps, finishedWork);
548 + } finally {
549 + popMutationContext(prev);
550 }
551 }
552 }
@@ -230,42 +556,46 @@ function insertDestinationClonesOfFiber(
556 }
557 case HostComponent: {
558 const instance: Instance = finishedWork.stateNode;
233 - if (current === null) {
234 - // For insertions we don't need to clone. It's already new state node.
235 - // TODO: Do we need to visit it for ViewTransitions though?
236 - appendChild(hostParentClone, instance);
237 - } else {
238 - let clone: Instance;
239 - if (finishedWork.child === null) {
240 - // This node is terminal. We still do a deep clone in case this has user
241 - // inserted content, text content or dangerouslySetInnerHTML.
242 - clone = cloneMutableInstance(instance, true);
243 - if (finishedWork.flags & ContentReset) {
244 - resetTextContent(clone);
245 - }
246 - } else {
247 - // If we have children we'll clone them as we walk the tree so we just
248 - // do a shallow clone here.
249 - clone = cloneMutableInstance(instance, false);
559 + let clone: Instance;
560 + if (finishedWork.child === null) {
561 + // This node is terminal. We still do a deep clone in case this has user
562 + // inserted content, text content or dangerouslySetInnerHTML.
563 + clone = cloneMutableInstance(instance, true);
564 + if (finishedWork.flags & ContentReset) {
565 + resetTextContent(clone);
566 }
567 + } else {
568 + // If we have children we'll clone them as we walk the tree so we just
569 + // do a shallow clone here.
570 + clone = cloneMutableInstance(instance, false);
571 + }
572
252 - if (flags & Update) {
253 - const newProps = finishedWork.memoizedProps;
254 - const oldProps = current.memoizedProps;
255 - const type = finishedWork.type;
256 - // Apply the delta to the clone.
257 - commitUpdate(clone, type, oldProps, newProps, finishedWork);
258 - }
573 + if (flags & Update) {
574 + const newProps = finishedWork.memoizedProps;
575 + const oldProps = current.memoizedProps;
576 + const type = finishedWork.type;
577 + // Apply the delta to the clone.
578 + commitUpdate(clone, type, oldProps, newProps, finishedWork);
579 + }
580
260 - if (unhideHostChildren) {
261 - unhideHostChildren = false;
262 - recursivelyInsertClones(finishedWork, clone);
263 - appendChild(hostParentClone, clone);
264 - unhideHostChildren = true;
265 - unhideInstance(clone, finishedWork.memoizedProps);
581 + if (visitPhase === CLONE_EXIT || visitPhase === CLONE_UNHIDE) {
582 + recursivelyInsertClones(
583 + finishedWork,
584 + clone,
585 + null,
586 + CLONE_APPEARING_PAIR,
587 + );
588 + appendChild(hostParentClone, clone);
589 + unhideInstance(clone, finishedWork.memoizedProps);
590 + } else {
591 + recursivelyInsertClones(finishedWork, clone, null, visitPhase);
592 + appendChild(hostParentClone, clone);
593 + }
594 + if (parentViewTransition !== null) {
595 + if (parentViewTransition.clones === null) {
596 + parentViewTransition.clones = [clone];
597 } else {
267 - recursivelyInsertClones(finishedWork, clone);
268 - appendChild(hostParentClone, clone);
598 + parentViewTransition.clones.push(clone);
599 }
600 }
601 break;
@@ -278,20 +608,15 @@ function insertDestinationClonesOfFiber(
608 'caused by a bug in React. Please file an issue.',
609 );
610 }
281 - if (current === null) {
282 - // For insertions we don't need to clone. It's already new state node.
283 - appendChild(hostParentClone, textInstance);
284 - } else {
285 - const clone = cloneMutableTextInstance(textInstance);
286 - if (flags & Update) {
287 - const newText: string = finishedWork.memoizedProps;
288 - const oldText: string = current.memoizedProps;
289 - commitTextUpdate(clone, newText, oldText);
290 - }
291 - appendChild(hostParentClone, clone);
292 - if (unhideHostChildren) {
293 - unhideTextInstance(clone, finishedWork.memoizedProps);
294 - }
611 + const clone = cloneMutableTextInstance(textInstance);
612 + if (flags & Update) {
613 + const newText: string = finishedWork.memoizedProps;
614 + const oldText: string = current.memoizedProps;
615 + commitTextUpdate(clone, newText, oldText);
616 + }
617 + appendChild(hostParentClone, clone);
618 + if (visitPhase === CLONE_EXIT || visitPhase === CLONE_UNHIDE) {
619 + unhideTextInstance(clone, finishedWork.memoizedProps);
620 }
621 break;
622 }
@@ -306,17 +631,45 @@ function insertDestinationClonesOfFiber(
631 // Only insert clones if this tree is going to be visible. No need to
632 // clone invisible content.
633 // TODO: If this is visible but detached it should still be cloned.
309 - const prevUnhide = unhideHostChildren;
310 - unhideHostChildren = prevUnhide || (flags & Visibility) !== NoFlags;
311 - recursivelyInsertClones(finishedWork, hostParentClone);
312 - unhideHostChildren = prevUnhide;
634 + let nextPhase;
635 + if (visitPhase === CLONE_UPDATE && (flags & Visibility) !== NoFlags) {
636 + // This is the root of an appear. We need to trigger Enter transitions.
637 + nextPhase = CLONE_EXIT;
638 + } else {
639 + nextPhase = visitPhase;
640 + }
641 + recursivelyInsertClones(
642 + finishedWork,
643 + hostParentClone,
644 + parentViewTransition,
645 + nextPhase,
646 + );
647 + } else if (current !== null && current.memoizedState === null) {
648 + // Was previously mounted as visible but is now hidden.
649 + trackEnterViewTransitions(current);
650 }
651 break;
652 }
653 case ViewTransitionComponent:
654 const prevMutationContext = pushMutationContext();
655 + const viewTransitionState: ViewTransitionState = finishedWork.stateNode;
656 // TODO: If this was already cloned by a previous pass we can reuse those clones.
319 - recursivelyInsertClones(finishedWork, hostParentClone);
657 + viewTransitionState.clones = null;
658 + let nextPhase;
659 + if (visitPhase === CLONE_EXIT) {
660 + // This was an Enter of a ViewTransition. We now move onto unhiding the inner
661 + // HostComponents and finding inner pairs.
662 + nextPhase = CLONE_UNHIDE;
663 + // TODO: Mark the name and find a pair.
664 + } else {
665 + nextPhase = visitPhase;
666 + }
667 + recursivelyInsertClones(
668 + finishedWork,
669 + hostParentClone,
670 + viewTransitionState,
671 + nextPhase,
672 + );
673 if (viewTransitionMutationContext) {
674 // Track that this boundary had a mutation and therefore needs to animate
675 // whether it resized or not.
@@ -325,7 +678,12 @@ function insertDestinationClonesOfFiber(
678 popMutationContext(prevMutationContext);
679 break;
680 default: {
328 - recursivelyInsertClones(finishedWork, hostParentClone);
681 + recursivelyInsertClones(
682 + finishedWork,
683 + hostParentClone,
684 + parentViewTransition,
685 + visitPhase,
686 + );
687 break;
688 }
689 }
@@ -337,7 +695,6 @@ export function insertDestinationClones(
695 root: FiberRoot,
696 finishedWork: Fiber,
697 ): void {
340 - unhideHostChildren = false;
698 // We'll either not transition the root, or we'll transition the clone. Regardless
699 // we cancel the root view transition name.
700 const needsClone = detectMutationOrInsertClones(finishedWork);
@@ -356,24 +713,274 @@ export function insertDestinationClones(
713 // Clone the whole root
714 const rootClone = cloneRootViewTransitionContainer(root.containerInfo);
715 root.gestureClone = rootClone;
359 - recursivelyInsertClones(finishedWork, rootClone);
716 + recursivelyInsertClones(finishedWork, rootClone, null, CLONE_UPDATE);
717 } else {
718 root.gestureClone = null;
719 cancelRootViewTransitionName(root.containerInfo);
720 }
721 }
722
723 +function applyDeletedPairViewTransitions(deletion: Fiber): void {
724 + if ((deletion.subtreeFlags & ViewTransitionNamedStatic) === NoFlags) {
725 + // This has no named view transitions in its subtree.
726 + return;
727 + }
728 + let child = deletion.child;
729 + while (child !== null) {
730 + if (child.tag === OffscreenComponent && child.memoizedState === null) {
731 + // This tree was already hidden so we skip it.
732 + } else {
733 + if (
734 + child.tag === ViewTransitionComponent &&
735 + (child.flags & ViewTransitionNamedStatic) !== NoFlags
736 + ) {
737 + const props: ViewTransitionProps = child.memoizedProps;
738 + const name = props.name;
739 + if (name != null && name !== 'auto') {
740 + // TODO: Find a pair
741 + }
742 + }
743 + applyDeletedPairViewTransitions(child);
744 + }
745 + child = child.sibling;
746 + }
747 +}
748 +
749 +function applyEnterViewTransitions(deletion: Fiber): void {
750 + if (deletion.tag === ViewTransitionComponent) {
751 + const props: ViewTransitionProps = deletion.memoizedProps;
752 + const name = props.name;
753 + if (name != null && name !== 'auto') {
754 + // TODO: Find a pair
755 + }
756 + // Look for more pairs deeper in the tree.
757 + applyDeletedPairViewTransitions(deletion);
758 + } else if ((deletion.subtreeFlags & ViewTransitionStatic) !== NoFlags) {
759 + // TODO: Check if this is a hidden Offscreen or a Portal.
760 + let child = deletion.child;
761 + while (child !== null) {
762 + applyEnterViewTransitions(child);
763 + child = child.sibling;
764 + }
765 + } else {
766 + applyDeletedPairViewTransitions(deletion);
767 + }
768 +}
769 +
770 +function measureExitViewTransitions(placement: Fiber): void {
771 + if (placement.tag === ViewTransitionComponent) {
772 + // const state: ViewTransitionState = placement.stateNode;
773 + const props: ViewTransitionProps = placement.memoizedProps;
774 + const name = props.name;
775 + if (name != null && name !== 'auto') {
776 + // TODO: Find a pair
777 + }
778 + } else if ((placement.subtreeFlags & ViewTransitionStatic) !== NoFlags) {
779 + // TODO: Check if this is a hidden Offscreen or a Portal.
780 + let child = placement.child;
781 + while (child !== null) {
782 + measureExitViewTransitions(child);
783 + child = child.sibling;
784 + }
785 + } else {
786 + // We don't need to find pairs here because we would've already found and
787 + // measured the pairs inside the deletion phase.
788 + }
789 +}
790 +
791 +function measureNestedViewTransitions(changedParent: Fiber): void {
792 + let child = changedParent.child;
793 + while (child !== null) {
794 + if (child.tag === ViewTransitionComponent) {
795 + const current = child.alternate;
796 + if (current !== null) {
797 + // const props: ViewTransitionProps = child.memoizedProps;
798 + // const name = getViewTransitionName(props, child.stateNode);
799 + // TODO: Measure both the old and new state and see if they're different.
800 + }
801 + } else if ((child.subtreeFlags & ViewTransitionStatic) !== NoFlags) {
802 + // TODO: Check if this is a hidden Offscreen or a Portal.
803 + measureNestedViewTransitions(child);
804 + }
805 + child = child.sibling;
806 + }
807 +}
808 +
809 +function measureUpdateViewTransition(
810 + current: Fiber,
811 + finishedWork: Fiber,
812 +): void {
813 + // TODO
814 +}
815 +
816 +function recursivelyApplyViewTransitions(parentFiber: Fiber) {
817 + const deletions = parentFiber.deletions;
818 + if (deletions !== null) {
819 + for (let i = 0; i < deletions.length; i++) {
820 + const childToDelete = deletions[i];
821 + applyEnterViewTransitions(childToDelete);
822 + }
823 + }
824 +
825 + if (
826 + parentFiber.alternate === null ||
827 + (parentFiber.subtreeFlags & MutationMask) !== NoFlags
828 + ) {
829 + // If we have mutations or if this is a newly inserted tree, clone as we go.
830 + let child = parentFiber.child;
831 + while (child !== null) {
832 + applyViewTransitionsOnFiber(child);
833 + child = child.sibling;
834 + }
835 + } else {
836 + // Nothing has changed in this subtree, but the parent may have still affected
837 + // its size and position. We need to measure the old and new state to see if
838 + // we should animate its size and position.
839 + measureNestedViewTransitions(parentFiber);
840 + }
841 +}
842 +
843 +function applyViewTransitionsOnFiber(finishedWork: Fiber) {
844 + const current = finishedWork.alternate;
845 + if (current === null) {
846 + measureExitViewTransitions(finishedWork);
847 + return;
848 + }
849 +
850 + const flags = finishedWork.flags;
851 + // The effect flag should be checked *after* we refine the type of fiber,
852 + // because the fiber tag is more specific. An exception is any flag related
853 + // to reconciliation, because those can be set on all fiber types.
854 + switch (finishedWork.tag) {
855 + case HostComponent: {
856 + // const instance: Instance = finishedWork.stateNode;
857 + // TODO: Apply name and measure.
858 + recursivelyApplyViewTransitions(finishedWork);
859 + break;
860 + }
861 + case HostText: {
862 + break;
863 + }
864 + case HostPortal: {
865 + // TODO: Consider what should happen to Portals. For now we exclude them.
866 + break;
867 + }
868 + case OffscreenComponent: {
869 + if (flags & Visibility) {
870 + const newState: OffscreenState | null = finishedWork.memoizedState;
871 + const isHidden = newState !== null;
872 + if (!isHidden) {
873 + measureExitViewTransitions(finishedWork);
874 + } else if (current !== null && current.memoizedState === null) {
875 + // Was previously mounted as visible but is now hidden.
876 + applyEnterViewTransitions(current);
877 + }
878 + }
879 + break;
880 + }
881 + case ViewTransitionComponent:
882 + measureUpdateViewTransition(current, finishedWork);
883 + const viewTransitionState: ViewTransitionState = finishedWork.stateNode;
884 + viewTransitionState.clones = null; // Reset
885 + recursivelyApplyViewTransitions(finishedWork);
886 + break;
887 + default: {
888 + recursivelyApplyViewTransitions(finishedWork);
889 + break;
890 + }
891 + }
892 +}
893 +
894 // Revert insertions and apply view transition names to the "new" (current) state.
895 export function applyDepartureTransitions(
896 root: FiberRoot,
897 finishedWork: Fiber,
898 ): void {
899 + // First measure and apply view-transition-names to the "new" states.
900 + recursivelyApplyViewTransitions(finishedWork);
901 + // Then remove the clones.
902 const rootClone = root.gestureClone;
903 if (rootClone !== null) {
904 root.gestureClone = null;
905 removeRootViewTransitionClone(root.containerInfo, rootClone);
906 }
376 - // TODO
907 +}
908 +
909 +function recursivelyRestoreViewTransitions(parentFiber: Fiber) {
910 + const deletions = parentFiber.deletions;
911 + if (deletions !== null) {
912 + for (let i = 0; i < deletions.length; i++) {
913 + const childToDelete = deletions[i];
914 + restoreEnterOrExitViewTransitions(childToDelete);
915 + }
916 + }
917 +
918 + if (
919 + parentFiber.alternate === null ||
920 + (parentFiber.subtreeFlags & MutationMask) !== NoFlags
921 + ) {
922 + // If we have mutations or if this is a newly inserted tree, clone as we go.
923 + let child = parentFiber.child;
924 + while (child !== null) {
925 + restoreViewTransitionsOnFiber(child);
926 + child = child.sibling;
927 + }
928 + } else {
929 + // Nothing has changed in this subtree, but the parent may have still affected
930 + // its size and position. We need to measure the old and new state to see if
931 + // we should animate its size and position.
932 + restoreNestedViewTransitions(parentFiber);
933 + }
934 +}
935 +
936 +function restoreViewTransitionsOnFiber(finishedWork: Fiber) {
937 + const current = finishedWork.alternate;
938 + if (current === null) {
939 + restoreEnterOrExitViewTransitions(finishedWork);
940 + return;
941 + }
942 +
943 + const flags = finishedWork.flags;
944 + // The effect flag should be checked *after* we refine the type of fiber,
945 + // because the fiber tag is more specific. An exception is any flag related
946 + // to reconciliation, because those can be set on all fiber types.
947 + switch (finishedWork.tag) {
948 + case HostComponent: {
949 + // const instance: Instance = finishedWork.stateNode;
950 + // TODO: Restore the name.
951 + recursivelyRestoreViewTransitions(finishedWork);
952 + break;
953 + }
954 + case HostText: {
955 + break;
956 + }
957 + case HostPortal: {
958 + // TODO: Consider what should happen to Portals. For now we exclude them.
959 + break;
960 + }
961 + case OffscreenComponent: {
962 + if (flags & Visibility) {
963 + const newState: OffscreenState | null = finishedWork.memoizedState;
964 + const isHidden = newState !== null;
965 + if (!isHidden) {
966 + restoreEnterOrExitViewTransitions(finishedWork);
967 + } else if (current !== null && current.memoizedState === null) {
968 + // Was previously mounted as visible but is now hidden.
969 + restoreEnterOrExitViewTransitions(current);
970 + }
971 + }
972 + break;
973 + }
974 + case ViewTransitionComponent:
975 + const viewTransitionState: ViewTransitionState = finishedWork.stateNode;
976 + viewTransitionState.clones = null; // Reset
977 + recursivelyRestoreViewTransitions(finishedWork);
978 + break;
979 + default: {
980 + recursivelyRestoreViewTransitions(finishedWork);
981 + break;
982 + }
983 + }
984 }
985
986 // Revert transition names and start/adjust animations on the started View Transition.
@@ -381,6 +988,6 @@ export function startGestureAnimations(
988 root: FiberRoot,
989 finishedWork: Fiber,
990 ): void {
384 - // TODO
991 + restoreViewTransitionsOnFiber(finishedWork);
992 restoreRootViewTransitionName(root.containerInfo);
993 }
packages/react-reconciler/src/ReactFiberViewTransitionComponent.js
+2 -1
@@ -9,7 +9,7 @@
9
10 import type {ReactNodeList} from 'shared/ReactTypes';
11 import type {FiberRoot} from './ReactInternalTypes';
12 -import type {ViewTransitionInstance} from './ReactFiberConfig';
12 +import type {ViewTransitionInstance, Instance} from './ReactFiberConfig';
13
14 import {
15 getWorkInProgressRoot,
@@ -45,6 +45,7 @@ export type ViewTransitionProps = {
45 export type ViewTransitionState = {
46 autoName: null | string, // the view-transition-name to use when an explicit one is not specified
47 paired: null | ViewTransitionState, // a temporary state during the commit phase if we have paired this with another instance
48 + clones: null | Array<Instance>, // a temporary state during the apply gesture phase if we cloned this boundary
49 ref: null | ViewTransitionInstance, // the current ref instance. This can change through the lifetime of the instance.
50 };
51