packages/react-reconciler/src/ReactFiberCommitViewTransitions.js JAVASCRIPT 1,123 lines View on github.com → Search inside
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';11import type {Instance, InstanceMeasurement, Props} from './ReactFiberConfig';12import type {Fiber} from './ReactInternalTypes';13import type {ViewTransitionState} from './ReactFiberViewTransitionComponent';1415import {16  HostComponent,17  OffscreenComponent,18  ViewTransitionComponent,19} from './ReactWorkTags';20import {21  NoFlags,22  Update,23  ViewTransitionStatic,24  ViewTransitionStaticParent,25  AffectedParentLayout,26  ViewTransitionNamedStatic,27} from './ReactFiberFlags';28import {29  supportsMutation,30  applyViewTransitionName,31  restoreViewTransitionName,32  measureInstance,33  measureClonedInstance,34  hasInstanceChanged,35  hasInstanceAffectedParent,36  wasInstanceInViewport,37} from './ReactFiberConfig';38import {39  scheduleViewTransitionEvent,40  scheduleGestureTransitionEvent,41} from './ReactFiberWorkLoop';42import {43  getViewTransitionName,44  getViewTransitionClassName,45} from './ReactFiberViewTransitionComponent';46import {trackAnimatingTask} from './ReactProfilerTimer';47import {48  enableComponentPerformanceTrack,49  enableProfilerTimer,50  enableViewTransitionForPersistenceMode,51  enableViewTransitionParentEnterExit,52} from 'shared/ReactFeatureFlags';5354export let shouldStartViewTransition: boolean = false;5556export function resetShouldStartViewTransition(): void {57  shouldStartViewTransition = false;58}5960// This tracks named ViewTransition components found in the accumulateSuspenseyCommit61// phase that might need to find deleted pairs in the beforeMutation phase.62export let appearingViewTransitions: Map<string, ViewTransitionState> | null =63  null;6465export function resetAppearingViewTransitions(): void {66  appearingViewTransitions = null;67}6869export function trackAppearingViewTransition(70  name: string,71  state: ViewTransitionState,72): void {73  if (appearingViewTransitions === null) {74    appearingViewTransitions = new Map();75  }76  appearingViewTransitions.set(name, state);77}7879export function trackEnterViewTransitions(placement: Fiber): void {80  if (81    placement.tag === ViewTransitionComponent ||82    (placement.subtreeFlags & ViewTransitionStatic) !== NoFlags83  ) {84    // If an inserted or appearing Fiber is a ViewTransition component or has one as85    // an immediate child, then that will trigger as an "Enter" in future passes.86    // We don't do anything else for that case in the "before mutation" phase but we87    // still have to mark it as needing to call startViewTransition if nothing else88    // updates.89    shouldStartViewTransition = true;90  }91}9293// We can't cancel view transition children until we know that their parent also94// don't need to transition.95export let viewTransitionCancelableChildren: null | Array<96  Instance | string | Props,97> = null; // tupled array where each entry is [instance: Instance, oldName: string, props: Props]9899export function pushViewTransitionCancelableScope(): null | Array<100  Instance | string | Props,101> {102  const prevChildren = viewTransitionCancelableChildren;103  viewTransitionCancelableChildren = null;104  return prevChildren;105}106107export function popViewTransitionCancelableScope(108  prevChildren: null | Array<Instance | string | Props>,109): void {110  viewTransitionCancelableChildren = prevChildren;111}112113let viewTransitionHostInstanceIdx = 0;114115function applyViewTransitionToHostInstances(116  fiber: Fiber,117  name: string,118  className: ?string,119  collectMeasurements: null | Array<InstanceMeasurement>,120  stopAtNestedViewTransitions: boolean,121): boolean {122  viewTransitionHostInstanceIdx = 0;123  const inViewport = applyViewTransitionToHostInstancesRecursive(124    fiber.child,125    name,126    className,127    collectMeasurements,128    stopAtNestedViewTransitions,129  );130  if (enableProfilerTimer && enableComponentPerformanceTrack && inViewport) {131    if (fiber._debugTask != null) {132      trackAnimatingTask(fiber._debugTask);133    }134  }135  return inViewport;136}137138function applyViewTransitionToHostInstancesRecursive(139  child: null | Fiber,140  name: string,141  className: ?string,142  collectMeasurements: null | Array<InstanceMeasurement>,143  stopAtNestedViewTransitions: boolean,144): boolean {145  // $FlowFixMe[constant-condition]146  if (!supportsMutation) {147    if (enableViewTransitionForPersistenceMode) {148      while (child !== null) {149        if (child.tag === HostComponent) {150          const instance: Instance = child.stateNode;151          // TODO: calculate whether component is in viewport152          shouldStartViewTransition = true;153          applyViewTransitionName(154            instance,155            viewTransitionHostInstanceIdx === 0156              ? name157              : name + '_' + viewTransitionHostInstanceIdx,158            className,159          );160          viewTransitionHostInstanceIdx++;161        } else if (162          child.tag === OffscreenComponent &&163          child.memoizedState !== null164        ) {165          // Skip any hidden subtrees. They were or are effectively not there.166        } else if (167          child.tag === ViewTransitionComponent &&168          stopAtNestedViewTransitions169        ) {170          // Skip any nested view transitions for updates since in that case the171          // inner most one is the one that handles the update.172        } else {173          applyViewTransitionToHostInstancesRecursive(174            child.child,175            name,176            className,177            collectMeasurements,178            stopAtNestedViewTransitions,179          );180        }181        child = child.sibling;182      }183      return true;184    } else {185      return false;186    }187  }188  let inViewport = false;189  while (child !== null) {190    if (child.tag === HostComponent) {191      const instance: Instance = child.stateNode;192      if (collectMeasurements !== null) {193        const measurement = measureInstance(instance);194        collectMeasurements.push(measurement);195        if (wasInstanceInViewport(measurement)) {196          inViewport = true;197        }198      } else if (!inViewport) {199        if (wasInstanceInViewport(measureInstance(instance))) {200          inViewport = true;201        }202      }203      shouldStartViewTransition = true;204      applyViewTransitionName(205        instance,206        viewTransitionHostInstanceIdx === 0207          ? name208          : // If we have multiple Host Instances below, we add a suffix to the name to give209            // each one a unique name.210            name + '_' + viewTransitionHostInstanceIdx,211        className,212      );213      viewTransitionHostInstanceIdx++;214    } else if (215      child.tag === OffscreenComponent &&216      child.memoizedState !== null217    ) {218      // Skip any hidden subtrees. They were or are effectively not there.219    } else if (220      child.tag === ViewTransitionComponent &&221      stopAtNestedViewTransitions222    ) {223      // Skip any nested view transitions for updates since in that case the224      // inner most one is the one that handles the update.225    } else {226      if (227        applyViewTransitionToHostInstancesRecursive(228          child.child,229          name,230          className,231          collectMeasurements,232          stopAtNestedViewTransitions,233        )234      ) {235        inViewport = true;236      }237    }238    child = child.sibling;239  }240  return inViewport;241}242243function restoreViewTransitionOnHostInstances(244  child: null | Fiber,245  stopAtNestedViewTransitions: boolean,246): void {247  // $FlowFixMe[constant-condition]248  if (!supportsMutation) {249    return;250  }251  while (child !== null) {252    if (child.tag === HostComponent) {253      const instance: Instance = child.stateNode;254      restoreViewTransitionName(instance, child.memoizedProps);255    } else if (256      child.tag === OffscreenComponent &&257      child.memoizedState !== null258    ) {259      // Skip any hidden subtrees. They were or are effectively not there.260    } else if (261      child.tag === ViewTransitionComponent &&262      stopAtNestedViewTransitions263    ) {264      // Skip any nested view transitions for updates since in that case the265      // inner most one is the one that handles the update.266    } else {267      restoreViewTransitionOnHostInstances(268        child.child,269        stopAtNestedViewTransitions,270      );271    }272    child = child.sibling;273  }274}275276function commitAppearingPairViewTransitions(placement: Fiber): void {277  if ((placement.subtreeFlags & ViewTransitionNamedStatic) === NoFlags) {278    // This has no named view transitions in its subtree.279    return;280  }281  let child = placement.child;282  while (child !== null) {283    if (child.tag === OffscreenComponent && child.memoizedState !== null) {284      // This tree was already hidden so we skip it.285    } else {286      commitAppearingPairViewTransitions(child);287      if (288        child.tag === ViewTransitionComponent &&289        (child.flags & ViewTransitionNamedStatic) !== NoFlags290      ) {291        const instance: ViewTransitionState = child.stateNode;292        if (instance.paired) {293          const props: ViewTransitionProps = child.memoizedProps;294          if (props.name == null || props.name === 'auto') {295            throw new Error(296              'Found a pair with an auto name. This is a bug in React.',297            );298          }299          const name = props.name;300          const className: ?string = getViewTransitionClassName(301            props.default,302            props.share,303          );304          if (className !== 'none') {305            // We found a new appearing view transition with the same name as this deletion.306            // We'll transition between them.307            const inViewport = applyViewTransitionToHostInstances(308              child,309              name,310              className,311              null,312              false,313            );314            if (!inViewport) {315              // This boundary is exiting within the viewport but is going to leave the viewport.316              // Instead, we treat this as an exit of the previous entry by reverting the new name.317              // Ideally we could undo the old transition but it's now too late. It's also on its318              // on snapshot. We have know was for it to paint onto the original group.319              // TODO: This will lead to things unexpectedly having exit animations that normally320              // wouldn't happen. Consider if we should just let this fly off the screen instead.321              restoreViewTransitionOnHostInstances(child.child, false);322            }323          }324        }325      }326    }327    child = child.sibling;328  }329}330331export function commitParentEnterViewTransitions(332  parent: Fiber,333  gesture: boolean,334): void {335  let child = parent.child;336  while (child !== null) {337    if (child.tag === OffscreenComponent && child.memoizedState !== null) {338      // Skip hidden subtrees.339    } else if (child.tag === ViewTransitionComponent) {340      const props: ViewTransitionProps = child.memoizedProps;341      const hasParentClass = props.parentEnter !== undefined;342      const hasParentHandler = gesture343        ? props.onGestureParentEnter != null344        : props.onParentEnter != null;345      if (hasParentClass || hasParentHandler) {346        let relay = true;347        if (hasParentClass) {348          const state: ViewTransitionState = child.stateNode;349          const name = getViewTransitionName(props, state);350          const className: ?string = getViewTransitionClassName(351            props.default,352            props.parentEnter,353          );354          if (className === 'none') {355            relay = false;356          } else {357            applyViewTransitionToHostInstances(358              child,359              name,360              className,361              null,362              false,363            );364            if (hasParentHandler) {365              if (gesture) {366                scheduleGestureTransitionEvent(367                  child,368                  props.onGestureParentEnter,369                );370              } else {371                scheduleViewTransitionEvent(child, props.onParentEnter);372              }373            }374          }375        } else {376          if (gesture) {377            scheduleGestureTransitionEvent(child, props.onGestureParentEnter);378          } else {379            scheduleViewTransitionEvent(child, props.onParentEnter);380          }381        }382        if (relay) {383          commitParentEnterViewTransitions(child, gesture);384        }385      }386    } else if ((child.subtreeFlags & ViewTransitionStaticParent) !== NoFlags) {387      commitParentEnterViewTransitions(child, gesture);388    }389    child = child.sibling;390  }391}392393export function commitParentExitViewTransitions(394  parent: Fiber,395  gesture: boolean,396): void {397  let child = parent.child;398  while (child !== null) {399    if (child.tag === OffscreenComponent && child.memoizedState !== null) {400      // Skip hidden subtrees.401    } else if (child.tag === ViewTransitionComponent) {402      const props: ViewTransitionProps = child.memoizedProps;403      const hasParentClass = props.parentExit !== undefined;404      const hasParentHandler = gesture405        ? props.onGestureParentExit != null406        : props.onParentExit != null;407      if (hasParentClass || hasParentHandler) {408        let relay = true;409        if (hasParentClass) {410          const state: ViewTransitionState = child.stateNode;411          const name = getViewTransitionName(props, state);412          const className: ?string = getViewTransitionClassName(413            props.default,414            props.parentExit,415          );416          if (className === 'none') {417            relay = false;418          } else {419            applyViewTransitionToHostInstances(420              child,421              name,422              className,423              null,424              false,425            );426            if (hasParentHandler) {427              if (gesture) {428                scheduleGestureTransitionEvent(429                  child,430                  props.onGestureParentExit,431                );432              } else {433                scheduleViewTransitionEvent(child, props.onParentExit);434              }435            }436          }437        } else {438          if (gesture) {439            scheduleGestureTransitionEvent(child, props.onGestureParentExit);440          } else {441            scheduleViewTransitionEvent(child, props.onParentExit);442          }443        }444        if (relay) {445          commitParentExitViewTransitions(child, gesture);446        }447      }448    } else if ((child.subtreeFlags & ViewTransitionStaticParent) !== NoFlags) {449      commitParentExitViewTransitions(child, gesture);450    }451    child = child.sibling;452  }453}454455function restoreParentEnterOrExitViewTransitions(parent: Fiber): void {456  let child = parent.child;457  while (child !== null) {458    if (child.tag === OffscreenComponent && child.memoizedState !== null) {459      // Skip hidden subtrees.460    } else if (child.tag === ViewTransitionComponent) {461      const props: ViewTransitionProps = child.memoizedProps;462      const hasParentClass =463        props.parentEnter !== undefined || props.parentExit !== undefined;464      const hasParentHandler =465        props.onParentEnter != null ||466        props.onParentExit != null ||467        props.onGestureParentEnter != null ||468        props.onGestureParentExit != null;469      if (hasParentClass) {470        restoreViewTransitionOnHostInstances(child.child, false);471      }472      if (hasParentClass || hasParentHandler) {473        restoreParentEnterOrExitViewTransitions(child);474      }475    } else if ((child.subtreeFlags & ViewTransitionStaticParent) !== NoFlags) {476      restoreParentEnterOrExitViewTransitions(child);477    }478    child = child.sibling;479  }480}481482export function commitEnterViewTransitions(483  placement: Fiber,484  gesture: boolean,485): void {486  if (placement.tag === ViewTransitionComponent) {487    const state: ViewTransitionState = placement.stateNode;488    const props: ViewTransitionProps = placement.memoizedProps;489    const name = getViewTransitionName(props, state);490    const className: ?string = getViewTransitionClassName(491      props.default,492      state.paired ? props.share : props.enter,493    );494    if (className !== 'none') {495      const inViewport = applyViewTransitionToHostInstances(496        placement,497        name,498        className,499        null,500        false,501      );502      if (!inViewport) {503        // TODO: If this was part of a pair we will still run the onShare callback.504        // Revert the transition names. This boundary is not in the viewport505        // so we won't bother animating it.506        restoreViewTransitionOnHostInstances(placement.child, false);507        // TODO: Should we still visit the children in case a named one was in the viewport?508      } else {509        commitAppearingPairViewTransitions(placement);510511        if (!state.paired) {512          if (gesture) {513            scheduleGestureTransitionEvent(placement, props.onGestureEnter);514          } else {515            scheduleViewTransitionEvent(placement, props.onEnter);516          }517          if (enableViewTransitionParentEnterExit) {518            commitParentEnterViewTransitions(placement, gesture);519          }520        }521      }522    } else {523      commitAppearingPairViewTransitions(placement);524    }525  } else if ((placement.subtreeFlags & ViewTransitionStatic) !== NoFlags) {526    let child = placement.child;527    while (child !== null) {528      commitEnterViewTransitions(child, gesture);529      child = child.sibling;530    }531  } else {532    commitAppearingPairViewTransitions(placement);533  }534}535536function commitDeletedPairViewTransitions(deletion: Fiber): void {537  if (538    appearingViewTransitions === null ||539    appearingViewTransitions.size === 0540  ) {541    // We've found all.542    return;543  }544  const pairs = appearingViewTransitions;545  if ((deletion.subtreeFlags & ViewTransitionNamedStatic) === NoFlags) {546    // This has no named view transitions in its subtree.547    return;548  }549  let child = deletion.child;550  while (child !== null) {551    if (child.tag === OffscreenComponent && child.memoizedState !== null) {552      // This tree was already hidden so we skip it.553    } else {554      if (555        child.tag === ViewTransitionComponent &&556        (child.flags & ViewTransitionNamedStatic) !== NoFlags557      ) {558        const props: ViewTransitionProps = child.memoizedProps;559        const name = props.name;560        if (name != null && name !== 'auto') {561          const pair = pairs.get(name);562          if (pair !== undefined) {563            const className: ?string = getViewTransitionClassName(564              props.default,565              props.share,566            );567            if (className !== 'none') {568              // We found a new appearing view transition with the same name as this deletion.569              const inViewport = applyViewTransitionToHostInstances(570                child,571                name,572                className,573                null,574                false,575              );576              if (!inViewport) {577                // This boundary is not in the viewport so we won't treat it as a matched pair.578                // Revert the transition names. This avoids it flying onto the screen which can579                // be disruptive and doesn't really preserve any continuity anyway.580                restoreViewTransitionOnHostInstances(child.child, false);581              } else {582                // We'll transition between them.583                const oldInstance: ViewTransitionState = child.stateNode;584                const newInstance: ViewTransitionState = pair;585                newInstance.paired = oldInstance;586                oldInstance.paired = newInstance;587                // Note: If the other side ends up outside the viewport, we'll still run this.588                // Therefore it's possible for onShare to be called with only an old snapshot.589                scheduleViewTransitionEvent(child, props.onShare);590              }591            }592            // Delete the entry so that we know when we've found all of them593            // and can stop searching (size reaches zero).594            pairs.delete(name);595            if (pairs.size === 0) {596              break;597            }598          }599        }600      }601      commitDeletedPairViewTransitions(child);602    }603    child = child.sibling;604  }605}606607export function commitExitViewTransitions(deletion: Fiber): void {608  if (deletion.tag === ViewTransitionComponent) {609    const props: ViewTransitionProps = deletion.memoizedProps;610    const name = getViewTransitionName(props, deletion.stateNode);611    const pair =612      appearingViewTransitions !== null613        ? appearingViewTransitions.get(name)614        : undefined;615    const className: ?string = getViewTransitionClassName(616      props.default,617      pair !== undefined ? props.share : props.exit,618    );619    if (className !== 'none') {620      const inViewport = applyViewTransitionToHostInstances(621        deletion,622        name,623        className,624        null,625        false,626      );627      if (!inViewport) {628        // Revert the transition names. This boundary is not in the viewport629        // so we won't bother animating it.630        restoreViewTransitionOnHostInstances(deletion.child, false);631        // TODO: Should we still visit the children in case a named one was in the viewport?632      } else if (pair !== undefined) {633        // We found a new appearing view transition with the same name as this deletion.634        // We'll transition between them instead of running the normal exit.635        const oldInstance: ViewTransitionState = deletion.stateNode;636        const newInstance: ViewTransitionState = pair;637        newInstance.paired = oldInstance;638        oldInstance.paired = newInstance;639        // Delete the entry so that we know when we've found all of them640        // and can stop searching (size reaches zero).641        // $FlowFixMe[incompatible-use]: Refined by the pair.642        appearingViewTransitions.delete(name);643        // Note: If the other side ends up outside the viewport, we'll still run this.644        // Therefore it's possible for onShare to be called with only an old snapshot.645        scheduleViewTransitionEvent(deletion, props.onShare);646      } else {647        scheduleViewTransitionEvent(deletion, props.onExit);648        if (enableViewTransitionParentEnterExit) {649          commitParentExitViewTransitions(deletion, false);650        }651      }652    }653    if (appearingViewTransitions !== null) {654      // Look for more pairs deeper in the tree.655      commitDeletedPairViewTransitions(deletion);656    }657  } else if ((deletion.subtreeFlags & ViewTransitionStatic) !== NoFlags) {658    let child = deletion.child;659    while (child !== null) {660      commitExitViewTransitions(child);661      child = child.sibling;662    }663  } else {664    if (appearingViewTransitions !== null) {665      commitDeletedPairViewTransitions(deletion);666    }667  }668}669670export function commitBeforeUpdateViewTransition(671  current: Fiber,672  finishedWork: Fiber,673): void {674  // The way we deal with multiple HostInstances as children of a View Transition in an675  // update can get tricky. The important bit is that if you swap out n HostInstances676  // from n HostInstances then they match up in order. Similarly, if you don't swap677  // any HostInstances each instance just transitions as is.678  //679  // We call this function twice. First we apply the view transition names on the680  // "current" tree in the snapshot phase. Then in the mutation phase we apply view681  // transition names to the "finishedWork" tree.682  //683  // This means that if there were insertions or deletions before an updated Instance684  // that same Instance might get different names in the "old" and the "new" state.685  // For example if you swap two HostInstances inside a ViewTransition they don't686  // animate to swap position but rather cross-fade into the other instance. This might687  // be unexpected but it is in line with the semantics that the ViewTransition is its688  // own layer that cross-fades its content when it updates. If you want to reorder then689  // each child needs its own ViewTransition.690  const oldProps: ViewTransitionProps = current.memoizedProps;691  const oldName = getViewTransitionName(oldProps, current.stateNode);692  const newProps: ViewTransitionProps = finishedWork.memoizedProps;693  // This className applies only if there are fewer child DOM nodes than694  // before or if this update should've been cancelled but we ended up with695  // a parent animating so we need to animate the child too.696  // For example, if update="foo" layout="none" and it turns out this was697  // a layout only change, then the "foo" class will be applied even though698  // it was not actually an update. Which is a bug.699  const className: ?string = getViewTransitionClassName(700    newProps.default,701    newProps.update,702  );703  if (className === 'none') {704    // If update is "none" then we don't have to apply a name. Since we won't animate this boundary.705    return;706  }707  applyViewTransitionToHostInstances(708    current,709    oldName,710    className,711    (current.memoizedState = []),712    true,713  );714}715716export function commitNestedViewTransitions(changedParent: Fiber): void {717  let child = changedParent.child;718  while (child !== null) {719    if (child.tag === ViewTransitionComponent) {720      // In this case the outer ViewTransition component wins but if there721      // was an update through this component then the inner one wins.722      const props: ViewTransitionProps = child.memoizedProps;723      const name = getViewTransitionName(props, child.stateNode);724      const className: ?string = getViewTransitionClassName(725        props.default,726        props.update,727      );728      // "Nested" view transitions are in subtrees that didn't update so729      // this is a "current". We normally clear this upon rerendering730      // but we use this flag to track changes from layout in the commit.731      // So we need it to be cleared before we do that.732      // TODO: Use some other temporary state to track this.733      child.flags &= ~Update;734      if (className !== 'none') {735        applyViewTransitionToHostInstances(736          child,737          name,738          className,739          (child.memoizedState = []),740          false,741        );742      }743    } else if ((child.subtreeFlags & ViewTransitionStatic) !== NoFlags) {744      commitNestedViewTransitions(child);745    }746    child = child.sibling;747  }748}749750function restorePairedViewTransitions(parent: Fiber): void {751  if ((parent.subtreeFlags & ViewTransitionNamedStatic) === NoFlags) {752    // This has no named view transitions in its subtree.753    return;754  }755  let child = parent.child;756  while (child !== null) {757    if (child.tag === OffscreenComponent && child.memoizedState !== null) {758      // This tree was already hidden so we skip it.759    } else {760      if (761        child.tag === ViewTransitionComponent &&762        (child.flags & ViewTransitionNamedStatic) !== NoFlags763      ) {764        const instance: ViewTransitionState = child.stateNode;765        if (instance.paired !== null) {766          instance.paired = null;767          restoreViewTransitionOnHostInstances(child.child, false);768        }769      }770      restorePairedViewTransitions(child);771    }772    child = child.sibling;773  }774}775776export function restoreEnterOrExitViewTransitions(fiber: Fiber): void {777  if (fiber.tag === ViewTransitionComponent) {778    const instance: ViewTransitionState = fiber.stateNode;779    instance.paired = null;780    restoreViewTransitionOnHostInstances(fiber.child, false);781    if (enableViewTransitionParentEnterExit) {782      restoreParentEnterOrExitViewTransitions(fiber);783    }784    restorePairedViewTransitions(fiber);785  } else if ((fiber.subtreeFlags & ViewTransitionStatic) !== NoFlags) {786    let child = fiber.child;787    while (child !== null) {788      restoreEnterOrExitViewTransitions(child);789      child = child.sibling;790    }791  } else {792    restorePairedViewTransitions(fiber);793  }794}795796export function restoreUpdateViewTransition(797  current: Fiber,798  finishedWork: Fiber,799): void {800  restoreViewTransitionOnHostInstances(current.child, true);801  restoreViewTransitionOnHostInstances(finishedWork.child, true);802}803804export function restoreUpdateViewTransitionForGesture(805  current: Fiber,806  finishedWork: Fiber,807): void {808  // For gestures we don't need to reset "finishedWork" because those would809  // have all been clones that got deleted.810  restoreViewTransitionOnHostInstances(current.child, true);811}812813export function restoreNestedViewTransitions(changedParent: Fiber): void {814  let child = changedParent.child;815  while (child !== null) {816    if (child.tag === ViewTransitionComponent) {817      restoreViewTransitionOnHostInstances(child.child, false);818    } else if ((child.subtreeFlags & ViewTransitionStatic) !== NoFlags) {819      restoreNestedViewTransitions(child);820    }821    child = child.sibling;822  }823}824825export function measureViewTransitionHostInstances(826  parentViewTransition: Fiber,827  child: null | Fiber,828  newName: string,829  oldName: string,830  className: ?string,831  previousMeasurements: null | Array<InstanceMeasurement>,832  stopAtNestedViewTransitions: boolean,833): boolean {834  viewTransitionHostInstanceIdx = 0;835  return measureViewTransitionHostInstancesRecursive(836    parentViewTransition,837    child,838    newName,839    oldName,840    className,841    previousMeasurements,842    stopAtNestedViewTransitions,843  );844}845846function measureViewTransitionHostInstancesRecursive(847  parentViewTransition: Fiber,848  child: null | Fiber,849  newName: string,850  oldName: string,851  className: ?string,852  previousMeasurements: null | Array<InstanceMeasurement>,853  stopAtNestedViewTransitions: boolean,854): boolean {855  // $FlowFixMe[constant-condition]856  if (!supportsMutation) {857    if (enableViewTransitionForPersistenceMode) {858      while (child !== null) {859        if (child.tag === HostComponent) {860          const instance: Instance = child.stateNode;861          if (862            previousMeasurements == null ||863            viewTransitionHostInstanceIdx >= previousMeasurements.length864          ) {865            // If there was an insertion of extra nodes, we have to assume they affected the parent.866            // It should have already been marked as an Update due to the mutation.867            parentViewTransition.flags |= AffectedParentLayout;868          }869          // TODO: check if instance is out of viewport870          if ((parentViewTransition.flags & Update) !== NoFlags) {871            applyViewTransitionName(872              instance,873              viewTransitionHostInstanceIdx === 0874                ? newName875                : newName + '_' + viewTransitionHostInstanceIdx,876              className,877            );878          }879          // TODO: cancel transition by pushing into viewTransitionCancelableChildren880          viewTransitionHostInstanceIdx++;881        } else if (882          child.tag === OffscreenComponent &&883          child.memoizedState !== null884        ) {885          // Skip any hidden subtrees. They were or are effectively not there.886        } else if (887          child.tag === ViewTransitionComponent &&888          stopAtNestedViewTransitions889        ) {890          // Skip any nested view transitions for updates since in that case the891          // inner most one is the one that handles the update.892          // If this inner boundary resized we need to bubble that information up.893          parentViewTransition.flags |= child.flags & AffectedParentLayout;894        } else {895          measureViewTransitionHostInstancesRecursive(896            parentViewTransition,897            child.child,898            newName,899            oldName,900            className,901            previousMeasurements,902            stopAtNestedViewTransitions,903          );904        }905        child = child.sibling;906      }907      return true;908    } else {909      return false;910    }911  }912  let inViewport = false;913  while (child !== null) {914    if (child.tag === HostComponent) {915      const instance: Instance = child.stateNode;916      if (917        previousMeasurements !== null &&918        viewTransitionHostInstanceIdx < previousMeasurements.length919      ) {920        // The previous measurement of the Instance in this location within the ViewTransition.921        // Note that this might not be the same exact Instance if the Instances within the922        // ViewTransition changed.923        const previousMeasurement =924          previousMeasurements[viewTransitionHostInstanceIdx];925        const nextMeasurement = measureInstance(instance);926        if (927          wasInstanceInViewport(previousMeasurement) ||928          wasInstanceInViewport(nextMeasurement)929        ) {930          // If either the old or new state was within the viewport we have to animate this.931          // But if it turns out that none of them were we'll be able to skip it.932          inViewport = true;933        }934        if (935          (parentViewTransition.flags & Update) === NoFlags &&936          hasInstanceChanged(previousMeasurement, nextMeasurement)937        ) {938          parentViewTransition.flags |= Update;939        }940        if (hasInstanceAffectedParent(previousMeasurement, nextMeasurement)) {941          // If this instance size within its parent has changed it might have caused the942          // parent to relayout which needs a cross fade.943          parentViewTransition.flags |= AffectedParentLayout;944        }945      } else {946        // If there was an insertion of extra nodes, we have to assume they affected the parent.947        // It should have already been marked as an Update due to the mutation.948        parentViewTransition.flags |= AffectedParentLayout;949      }950      if ((parentViewTransition.flags & Update) !== NoFlags) {951        // We might update this node so we need to apply its new name for the new state.952        // Additionally in the ApplyGesture case we also need to do this because the clone953        // will have the name but this one won't.954        applyViewTransitionName(955          instance,956          viewTransitionHostInstanceIdx === 0957            ? newName958            : // If we have multiple Host Instances below, we add a suffix to the name to give959              // each one a unique name.960              newName + '_' + viewTransitionHostInstanceIdx,961          className,962        );963      }964      if (!inViewport || (parentViewTransition.flags & Update) === NoFlags) {965        // It turns out that we had no other deeper mutations, the child transitions didn't966        // affect the parent layout and this instance hasn't changed size. So we can skip967        // animating it. However, in the current model this only works if the parent also968        // doesn't animate. So we have to queue these and wait until we complete the parent969        // to cancel them.970        if (viewTransitionCancelableChildren === null) {971          viewTransitionCancelableChildren = [];972        }973        viewTransitionCancelableChildren.push(974          instance,975          viewTransitionHostInstanceIdx === 0976            ? oldName977            : // If we have multiple Host Instances below, we add a suffix to the name to give978              // each one a unique name.979              oldName + '_' + viewTransitionHostInstanceIdx,980          child.memoizedProps,981        );982      }983      viewTransitionHostInstanceIdx++;984    } else if (985      child.tag === OffscreenComponent &&986      child.memoizedState !== null987    ) {988      // Skip any hidden subtrees. They were or are effectively not there.989    } else if (990      child.tag === ViewTransitionComponent &&991      stopAtNestedViewTransitions992    ) {993      // Skip any nested view transitions for updates since in that case the994      // inner most one is the one that handles the update.995      // If this inner boundary resized we need to bubble that information up.996      parentViewTransition.flags |= child.flags & AffectedParentLayout;997    } else {998      if (999        measureViewTransitionHostInstancesRecursive(1000          parentViewTransition,1001          child.child,1002          newName,1003          oldName,1004          className,1005          previousMeasurements,1006          stopAtNestedViewTransitions,1007        )1008      ) {1009        inViewport = true;1010      }1011    }1012    child = child.sibling;1013  }1014  return inViewport;1015}10161017export function measureUpdateViewTransition(1018  current: Fiber,1019  finishedWork: Fiber,1020  gesture: boolean,1021): boolean {1022  // If this was a gesture then which Fiber was used for the "old" vs "new" state is reversed.1023  // We still need to treat "finishedWork" as the Fiber that contains the flags for this commmit.1024  const oldFiber = gesture ? finishedWork : current;1025  const newFiber = gesture ? current : finishedWork;1026  const props: ViewTransitionProps = newFiber.memoizedProps;1027  const state: ViewTransitionState = newFiber.stateNode;1028  const newName = getViewTransitionName(props, state);1029  const oldName = getViewTransitionName(oldFiber.memoizedProps, state);1030  // Whether it ends up having been updated or relayout we apply the update class name.1031  const className: ?string = getViewTransitionClassName(1032    props.default,1033    props.update,1034  );1035  if (className === 'none') {1036    // If update is "none" then we don't have to apply a name. Since we won't animate this boundary.1037    return false;1038  }1039  // If nothing changed due to a mutation, or children changing size1040  // and the measurements end up unchanged, we should restore it to not animate.1041  let previousMeasurements: null | Array<InstanceMeasurement>;1042  if (gesture) {1043    const clones = state.clones;1044    if (clones === null) {1045      previousMeasurements = null;1046    } else {1047      previousMeasurements = clones.map(measureClonedInstance);1048    }1049  } else {1050    previousMeasurements = oldFiber.memoizedState;1051    oldFiber.memoizedState = null; // Clear it. We won't need it anymore.1052  }1053  const inViewport = measureViewTransitionHostInstances(1054    finishedWork, // This is always finishedWork since it's used to assign flags.1055    newFiber.child, // This either current or finishedWork depending on if was a gesture.1056    newName,1057    oldName,1058    className,1059    previousMeasurements,1060    true,1061  );1062  const previousCount =1063    previousMeasurements === null ? 0 : previousMeasurements.length;1064  if (viewTransitionHostInstanceIdx !== previousCount) {1065    // If we found a different number of child DOM nodes we need to assume that1066    // the parent layout may have changed as a result. This is not necessarily1067    // true if those nodes were absolutely positioned.1068    finishedWork.flags |= AffectedParentLayout;1069  }1070  return inViewport;1071}10721073export function measureNestedViewTransitions(1074  changedParent: Fiber,1075  gesture: boolean,1076): void {1077  let child = changedParent.child;1078  while (child !== null) {1079    if (child.tag === ViewTransitionComponent) {1080      const props: ViewTransitionProps = child.memoizedProps;1081      const state: ViewTransitionState = child.stateNode;1082      const name = getViewTransitionName(props, state);1083      const className: ?string = getViewTransitionClassName(1084        props.default,1085        props.update,1086      );1087      let previousMeasurements: null | Array<InstanceMeasurement>;1088      if (gesture) {1089        const clones = state.clones;1090        if (clones === null) {1091          previousMeasurements = null;1092        } else {1093          previousMeasurements = clones.map(measureClonedInstance);1094        }1095      } else {1096        previousMeasurements = child.memoizedState;1097        child.memoizedState = null; // Clear it. We won't need it anymore.1098      }1099      const inViewport = measureViewTransitionHostInstances(1100        child,1101        child.child,1102        name,1103        name, // Since this is unchanged, new and old name is the same.1104        className,1105        previousMeasurements,1106        false,1107      );1108      if ((child.flags & Update) === NoFlags || !inViewport) {1109        // Nothing changed.1110      } else {1111        if (gesture) {1112          scheduleGestureTransitionEvent(child, props.onGestureUpdate);1113        } else {1114          scheduleViewTransitionEvent(child, props.onUpdate);1115        }1116      }1117    } else if ((child.subtreeFlags & ViewTransitionStatic) !== NoFlags) {1118      measureNestedViewTransitions(child, gesture);1119    }1120    child = child.sibling;1121  }1122}

