Use strict equality (===) to prevent type coercion bugs
i === 0
1/**2 * Copyright (c) Meta Platforms, Inc. and affiliates.3 *4 * This source code is licensed under the MIT license found in the5 * LICENSE file in the root directory of this source tree.6 *7 * @flow8 */910import type {ViewTransitionProps} from 'shared/ReactTypes';1112import type {Fiber, FiberRoot} from './ReactInternalTypes';1314import type {Instance, TextInstance, Props} from './ReactFiberConfig';1516import type {OffscreenState} from './ReactFiberOffscreenComponent';1718import type {ViewTransitionState} from './ReactFiberViewTransitionComponent';1920import {21 cloneMutableInstance,22 cloneMutableTextInstance,23 cloneRootViewTransitionContainer,24 removeRootViewTransitionClone,25 cancelRootViewTransitionName,26 restoreRootViewTransitionName,27 cancelViewTransitionName,28 applyViewTransitionName,29 appendChild,30 commitUpdate,31 commitTextUpdate,32 resetTextContent,33 supportsResources,34 supportsSingletons,35 unhideInstance,36 unhideTextInstance,37} from './ReactFiberConfig';38import {39 popMutationContext,40 pushMutationContext,41 viewTransitionMutationContext,42 trackHostMutation,43} from './ReactFiberMutationTracking';44import {45 MutationMask,46 Placement,47 Update,48 ContentReset,49 NoFlags,50 Visibility,51 ViewTransitionNamedStatic,52 ViewTransitionStatic,53 AffectedParentLayout,54} from './ReactFiberFlags';55import {56 HasEffect as HookHasEffect,57 Insertion as HookInsertion,58} from './ReactHookEffectTags';59import {60 FunctionComponent,61 ForwardRef,62 MemoComponent,63 SimpleMemoComponent,64 HostComponent,65 HostHoistable,66 HostSingleton,67 HostText,68 HostPortal,69 OffscreenComponent,70 ViewTransitionComponent,71} from './ReactWorkTags';72import {73 restoreEnterOrExitViewTransitions,74 restoreNestedViewTransitions,75 restoreUpdateViewTransitionForGesture,76 appearingViewTransitions,77 commitEnterViewTransitions,78 commitParentExitViewTransitions,79 measureNestedViewTransitions,80 measureUpdateViewTransition,81 viewTransitionCancelableChildren,82 pushViewTransitionCancelableScope,83 popViewTransitionCancelableScope,84} from './ReactFiberCommitViewTransitions';85import {86 commitHookEffectListMount,87 commitHookEffectListUnmount,88} from './ReactFiberCommitEffects';89import {90 getViewTransitionName,91 getViewTransitionClassName,92} from './ReactFiberViewTransitionComponent';9394import {95 enableProfilerTimer,96 enableComponentPerformanceTrack,97 enableViewTransitionParentEnterExit,98} from 'shared/ReactFeatureFlags';99import {trackAnimatingTask} from './ReactProfilerTimer';100import {scheduleGestureTransitionEvent} from './ReactFiberWorkLoop';101102// Used during the apply phase to track whether a parent ViewTransition component103// might have been affected by any mutations / relayouts below.104let viewTransitionContextChanged: boolean = false;105106function detectMutationOrInsertClones(finishedWork: Fiber): boolean {107 return true;108}109110const CLONE_UPDATE = 0; // Mutations in this subtree or potentially affected by layout.111const CLONE_EXIT = 1; // Inside a reappearing offscreen before the next ViewTransition or HostComponent.112const CLONE_UNHIDE = 2; // Inside a reappearing offscreen before the next HostComponent.113const CLONE_APPEARING_PAIR = 3; // Like UNHIDE but we're already inside the first Host Component only finding pairs.114const CLONE_UNCHANGED = 4; // Nothing in this tree was changed but we're still walking to clone it.115const INSERT_EXIT = 5; // Inside a newly mounted tree before the next ViewTransition or HostComponent.116const INSERT_APPEND = 6; // Inside a newly mounted tree before the next HostComponent.117const INSERT_APPEARING_PAIR = 7; // Inside a newly mounted tree only finding pairs.118type VisitPhase = 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7;119120function applyViewTransitionToClones(121 name: string,122 className: ?string,123 clones: Array<Instance>,124 fiber: Fiber,125): void {126 // This gets called when we have found a pair, but after the clone in created. The clone is127 // created by the insertion side. If the insertion side if found before the deletion side128 // then this is called by the deletion. If the deletion is visited first then this is called129 // later by the insertion when the clone has been created.130 for (let i = 0; i < clones.length; i++) {131 applyViewTransitionName(132 clones[i],133 i === 0134 ? name135 : // If we have multiple Host Instances below, we add a suffix to the name to give136 // each one a unique name.137 name + '_' + i,138 className,139 );140 }141 if (enableProfilerTimer && enableComponentPerformanceTrack) {142 if (fiber._debugTask != null) {143 trackAnimatingTask(fiber._debugTask);144 }145 }146}147148function trackDeletedPairViewTransitions(deletion: Fiber): void {149 if (150 appearingViewTransitions === null ||151 appearingViewTransitions.size === 0152 ) {153 // We've found all.154 return;155 }156 const pairs = appearingViewTransitions;157 if ((deletion.subtreeFlags & ViewTransitionNamedStatic) === NoFlags) {158 // This has no named view transitions in its subtree.159 return;160 }161 let child = deletion.child;162 while (child !== null) {163 if (child.tag === OffscreenComponent && child.memoizedState !== null) {164 // This tree was already hidden so we skip it.165 } else {166 if (167 child.tag === ViewTransitionComponent &&168 (child.flags & ViewTransitionNamedStatic) !== NoFlags169 ) {170 const props: ViewTransitionProps = child.memoizedProps;171 const name = props.name;172 if (name != null && name !== 'auto') {173 const pair = pairs.get(name);174 if (pair !== undefined) {175 // Delete the entry so that we know when we've found all of them176 // and can stop searching (size reaches zero).177 pairs.delete(name);178 const className: ?string = getViewTransitionClassName(179 props.default,180 props.share,181 );182 if (className !== 'none') {183 // TODO: Since the deleted instance already has layout we could184 // check if it's in the viewport and if not skip the pairing.185 // It would currently cause layout thrash though so if we did that186 // we need to avoid inserting the root of the cloned trees until187 // the end.188189 // The "old" instance is actually the one we're inserting.190 const oldInstance: ViewTransitionState = pair;191 // The "new" instance is the already mounted one we're deleting.192 const newInstance: ViewTransitionState = child.stateNode;193 oldInstance.paired = newInstance;194 newInstance.paired = oldInstance;195 const clones = oldInstance.clones;196 if (clones !== null) {197 // If we have clones that means that we've already visited this198 // ViewTransition boundary before and we can now apply the name199 // to those clones. Otherwise, we have to wait until we clone it.200 applyViewTransitionToClones(name, className, clones, child);201 }202 }203 if (pairs.size === 0) {204 break;205 }206 }207 }208 }209 trackDeletedPairViewTransitions(child);210 }211 child = child.sibling;212 }213}214215function trackEnterViewTransitions(deletion: Fiber): void {216 if (deletion.tag === ViewTransitionComponent) {217 const props: ViewTransitionProps = deletion.memoizedProps;218 const name = getViewTransitionName(props, deletion.stateNode);219 const pair =220 appearingViewTransitions !== null221 ? appearingViewTransitions.get(name)222 : undefined;223 const className: ?string = getViewTransitionClassName(224 props.default,225 pair !== undefined ? props.share : props.enter,226 );227 if (className !== 'none') {228 if (pair !== undefined) {229 // TODO: Since the deleted instance already has layout we could230 // check if it's in the viewport and if not skip the pairing.231 // It would currently cause layout thrash though so if we did that232 // we need to avoid inserting the root of the cloned trees until233 // the end.234235 // Delete the entry so that we know when we've found all of them236 // and can stop searching (size reaches zero).237 // $FlowFixMe[incompatible-use]: Refined by the pair.238 appearingViewTransitions.delete(name);239 // The "old" instance is actually the one we're inserting.240 const oldInstance: ViewTransitionState = pair;241 // The "new" instance is the already mounted one we're deleting.242 const newInstance: ViewTransitionState = deletion.stateNode;243 oldInstance.paired = newInstance;244 newInstance.paired = oldInstance;245 const clones = oldInstance.clones;246 if (clones !== null) {247 // If we have clones that means that we've already visited this248 // ViewTransition boundary before and we can now apply the name249 // to those clones. Otherwise, we have to wait until we clone it.250 applyViewTransitionToClones(name, className, clones, deletion);251 }252 }253 }254 // Look for more pairs deeper in the tree.255 trackDeletedPairViewTransitions(deletion);256 } else if ((deletion.subtreeFlags & ViewTransitionStatic) !== NoFlags) {257 let child = deletion.child;258 while (child !== null) {259 trackEnterViewTransitions(child);260 child = child.sibling;261 }262 } else {263 trackDeletedPairViewTransitions(deletion);264 }265}266267function applyAppearingPairViewTransition(child: Fiber): void {268 // Normally these helpers do recursive calls but since insertion/offscreen is forked269 // we call this helper from those loops instead. This must be called only on270 // ViewTransitionComponent that has already had their clones filled.271 if ((child.flags & ViewTransitionNamedStatic) !== NoFlags) {272 const state: ViewTransitionState = child.stateNode;273 // If this is not yet paired, it doesn't mean that we won't pair it later when274 // we find the deletion side. If that's the case then we'll add the names to275 // the clones then.276 if (state.paired) {277 const props: ViewTransitionProps = child.memoizedProps;278 if (props.name == null || props.name === 'auto') {279 throw new Error(280 'Found a pair with an auto name. This is a bug in React.',281 );282 }283 const name = props.name;284 // Note that this class name that doesn't actually really matter because the285 // "new" side will be the one that wins in practice.286 const className: ?string = getViewTransitionClassName(287 props.default,288 props.share,289 );290 if (className !== 'none') {291 const clones = state.clones;292 // If there are no clones at this point, that should mean that there are no293 // HostComponent children in this ViewTransition.294 if (clones !== null) {295 applyViewTransitionToClones(name, className, clones, child);296 }297 scheduleGestureTransitionEvent(child, props.onGestureShare);298 }299 }300 }301}302303function applyExitViewTransition(placement: Fiber): void {304 // Normally these helpers do recursive calls but since insertion/offscreen is forked305 // we call this helper from those loops instead. This must be called only on306 // ViewTransitionComponent that has already had their clones filled.307 const state: ViewTransitionState = placement.stateNode;308 const props: ViewTransitionProps = placement.memoizedProps;309 const name = getViewTransitionName(props, state);310 const className: ?string = getViewTransitionClassName(311 props.default,312 // Note that just because we don't have a pair yet doesn't mean we won't find one313 // later. However, that doesn't matter because if we do the class name that wins314 // is the one applied by the "new" side anyway.315 state.paired ? props.share : props.exit,316 );317 if (className !== 'none') {318 // TODO: Ideally we could determine if this exit is in the viewport and319 // exclude it otherwise but that would require waiting until we insert320 // and layout the clones first. Currently wait until the view transition321 // starts before reading the layout.322 const clones = state.clones;323 // If there are no clones at this point, that should mean that there are no324 // HostComponent children in this ViewTransition.325 if (clones !== null) {326 applyViewTransitionToClones(name, className, clones, placement);327 }328 if (state.paired) {329 scheduleGestureTransitionEvent(placement, props.onGestureShare);330 } else {331 scheduleGestureTransitionEvent(placement, props.onGestureExit);332 if (enableViewTransitionParentEnterExit) {333 commitParentExitViewTransitions(placement, true);334 }335 }336 }337}338339function applyNestedViewTransition(child: Fiber): void {340 const state: ViewTransitionState = child.stateNode;341 const props: ViewTransitionProps = child.memoizedProps;342 const name = getViewTransitionName(props, state);343 const className: ?string = getViewTransitionClassName(344 props.default,345 props.update,346 );347 if (className !== 'none') {348 const clones = state.clones;349 // If there are no clones at this point, that should mean that there are no350 // HostComponent children in this ViewTransition.351 if (clones !== null) {352 applyViewTransitionToClones(name, className, clones, child);353 }354 }355}356357function applyUpdateViewTransition(current: Fiber, finishedWork: Fiber): void {358 const state: ViewTransitionState = finishedWork.stateNode;359 // Updates can have conflicting names and classNames.360 // Since we're doing a reverse animation the "new" state is actually the current361 // and the "old" state is the finishedWork.362 const newProps: ViewTransitionProps = current.memoizedProps;363 const oldProps: ViewTransitionProps = finishedWork.memoizedProps;364 const oldName = getViewTransitionName(oldProps, state);365 // This className applies only if there are fewer child DOM nodes than366 // before or if this update should've been cancelled but we ended up with367 // a parent animating so we need to animate the child too. Otherwise368 // the "new" state wins. Since "new" normally wins, that's usually what369 // we would use. However, since this animation is going in reverse we actually370 // want the props from "current" since that's the class that would've won if371 // it was the normal direction. To preserve the same effect in either direction.372 const className: ?string = getViewTransitionClassName(373 newProps.default,374 newProps.update,375 );376 if (className === 'none') {377 // If update is "none" then we don't have to apply a name. Since we won't animate this boundary.378 return;379 }380 const clones = state.clones;381 // If there are no clones at this point, that should mean that there are no382 // HostComponent children in this ViewTransition.383 if (clones !== null) {384 applyViewTransitionToClones(oldName, className, clones, finishedWork);385 }386}387388function recursivelyInsertNew(389 parentFiber: Fiber,390 hostParentClone: Instance,391 parentViewTransition: null | ViewTransitionState,392 visitPhase: VisitPhase,393): void {394 if (395 visitPhase === INSERT_APPEARING_PAIR &&396 parentViewTransition === null &&397 (parentFiber.subtreeFlags & (ViewTransitionNamedStatic | Placement)) ===398 NoFlags399 ) {400 // We're just searching for pairs or insertion effects but we have reached the end.401 return;402 }403 let child = parentFiber.child;404 while (child !== null) {405 recursivelyInsertNewFiber(406 child,407 hostParentClone,408 parentViewTransition,409 visitPhase,410 );411 child = child.sibling;412 }413}414415function recursivelyInsertNewFiber(416 finishedWork: Fiber,417 hostParentClone: Instance,418 parentViewTransition: null | ViewTransitionState,419 visitPhase: VisitPhase,420): void {421 switch (finishedWork.tag) {422 case FunctionComponent:423 case ForwardRef:424 case MemoComponent:425 case SimpleMemoComponent: {426 recursivelyInsertNew(427 finishedWork,428 hostParentClone,429 parentViewTransition,430 visitPhase,431 );432 if (finishedWork.flags & Update) {433 // Insertion Effects are mounted temporarily during the rendering of the snapshot.434 // This does not affect cloned Offscreen content since those would've been mounted435 // while inside the offscreen tree already.436 // Note that because we are mounting a clone of the DOM tree and the previous DOM437 // tree remains mounted during the snapshot, we can't unmount any previous insertion438 // effects. This can lead to conflicts but that is similar to what can happen with439 // conflicts for two mounted Activity boundaries.440 commitHookEffectListMount(HookInsertion | HookHasEffect, finishedWork);441 }442 break;443 }444 case HostHoistable: {445 // $FlowFixMe[constant-condition]446 if (supportsResources) {447 // TODO: Hoistables should get optimistically inserted and then removed.448 recursivelyInsertNew(449 finishedWork,450 hostParentClone,451 parentViewTransition,452 visitPhase,453 );454 break;455 }456 // Fall through457 }458 case HostSingleton: {459 // $FlowFixMe[constant-condition]460 if (supportsSingletons) {461 recursivelyInsertNew(462 finishedWork,463 hostParentClone,464 parentViewTransition,465 visitPhase,466 );467468 if (__DEV__) {469 // We cannot apply mutations to Host Singletons since by definition470 // they cannot be cloned. Therefore we warn in DEV if this commit471 // had any effect.472 if (finishedWork.flags & Update) {473 console.error(474 'startGestureTransition() caused something to render a new <%s>. ' +475 'This is not possible in the current implementation. ' +476 "Make sure that the swipe doesn't mount any new <%s> elements.",477 finishedWork.type,478 finishedWork.type,479 );480 }481 }482 break;483 }484 // Fall through485 }486 case HostComponent: {487 const instance: Instance = finishedWork.stateNode;488 // For insertions we don't need to clone. It's already new state node.489 if (visitPhase !== INSERT_APPEARING_PAIR) {490 appendChild(hostParentClone, instance);491 trackHostMutation();492 recursivelyInsertNew(493 finishedWork,494 instance,495 null,496 INSERT_APPEARING_PAIR,497 );498 } else {499 recursivelyInsertNew(finishedWork, instance, null, visitPhase);500 }501 if (parentViewTransition !== null) {502 if (parentViewTransition.clones === null) {503 parentViewTransition.clones = [instance];504 } else {505 parentViewTransition.clones.push(instance);506 }507 }508 break;509 }510 case HostText: {511 const textInstance: TextInstance = finishedWork.stateNode;512 // $FlowFixMe[invalid-compare]513 if (textInstance === null) {514 throw new Error(515 'This should have a text node initialized. This error is likely ' +516 'caused by a bug in React. Please file an issue.',517 );518 }519 // For insertions we don't need to clone. It's already new state node.520 if (visitPhase !== INSERT_APPEARING_PAIR) {521 appendChild(hostParentClone, textInstance);522 trackHostMutation();523 }524 break;525 }526 case HostPortal: {527 // TODO: Consider what should happen to Portals. For now we exclude them.528 break;529 }530 case OffscreenComponent: {531 const newState: OffscreenState | null = finishedWork.memoizedState;532 const isHidden = newState !== null;533 if (!isHidden) {534 // Only insert nodes if this tree is going to be visible. No need to535 // insert invisible content.536 // Since there was no mutation to this node, it couldn't have changed537 // visibility so we don't need to update visitPhase here.538 recursivelyInsertNew(539 finishedWork,540 hostParentClone,541 parentViewTransition,542 visitPhase,543 );544 }545 break;546 }547 case ViewTransitionComponent:548 const prevMutationContext = pushMutationContext();549 const viewTransitionState: ViewTransitionState = finishedWork.stateNode;550 // TODO: If this was already cloned by a previous pass we can reuse those clones.551 viewTransitionState.clones = null;552 let nextPhase: VisitPhase;553 if (visitPhase === INSERT_EXIT) {554 // This was an Enter of a ViewTransition. We now move onto inserting the inner555 // HostComponents and finding inner pairs.556 nextPhase = INSERT_APPEND;557 } else {558 nextPhase = visitPhase;559 }560 recursivelyInsertNew(561 finishedWork,562 hostParentClone,563 viewTransitionState,564 nextPhase,565 );566 // After we've inserted the new nodes into the "clones" set we can apply share567 // or exit transitions to them.568 if (visitPhase === INSERT_EXIT) {569 applyExitViewTransition(finishedWork);570 } else if (571 visitPhase === INSERT_APPEARING_PAIR ||572 visitPhase === INSERT_APPEND573 ) {574 applyAppearingPairViewTransition(finishedWork);575 }576 popMutationContext(prevMutationContext);577 break;578 default: {579 recursivelyInsertNew(580 finishedWork,581 hostParentClone,582 parentViewTransition,583 visitPhase,584 );585 break;586 }587 }588}589590function recursivelyInsertClonesFromExistingTree(591 parentFiber: Fiber,592 hostParentClone: Instance,593 parentViewTransition: null | ViewTransitionState,594 visitPhase: VisitPhase,595): void {596 let child = parentFiber.child;597 while (child !== null) {598 switch (child.tag) {599 case HostComponent: {600 const instance: Instance = child.stateNode;601 let nextPhase: VisitPhase;602 switch (visitPhase) {603 case CLONE_EXIT:604 case CLONE_UNHIDE:605 case CLONE_APPEARING_PAIR:606 // If this was an unhide, we need to keep going if there are any named607 // pairs in this subtree, since they might need to be marked.608 nextPhase =609 (child.subtreeFlags & ViewTransitionNamedStatic) !== NoFlags610 ? CLONE_APPEARING_PAIR611 : CLONE_UNCHANGED;612 break;613 default:614 // We've found any "layout" View Transitions at this point so we can bail.615 nextPhase = CLONE_UNCHANGED;616 }617 let clone: Instance;618 if (nextPhase !== CLONE_UNCHANGED) {619 // We might need a handle on these clones, so we need to do a shallow clone620 // and keep going.621 clone = cloneMutableInstance(instance, false);622 recursivelyInsertClonesFromExistingTree(623 child,624 clone,625 null,626 nextPhase,627 );628 } else {629 // If we have no mutations in this subtree, and we don't need a handle on the630 // clones, then we can do a deep clone instead and bailout.631 clone = cloneMutableInstance(instance, true);632 // TODO: We may need to transfer some DOM state such as scroll position633 // for the deep clones.634 // TODO: If there's a manual view-transition-name inside the clone we635 // should ideally remove it from the original and then restore it in mutation636 // phase. Otherwise it leads to duplicate names.637 }638 appendChild(hostParentClone, clone);639 if (parentViewTransition !== null) {640 if (parentViewTransition.clones === null) {641 parentViewTransition.clones = [clone];642 } else {643 parentViewTransition.clones.push(clone);644 }645 }646 if (visitPhase === CLONE_EXIT || visitPhase === CLONE_UNHIDE) {647 unhideInstance(clone, child.memoizedProps);648 trackHostMutation();649 }650 break;651 }652 case HostText: {653 const textInstance: TextInstance = child.stateNode;654 // $FlowFixMe[invalid-compare]655 if (textInstance === null) {656 throw new Error(657 'This should have a text node initialized. This error is likely ' +658 'caused by a bug in React. Please file an issue.',659 );660 }661 const clone = cloneMutableTextInstance(textInstance);662 appendChild(hostParentClone, clone);663 if (visitPhase === CLONE_EXIT || visitPhase === CLONE_UNHIDE) {664 unhideTextInstance(clone, child.memoizedProps);665 trackHostMutation();666 }667 break;668 }669 case HostPortal: {670 // TODO: Consider what should happen to Portals. For now we exclude them.671 break;672 }673 case OffscreenComponent: {674 const newState: OffscreenState | null = child.memoizedState;675 const isHidden = newState !== null;676 if (!isHidden) {677 // Only insert clones if this tree is going to be visible. No need to678 // clone invisible content.679 // TODO: If this is visible but detached it should still be cloned.680 // Since there was no mutation to this node, it couldn't have changed681 // visibility so we don't need to update visitPhase here.682 recursivelyInsertClonesFromExistingTree(683 child,684 hostParentClone,685 parentViewTransition,686 visitPhase,687 );688 }689 break;690 }691 case ViewTransitionComponent:692 const prevMutationContext = pushMutationContext();693 const viewTransitionState: ViewTransitionState = child.stateNode;694 // TODO: If this was already cloned by a previous pass we can reuse those clones.695 viewTransitionState.clones = null;696 // "Existing" view transitions are in subtrees that didn't update so697 // this is a "current". We normally clear this upon rerendering698 // but we use this flag to track changes from layout in the commit.699 // So we need it to be cleared before we do that.700 // TODO: Use some other temporary state to track this.701 child.flags &= ~Update;702 let nextPhase: VisitPhase;703 if (visitPhase === CLONE_EXIT) {704 // This was an Enter of a ViewTransition. We now move onto unhiding the inner705 // HostComponents and finding inner pairs.706 nextPhase = CLONE_UNHIDE;707 // TODO: Mark the name and find a pair.708 } else if (visitPhase === CLONE_UPDATE) {709 // If the tree had no mutations and we've found the top most ViewTransition710 // then this is the one we might apply the "layout" state too if it has changed711 // position. After we've found its HostComponents we can bail out.712 nextPhase = CLONE_UNCHANGED;713 } else {714 nextPhase = visitPhase;715 }716 recursivelyInsertClonesFromExistingTree(717 child,718 hostParentClone,719 viewTransitionState,720 nextPhase,721 );722 // After we've collected the cloned instances, we can apply exit or share transitions723 // to them.724 if (visitPhase === CLONE_EXIT) {725 applyExitViewTransition(child);726 } else if (727 visitPhase === CLONE_APPEARING_PAIR ||728 visitPhase === CLONE_UNHIDE729 ) {730 applyAppearingPairViewTransition(child);731 } else if (visitPhase === CLONE_UPDATE) {732 applyNestedViewTransition(child);733 }734 popMutationContext(prevMutationContext);735 break;736 default: {737 recursivelyInsertClonesFromExistingTree(738 child,739 hostParentClone,740 parentViewTransition,741 visitPhase,742 );743 break;744 }745 }746 child = child.sibling;747 }748}749750function recursivelyInsertClones(751 parentFiber: Fiber,752 hostParentClone: Instance,753 parentViewTransition: null | ViewTransitionState,754 visitPhase: VisitPhase,755) {756 const deletions = parentFiber.deletions;757 if (deletions !== null) {758 for (let i = 0; i < deletions.length; i++) {759 const childToDelete = deletions[i];760 trackEnterViewTransitions(childToDelete);761 // Normally we would only mark something as triggering a mutation if there was762 // actually a HostInstance below here. If this tree didn't contain a HostInstances763 // we shouldn't trigger a mutation even though a virtual component was deleted.764 trackHostMutation();765 }766 }767768 if (769 parentFiber.alternate === null ||770 (parentFiber.subtreeFlags & MutationMask) !== NoFlags771 ) {772 // If we have mutations or if this is a newly inserted tree, clone as we go.773 let child = parentFiber.child;774 while (child !== null) {775 insertDestinationClonesOfFiber(776 child,777 hostParentClone,778 parentViewTransition,779 visitPhase,780 );781 child = child.sibling;782 }783 } else {784 // Once we reach a subtree with no more mutations we can bail out.785 // However, we must still insert deep clones of the HostComponents.786 recursivelyInsertClonesFromExistingTree(787 parentFiber,788 hostParentClone,789 parentViewTransition,790 visitPhase,791 );792 }793}794795function insertDestinationClonesOfFiber(796 finishedWork: Fiber,797 hostParentClone: Instance,798 parentViewTransition: null | ViewTransitionState,799 visitPhase: VisitPhase,800) {801 const current = finishedWork.alternate;802 if (current === null) {803 // This is a newly mounted subtree. Insert any HostComponents and trigger804 // Enter transitions.805 recursivelyInsertNewFiber(806 finishedWork,807 hostParentClone,808 parentViewTransition,809 INSERT_EXIT,810 );811 return;812 }813814 const flags = finishedWork.flags;815 // The effect flag should be checked *after* we refine the type of fiber,816 // because the fiber tag is more specific. An exception is any flag related817 // to reconciliation, because those can be set on all fiber types.818 switch (finishedWork.tag) {819 case HostHoistable: {820 // $FlowFixMe[constant-condition]821 if (supportsResources) {822 // TODO: Hoistables should get optimistically inserted and then removed.823 recursivelyInsertClones(824 finishedWork,825 hostParentClone,826 parentViewTransition,827 visitPhase,828 );829 break;830 }831 // Fall through832 }833 case HostSingleton: {834 // $FlowFixMe[constant-condition]835 if (supportsSingletons) {836 recursivelyInsertClones(837 finishedWork,838 hostParentClone,839 parentViewTransition,840 visitPhase,841 );842 if (__DEV__) {843 // We cannot apply mutations to Host Singletons since by definition844 // they cannot be cloned. Therefore we warn in DEV if this commit845 // had any effect.846 if (flags & Update) {847 const newProps = finishedWork.memoizedProps;848 const oldProps = current.memoizedProps;849 const instance = finishedWork.stateNode;850 const type = finishedWork.type;851 const prev = pushMutationContext();852853 try {854 // Since we currently don't have a separate diffing algorithm for855 // individual properties, the Update flag can be a false positive.856 // We have to apply the new props first o detect any mutations and857 // then revert them.858 commitUpdate(instance, type, oldProps, newProps, finishedWork);859 if (viewTransitionMutationContext) {860 console.error(861 'startGestureTransition() caused something to mutate <%s>. ' +862 'This is not possible in the current implementation. ' +863 "Make sure that the swipe doesn't update any state which " +864 'causes <%s> to change.',865 finishedWork.type,866 finishedWork.type,867 );868 }869 // Revert870 commitUpdate(instance, type, newProps, oldProps, finishedWork);871 } finally {872 popMutationContext(prev);873 }874 }875 }876 break;877 }878 // Fall through879 }880 case HostComponent: {881 const instance: Instance = finishedWork.stateNode;882 let clone: Instance;883 if (finishedWork.child === null) {884 // This node is terminal. We still do a deep clone in case this has user885 // inserted content, text content or dangerouslySetInnerHTML.886 clone = cloneMutableInstance(instance, true);887 if (finishedWork.flags & ContentReset) {888 resetTextContent(clone);889 trackHostMutation();890 }891 } else {892 // If we have children we'll clone them as we walk the tree so we just893 // do a shallow clone here.894 clone = cloneMutableInstance(instance, false);895 }896897 if (flags & Update) {898 const newProps = finishedWork.memoizedProps;899 const oldProps = current.memoizedProps;900 const type = finishedWork.type;901 // Apply the delta to the clone.902 commitUpdate(clone, type, oldProps, newProps, finishedWork);903 }904905 if (visitPhase === CLONE_EXIT || visitPhase === CLONE_UNHIDE) {906 appendChild(hostParentClone, clone);907 unhideInstance(clone, finishedWork.memoizedProps);908 recursivelyInsertClones(909 finishedWork,910 clone,911 null,912 CLONE_APPEARING_PAIR,913 );914 trackHostMutation();915 } else {916 appendChild(hostParentClone, clone);917 recursivelyInsertClones(finishedWork, clone, null, visitPhase);918 }919 if (parentViewTransition !== null) {920 if (parentViewTransition.clones === null) {921 parentViewTransition.clones = [clone];922 } else {923 parentViewTransition.clones.push(clone);924 }925 }926 break;927 }928 case HostText: {929 const textInstance: TextInstance = finishedWork.stateNode;930 // $FlowFixMe[invalid-compare]931 if (textInstance === null) {932 throw new Error(933 'This should have a text node initialized. This error is likely ' +934 'caused by a bug in React. Please file an issue.',935 );936 }937 const clone = cloneMutableTextInstance(textInstance);938 if (flags & Update) {939 const newText: string = finishedWork.memoizedProps;940 const oldText: string = current.memoizedProps;941 commitTextUpdate(clone, newText, oldText);942 trackHostMutation();943 }944 appendChild(hostParentClone, clone);945 if (visitPhase === CLONE_EXIT || visitPhase === CLONE_UNHIDE) {946 unhideTextInstance(clone, finishedWork.memoizedProps);947 trackHostMutation();948 }949 break;950 }951 case HostPortal: {952 // TODO: Consider what should happen to Portals. For now we exclude them.953 break;954 }955 case OffscreenComponent: {956 const newState: OffscreenState | null = finishedWork.memoizedState;957 const isHidden = newState !== null;958 if (!isHidden) {959 // Only insert clones if this tree is going to be visible. No need to960 // clone invisible content.961 // TODO: If this is visible but detached it should still be cloned.962 let nextPhase: VisitPhase;963 if (visitPhase === CLONE_UPDATE && (flags & Visibility) !== NoFlags) {964 // This is the root of an appear. We need to trigger Enter transitions.965 nextPhase = CLONE_EXIT;966 } else {967 nextPhase = visitPhase;968 }969 recursivelyInsertClones(970 finishedWork,971 hostParentClone,972 parentViewTransition,973 nextPhase,974 );975 // $FlowFixMe[invalid-compare]976 } else if (current !== null && current.memoizedState === null) {977 // Was previously mounted as visible but is now hidden.978 trackEnterViewTransitions(current);979 // Normally we would only mark something as triggering a mutation if there was980 // actually a HostInstance below here. If this tree didn't contain a HostInstances981 // we shouldn't trigger a mutation even though a virtual component was hidden.982 trackHostMutation();983 }984 break;985 }986 case ViewTransitionComponent:987 const prevMutationContext = pushMutationContext();988 const viewTransitionState: ViewTransitionState = finishedWork.stateNode;989 // TODO: If this was already cloned by a previous pass we can reuse those clones.990 viewTransitionState.clones = null;991 let nextPhase: VisitPhase;992 if (visitPhase === CLONE_EXIT) {993 // This was an Enter of a ViewTransition. We now move onto unhiding the inner994 // HostComponents and finding inner pairs.995 nextPhase = CLONE_UNHIDE;996 // TODO: Mark the name and find a pair.997 } else {998 nextPhase = visitPhase;999 }1000 recursivelyInsertClones(1001 finishedWork,1002 hostParentClone,1003 viewTransitionState,1004 nextPhase,1005 );1006 if (viewTransitionMutationContext) {1007 // Track that this boundary had a mutation and therefore needs to animate1008 // whether it resized or not.1009 finishedWork.flags |= Update;1010 }1011 // After we've collected the cloned instances, we can apply exit or share transitions1012 // to them.1013 if (visitPhase === CLONE_EXIT) {1014 applyExitViewTransition(finishedWork);1015 } else if (1016 visitPhase === CLONE_APPEARING_PAIR ||1017 visitPhase === CLONE_UNHIDE1018 ) {1019 applyAppearingPairViewTransition(finishedWork);1020 } else if (visitPhase === CLONE_UPDATE) {1021 applyUpdateViewTransition(current, finishedWork);1022 }1023 popMutationContext(prevMutationContext);1024 break;1025 default: {1026 recursivelyInsertClones(1027 finishedWork,1028 hostParentClone,1029 parentViewTransition,1030 visitPhase,1031 );1032 break;1033 }1034 }1035}10361037// Clone View Transition boundaries that have any mutations or might have had their1038// layout affected by child insertions.1039export function insertDestinationClones(1040 root: FiberRoot,1041 finishedWork: Fiber,1042): void {1043 // We'll either not transition the root, or we'll transition the clone. Regardless1044 // we cancel the root view transition name.1045 const needsClone = detectMutationOrInsertClones(finishedWork);1046 if (needsClone) {1047 // Clone the whole root1048 const rootClone = cloneRootViewTransitionContainer(root.containerInfo);1049 root.gestureClone = rootClone;1050 recursivelyInsertClones(finishedWork, rootClone, null, CLONE_UPDATE);1051 } else {1052 root.gestureClone = null;1053 cancelRootViewTransitionName(root.containerInfo);1054 }1055}10561057function measureExitViewTransitions(placement: Fiber): void {1058 if (placement.tag === ViewTransitionComponent) {1059 // const state: ViewTransitionState = placement.stateNode;1060 const props: ViewTransitionProps = placement.memoizedProps;1061 const name = props.name;1062 if (name != null && name !== 'auto') {1063 // TODO: Find a pair1064 }1065 } else if ((placement.subtreeFlags & ViewTransitionStatic) !== NoFlags) {1066 // TODO: Check if this is a hidden Offscreen or a Portal.1067 let child = placement.child;1068 while (child !== null) {1069 measureExitViewTransitions(child);1070 child = child.sibling;1071 }1072 } else {1073 // We don't need to find pairs here because we would've already found and1074 // measured the pairs inside the deletion phase.1075 }1076}10771078function recursivelyRestoreNew(1079 finishedWork: Fiber,1080 nearestMountedAncestor: Fiber,1081): void {1082 // There has to be move a Placement AND an Update flag somewhere below for this1083 // 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 unmount1100 // 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}11111112function recursivelyApplyViewTransitions(parentFiber: Fiber) {1113 const deletions = parentFiber.deletions;1114 if (deletions !== null) {1115 for (let i = 0; i < deletions.length; i++) {1116 const childToDelete = deletions[i];1117 commitEnterViewTransitions(childToDelete, true);1118 }1119 }11201121 if (1122 parentFiber.alternate === null ||1123 (parentFiber.subtreeFlags & MutationMask) !== NoFlags1124 ) {1125 // If we have mutations or if this is a newly inserted tree, clone as we go.1126 let child = parentFiber.child;1127 while (child !== null) {1128 const current = child.alternate;1129 if (current === null) {1130 measureExitViewTransitions(child);1131 recursivelyRestoreNew(child, parentFiber);1132 } else {1133 applyViewTransitionsOnFiber(child, current);1134 }1135 child = child.sibling;1136 }1137 } else {1138 // Nothing has changed in this subtree, but the parent may have still affected1139 // its size and position. We need to measure the old and new state to see if1140 // we should animate its size and position.1141 measureNestedViewTransitions(parentFiber, true);1142 }1143}11441145function applyViewTransitionsOnFiber(finishedWork: Fiber, current: Fiber) {1146 // The effect flag should be checked *after* we refine the type of fiber,1147 // because the fiber tag is more specific. An exception is any flag related1148 // to reconciliation, because those can be set on all fiber types.1149 switch (finishedWork.tag) {1150 case HostPortal: {1151 // TODO: Consider what should happen to Portals. For now we exclude them.1152 break;1153 }1154 case OffscreenComponent: {1155 const newState: OffscreenState | null = finishedWork.memoizedState;1156 const isHidden = newState !== null;1157 const wasHidden = current.memoizedState !== null;1158 if (!isHidden) {1159 if (wasHidden) {1160 measureExitViewTransitions(finishedWork);1161 recursivelyRestoreNew(finishedWork, finishedWork);1162 } else {1163 recursivelyApplyViewTransitions(finishedWork);1164 }1165 } else {1166 if (!wasHidden) {1167 // Was previously mounted as visible but is now hidden.1168 commitEnterViewTransitions(current, true);1169 }1170 }1171 break;1172 }1173 case ViewTransitionComponent: {1174 const prevContextChanged = viewTransitionContextChanged;1175 const prevCancelableChildren = pushViewTransitionCancelableScope();1176 viewTransitionContextChanged = false;1177 recursivelyApplyViewTransitions(finishedWork);11781179 if (viewTransitionContextChanged) {1180 finishedWork.flags |= Update;1181 }11821183 const inViewport = measureUpdateViewTransition(1184 current,1185 finishedWork,1186 true,1187 );11881189 if ((finishedWork.flags & Update) === NoFlags || !inViewport) {1190 // If this boundary didn't update, then we may be able to cancel its children.1191 // We bubble them up to the parent set to be determined later if we can cancel.1192 // Similarly, if old and new state was outside the viewport, we can skip it1193 // even if it did update.1194 if (prevCancelableChildren === null) {1195 // Bubbling up this whole set to the parent.1196 } else {1197 // Merge with parent set.1198 // $FlowFixMe[method-unbinding]1199 prevCancelableChildren.push.apply(1200 prevCancelableChildren,1201 viewTransitionCancelableChildren,1202 );1203 popViewTransitionCancelableScope(prevCancelableChildren);1204 }1205 // TODO: If this doesn't end up canceled, because a parent animates,1206 // then we should probably issue an event since this instance is part of it.1207 } else {1208 const props: ViewTransitionProps = finishedWork.memoizedProps;1209 scheduleGestureTransitionEvent(finishedWork, props.onGestureUpdate);1210 // If this boundary did update, we cannot cancel its children so those are dropped.1211 popViewTransitionCancelableScope(prevCancelableChildren);1212 }12131214 if ((finishedWork.flags & AffectedParentLayout) !== NoFlags) {1215 // This boundary changed size in a way that may have caused its parent to1216 // relayout. We need to bubble this information up to the parent.1217 viewTransitionContextChanged = true;1218 } else {1219 // Otherwise, we restore it to whatever the parent had found so far.1220 viewTransitionContextChanged = prevContextChanged;1221 }12221223 const viewTransitionState: ViewTransitionState = finishedWork.stateNode;1224 viewTransitionState.clones = null; // Reset1225 break;1226 }1227 default: {1228 recursivelyApplyViewTransitions(finishedWork);1229 break;1230 }1231 }1232}12331234// Revert insertions and apply view transition names to the "new" (current) state.1235export function applyDepartureTransitions(1236 root: FiberRoot,1237 finishedWork: Fiber,1238): void {1239 // First measure and apply view-transition-names to the "new" states.1240 viewTransitionContextChanged = false;1241 pushViewTransitionCancelableScope();12421243 recursivelyApplyViewTransitions(finishedWork);12441245 // Then remove the clones.1246 const rootClone = root.gestureClone;1247 if (rootClone !== null) {1248 root.gestureClone = null;1249 removeRootViewTransitionClone(root.containerInfo, rootClone);1250 }12511252 if (!viewTransitionContextChanged) {1253 // If we didn't leak any resizing out to the root, we don't have to transition1254 // the root itself. This means that we can now safely cancel any cancellations1255 // that bubbled all the way up.1256 const cancelableChildren = viewTransitionCancelableChildren;1257 if (cancelableChildren !== null) {1258 for (let i = 0; i < cancelableChildren.length; i += 3) {1259 cancelViewTransitionName(1260 cancelableChildren[i] as any as Instance,1261 cancelableChildren[i + 1] as any as string,1262 cancelableChildren[i + 2] as any as Props,1263 );1264 }1265 }1266 // We also cancel the root itself. First we restore the name to the documentElement1267 // and then we cancel it.1268 restoreRootViewTransitionName(root.containerInfo);1269 cancelRootViewTransitionName(root.containerInfo);1270 }1271 popViewTransitionCancelableScope(null);1272}12731274function recursivelyRestoreViewTransitions(parentFiber: Fiber) {1275 const deletions = parentFiber.deletions;1276 if (deletions !== null) {1277 for (let i = 0; i < deletions.length; i++) {1278 const childToDelete = deletions[i];1279 restoreEnterOrExitViewTransitions(childToDelete);1280 }1281 }12821283 if (1284 parentFiber.alternate === null ||1285 (parentFiber.subtreeFlags & MutationMask) !== NoFlags1286 ) {1287 // If we have mutations or if this is a newly inserted tree, clone as we go.1288 let child = parentFiber.child;1289 while (child !== null) {1290 restoreViewTransitionsOnFiber(child);1291 child = child.sibling;1292 }1293 } else {1294 // Nothing has changed in this subtree, but the parent may have still affected1295 // its size and position. We need to measure the old and new state to see if1296 // we should animate its size and position.1297 restoreNestedViewTransitions(parentFiber);1298 }1299}13001301function restoreViewTransitionsOnFiber(finishedWork: Fiber) {1302 const current = finishedWork.alternate;1303 if (current === null) {1304 restoreEnterOrExitViewTransitions(finishedWork);1305 return;1306 }13071308 const flags = finishedWork.flags;1309 // The effect flag should be checked *after* we refine the type of fiber,1310 // because the fiber tag is more specific. An exception is any flag related1311 // to reconciliation, because those can be set on all fiber types.1312 switch (finishedWork.tag) {1313 case HostPortal: {1314 // TODO: Consider what should happen to Portals. For now we exclude them.1315 break;1316 }1317 case OffscreenComponent: {1318 if (flags & Visibility) {1319 const newState: OffscreenState | null = finishedWork.memoizedState;1320 const isHidden = newState !== null;1321 if (!isHidden) {1322 restoreEnterOrExitViewTransitions(finishedWork);1323 // $FlowFixMe[invalid-compare]1324 } else if (current !== null && current.memoizedState === null) {1325 // Was previously mounted as visible but is now hidden.1326 restoreEnterOrExitViewTransitions(current);1327 }1328 }1329 break;1330 }1331 case ViewTransitionComponent:1332 restoreUpdateViewTransitionForGesture(current, finishedWork);1333 recursivelyRestoreViewTransitions(finishedWork);1334 break;1335 default: {1336 recursivelyRestoreViewTransitions(finishedWork);1337 break;1338 }1339 }1340}13411342// Revert transition names and start/adjust animations on the started View Transition.1343export function startGestureAnimations(1344 root: FiberRoot,1345 finishedWork: Fiber,1346): void {1347 restoreViewTransitionsOnFiber(finishedWork);1348 restoreRootViewTransitionName(root.containerInfo);1349}
Same data, no extra tab — call code_get_file + code_get_findings over MCP from Claude/Cursor/Copilot.