Code quality findings 100

Use strict equality (===) to prevent type coercion bugs
info correctness loose-equality
if (appearingViewTransitions === null) {
Use strict equality (===) to prevent type coercion bugs
info correctness loose-equality
placement.tag === ViewTransitionComponent ||
Use strict equality (===) to prevent type coercion bugs
info correctness loose-equality
(placement.subtreeFlags & ViewTransitionStatic) !== NoFlags
Use strict inequality (!==) to prevent type coercion bugs
info correctness loose-inequality
if (fiber._debugTask != null) {
Use strict equality (===) to prevent type coercion bugs
info correctness loose-equality
while (child !== null) {
Use strict equality (===) to prevent type coercion bugs
info correctness loose-equality
if (child.tag === HostComponent) {
Use strict equality (===) to prevent type coercion bugs
info correctness loose-equality
viewTransitionHostInstanceIdx === 0
Use strict equality (===) to prevent type coercion bugs
info correctness loose-equality
child.tag === OffscreenComponent &&
Use strict equality (===) to prevent type coercion bugs
info correctness loose-equality
child.memoizedState !== null
Use strict equality (===) to prevent type coercion bugs
info correctness loose-equality
child.tag === ViewTransitionComponent &&
Use strict equality (===) to prevent type coercion bugs
info correctness loose-equality
while (child !== null) {
Use strict equality (===) to prevent type coercion bugs
info correctness loose-equality
if (child.tag === HostComponent) {
Use strict equality (===) to prevent type coercion bugs
info correctness loose-equality
if (collectMeasurements !== null) {
Use strict equality (===) to prevent type coercion bugs
info correctness loose-equality
viewTransitionHostInstanceIdx === 0
Use strict equality (===) to prevent type coercion bugs
info correctness loose-equality
child.tag === OffscreenComponent &&
Use strict equality (===) to prevent type coercion bugs
info correctness loose-equality
child.memoizedState !== null
Use strict equality (===) to prevent type coercion bugs
info correctness loose-equality
child.tag === ViewTransitionComponent &&
Use strict equality (===) to prevent type coercion bugs
info correctness loose-equality
while (child !== null) {
Use strict equality (===) to prevent type coercion bugs
info correctness loose-equality
if (child.tag === HostComponent) {
Use strict equality (===) to prevent type coercion bugs
info correctness loose-equality
child.tag === OffscreenComponent &&
Use strict equality (===) to prevent type coercion bugs
info correctness loose-equality
child.memoizedState !== null
Use strict equality (===) to prevent type coercion bugs
info correctness loose-equality
child.tag === ViewTransitionComponent &&
Use strict equality (===) to prevent type coercion bugs
info correctness loose-equality
if ((placement.subtreeFlags & ViewTransitionNamedStatic) === NoFlags) {
Use strict equality (===) to prevent type coercion bugs
info correctness loose-equality
while (child !== null) {
Use strict equality (===) to prevent type coercion bugs
info correctness loose-equality
if (child.tag === OffscreenComponent && child.memoizedState !== null) {
Use strict equality (===) to prevent type coercion bugs
info correctness loose-equality
child.tag === ViewTransitionComponent &&
Use strict equality (===) to prevent type coercion bugs
info correctness loose-equality
(child.flags & ViewTransitionNamedStatic) !== NoFlags
Use strict equality (===) to prevent type coercion bugs
info correctness loose-equality
if (props.name == null || props.name === 'auto') {
Use strict equality (===) to prevent type coercion bugs
info correctness loose-equality
if (className !== 'none') {
Use strict equality (===) to prevent type coercion bugs
info correctness loose-equality
while (child !== null) {
Use strict equality (===) to prevent type coercion bugs
info correctness loose-equality
if (child.tag === OffscreenComponent && child.memoizedState !== null) {
Use strict equality (===) to prevent type coercion bugs
info correctness loose-equality
} else if (child.tag === ViewTransitionComponent) {
Use strict equality (===) to prevent type coercion bugs
info correctness loose-equality
const hasParentClass = props.parentEnter !== undefined;
Use strict inequality (!==) to prevent type coercion bugs
info correctness loose-inequality
? props.onGestureParentEnter != null
Use strict inequality (!==) to prevent type coercion bugs
info correctness loose-inequality
: props.onParentEnter != null;
Use strict equality (===) to prevent type coercion bugs
info correctness loose-equality
if (className === 'none') {
Use strict equality (===) to prevent type coercion bugs
info correctness loose-equality
} else if ((child.subtreeFlags & ViewTransitionStaticParent) !== NoFlags) {
Use strict equality (===) to prevent type coercion bugs
info correctness loose-equality
while (child !== null) {
Use strict equality (===) to prevent type coercion bugs
info correctness loose-equality
if (child.tag === OffscreenComponent && child.memoizedState !== null) {
Use strict equality (===) to prevent type coercion bugs
info correctness loose-equality
} else if (child.tag === ViewTransitionComponent) {
Use strict equality (===) to prevent type coercion bugs
info correctness loose-equality
const hasParentClass = props.parentExit !== undefined;
Use strict inequality (!==) to prevent type coercion bugs
info correctness loose-inequality
? props.onGestureParentExit != null
Use strict inequality (!==) to prevent type coercion bugs
info correctness loose-inequality
: props.onParentExit != null;
Use strict equality (===) to prevent type coercion bugs
info correctness loose-equality
if (className === 'none') {
Use strict equality (===) to prevent type coercion bugs
info correctness loose-equality
} else if ((child.subtreeFlags & ViewTransitionStaticParent) !== NoFlags) {
Use strict equality (===) to prevent type coercion bugs
info correctness loose-equality
while (child !== null) {
Use strict equality (===) to prevent type coercion bugs
info correctness loose-equality
if (child.tag === OffscreenComponent && child.memoizedState !== null) {
Use strict equality (===) to prevent type coercion bugs
info correctness loose-equality
} else if (child.tag === ViewTransitionComponent) {
Use strict equality (===) to prevent type coercion bugs
info correctness loose-equality
props.parentEnter !== undefined || props.parentExit !== undefined;
Use strict inequality (!==) to prevent type coercion bugs
info correctness loose-inequality
props.onParentEnter != null ||
Use strict inequality (!==) to prevent type coercion bugs
info correctness loose-inequality
props.onParentExit != null ||
Use strict inequality (!==) to prevent type coercion bugs
info correctness loose-inequality
props.onGestureParentEnter != null ||
Use strict inequality (!==) to prevent type coercion bugs
info correctness loose-inequality
props.onGestureParentExit != null;
Use strict equality (===) to prevent type coercion bugs
info correctness loose-equality
} else if ((child.subtreeFlags & ViewTransitionStaticParent) !== NoFlags) {
Use strict equality (===) to prevent type coercion bugs
info correctness loose-equality
if (placement.tag === ViewTransitionComponent) {
Use strict equality (===) to prevent type coercion bugs
info correctness loose-equality
if (className !== 'none') {
Use strict equality (===) to prevent type coercion bugs
info correctness loose-equality
} else if ((placement.subtreeFlags & ViewTransitionStatic) !== NoFlags) {
Use strict equality (===) to prevent type coercion bugs
info correctness loose-equality
while (child !== null) {
Use strict equality (===) to prevent type coercion bugs
info correctness loose-equality
appearingViewTransitions === null ||
Use strict equality (===) to prevent type coercion bugs
info correctness loose-equality
appearingViewTransitions.size === 0
Use strict equality (===) to prevent type coercion bugs
info correctness loose-equality
if ((deletion.subtreeFlags & ViewTransitionNamedStatic) === NoFlags) {
Use strict equality (===) to prevent type coercion bugs
info correctness loose-equality
while (child !== null) {
Use strict equality (===) to prevent type coercion bugs
info correctness loose-equality
if (child.tag === OffscreenComponent && child.memoizedState !== null) {
Use strict equality (===) to prevent type coercion bugs
info correctness loose-equality
child.tag === ViewTransitionComponent &&
Use strict equality (===) to prevent type coercion bugs
info correctness loose-equality
(child.flags & ViewTransitionNamedStatic) !== NoFlags
Use strict equality (===) to prevent type coercion bugs
info correctness loose-equality
if (name != null && name !== 'auto') {
Use strict inequality (!==) to prevent type coercion bugs
info correctness loose-inequality
if (name != null && name !== 'auto') {
Use strict equality (===) to prevent type coercion bugs
info correctness loose-equality
if (pair !== undefined) {
Use strict equality (===) to prevent type coercion bugs
info correctness loose-equality
if (className !== 'none') {
Use strict equality (===) to prevent type coercion bugs
info correctness loose-equality
if (pairs.size === 0) {
Use strict equality (===) to prevent type coercion bugs
info correctness loose-equality
if (deletion.tag === ViewTransitionComponent) {
Use strict equality (===) to prevent type coercion bugs
info correctness loose-equality
appearingViewTransitions !== null
Use strict equality (===) to prevent type coercion bugs
info correctness loose-equality
pair !== undefined ? props.share : props.exit,
Use strict equality (===) to prevent type coercion bugs
info correctness loose-equality
if (className !== 'none') {
Use strict equality (===) to prevent type coercion bugs
info correctness loose-equality
} else if (pair !== undefined) {
Use strict equality (===) to prevent type coercion bugs
info correctness loose-equality
if (appearingViewTransitions !== null) {
Use strict equality (===) to prevent type coercion bugs
info correctness loose-equality
} else if ((deletion.subtreeFlags & ViewTransitionStatic) !== NoFlags) {
Use strict equality (===) to prevent type coercion bugs
info correctness loose-equality
while (child !== null) {
Use strict equality (===) to prevent type coercion bugs
info correctness loose-equality
if (appearingViewTransitions !== null) {
Use strict equality (===) to prevent type coercion bugs
info correctness loose-equality
if (className === 'none') {
Use strict equality (===) to prevent type coercion bugs
info correctness loose-equality
while (child !== null) {
Use strict equality (===) to prevent type coercion bugs
info correctness loose-equality
if (child.tag === ViewTransitionComponent) {
Use strict equality (===) to prevent type coercion bugs
info correctness loose-equality
if (className !== 'none') {
Use strict equality (===) to prevent type coercion bugs
info correctness loose-equality
} else if ((child.subtreeFlags & ViewTransitionStatic) !== NoFlags) {
Use strict equality (===) to prevent type coercion bugs
info correctness loose-equality
if ((parent.subtreeFlags & ViewTransitionNamedStatic) === NoFlags) {
Use strict equality (===) to prevent type coercion bugs
info correctness loose-equality
while (child !== null) {
Use strict equality (===) to prevent type coercion bugs
info correctness loose-equality
if (child.tag === OffscreenComponent && child.memoizedState !== null) {
Use strict equality (===) to prevent type coercion bugs
info correctness loose-equality
child.tag === ViewTransitionComponent &&
Use strict equality (===) to prevent type coercion bugs
info correctness loose-equality
(child.flags & ViewTransitionNamedStatic) !== NoFlags
Use strict equality (===) to prevent type coercion bugs
info correctness loose-equality
if (instance.paired !== null) {
Use strict equality (===) to prevent type coercion bugs
info correctness loose-equality
if (fiber.tag === ViewTransitionComponent) {
Use strict equality (===) to prevent type coercion bugs
info correctness loose-equality
} else if ((fiber.subtreeFlags & ViewTransitionStatic) !== NoFlags) {
Use strict equality (===) to prevent type coercion bugs
info correctness loose-equality
while (child !== null) {
Use strict equality (===) to prevent type coercion bugs
info correctness loose-equality
while (child !== null) {
Use strict equality (===) to prevent type coercion bugs
info correctness loose-equality
if (child.tag === ViewTransitionComponent) {
Use strict equality (===) to prevent type coercion bugs
info correctness loose-equality
} else if ((child.subtreeFlags & ViewTransitionStatic) !== NoFlags) {
Use strict equality (===) to prevent type coercion bugs
info correctness loose-equality
while (child !== null) {
Use strict equality (===) to prevent type coercion bugs
info correctness loose-equality
if (child.tag === HostComponent) {
Use strict equality (===) to prevent type coercion bugs
info correctness loose-equality
previousMeasurements == null ||
Use strict equality (===) to prevent type coercion bugs
info correctness loose-equality
if ((parentViewTransition.flags & Update) !== NoFlags) {

Get this view in your editor

Same data, no extra tab — call code_get_file + code_get_findings over MCP from Claude/Cursor/Copilot.