@samitouri / QOS-React / commits / 3c3696d554

Measure Updated ViewTransition Boundaries (#32653)

This does the same thing for `measureUpdateViewTransition` that we did for `measureNestedViewTransitions` in https://github.com/facebook/react/pull/32612/commits/e3cbaffef05c7b476c07f7495e06788a9503e636. If a boundary hasn't mutated and didn't change in size, we mark it for cancellation. Otherwise we add names to it. The different from the CommitViewTransition path is that the "old" names are added to the clones so this is the first time the "new" names. Now we also cancel any boundaries that were unchanged. So now the root no longer animates. We still have to clone them. There are other optimizations that can avoid cloning but once we've done all the layouts we can still cancel the running animation and let them just be the regular content if they didn't change. Just like the regular fire-and-forget path. This also fixes the measurement so that we measure clones by adjusting their position back into the viewport. This actually surfaces a bug in Safari that was already in #32612. It turns out that the old names aren't picked up for some reason and so in Safari they looked more like a cross-fade than what #32612 was supposed to fix. However, now that bug is even more apparent because they actually just disappear in Safari. I'm not sure what that bug is but it's unrelated to this PR so will fix that separately.

Sebastian Markbåge committed Mar 17, 2025 at 21:38 UTC 3c3696d5548c8a67f2332fd78332b9366abaf2f9
12 files changed +197 -61
fixtures/view-transition/src/components/Page.js
+2 -2
@@ -77,9 +77,9 @@ export default function Page({url, navigate}) {
77 <div>
78 <button
79 onClick={() => {
80 - navigate(show ? '/?a' : '/?b');
80 + navigate(url === '/?b' ? '/?a' : '/?b');
81 }}>
82 - {show ? 'A' : 'B'}
82 + {url === '/?b' ? 'A' : 'B'}
83 </button>
84 <ViewTransition className="none">
85 <div>
packages/react-art/src/ReactFiberConfigART.js
+4
@@ -518,6 +518,10 @@ export function measureInstance(instance) {
518 return null;
519 }
520
521 +export function measureClonedInstance(instance) {
522 + return null;
523 +}
524 +
525 export function wasInstanceInViewport(measurement): boolean {
526 return true;
527 }
packages/react-dom-bindings/src/client/ReactFiberConfigDOM.js
+26 -4
@@ -1477,10 +1477,12 @@ export type InstanceMeasurement = {
1477 view: boolean, // is in viewport bounds
1478 };
1479
1480 -export function measureInstance(instance: Instance): InstanceMeasurement {
1481 - const ownerWindow = instance.ownerDocument.defaultView;
1482 - const rect = instance.getBoundingClientRect();
1483 - const computedStyle = getComputedStyle(instance);
1480 +function createMeasurement(
1481 + rect: ClientRect | DOMRect,
1482 + computedStyle: CSSStyleDeclaration,
1483 + element: Element,
1484 +): InstanceMeasurement {
1485 + const ownerWindow = element.ownerDocument.defaultView;
1486 return {
1487 rect: rect,
1488 abs:
@@ -1508,6 +1510,26 @@ export function measureInstance(instance: Instance): InstanceMeasurement {
1510 };
1511 }
1512
1513 +export function measureInstance(instance: Instance): InstanceMeasurement {
1514 + const rect = instance.getBoundingClientRect();
1515 + const computedStyle = getComputedStyle(instance);
1516 + return createMeasurement(rect, computedStyle, instance);
1517 +}
1518 +
1519 +export function measureClonedInstance(instance: Instance): InstanceMeasurement {
1520 + const measuredRect = instance.getBoundingClientRect();
1521 + // Adjust the DOMRect based on the translate that put it outside the viewport.
1522 + // TODO: This might not be completely correct if the parent also has a transform.
1523 + const rect = new DOMRect(
1524 + measuredRect.x + 20000,
1525 + measuredRect.y + 20000,
1526 + measuredRect.width,
1527 + measuredRect.height,
1528 + );
1529 + const computedStyle = getComputedStyle(instance);
1530 + return createMeasurement(rect, computedStyle, instance);
1531 +}
1532 +
1533 export function wasInstanceInViewport(
1534 measurement: InstanceMeasurement,
1535 ): boolean {
packages/react-native-renderer/src/ReactFiberConfigNative.js
+4
@@ -620,6 +620,10 @@ export function measureInstance(instance: Instance): InstanceMeasurement {
620 return null;
621 }
622
623 +export function measureClonedInstance(instance: Instance): InstanceMeasurement {
624 + return null;
625 +}
626 +
627 export function wasInstanceInViewport(
628 measurement: InstanceMeasurement,
629 ): boolean {
packages/react-noop-renderer/src/createReactNoop.js
+4
@@ -796,6 +796,10 @@ function createReactNoop(reconciler: Function, useMutation: boolean) {
796 return null;
797 },
798
799 + measureClonedInstance(instance: Instance): InstanceMeasurement {
800 + return null;
801 + },
802 +
803 wasInstanceInViewport(measurement: InstanceMeasurement): boolean {
804 return true;
805 },
packages/react-reconciler/src/ReactFiberApplyGesture.js
+105 -31
@@ -9,7 +9,7 @@
9
10 import type {Fiber, FiberRoot} from './ReactInternalTypes';
11
12 -import type {Instance, TextInstance} from './ReactFiberConfig';
12 +import type {Instance, TextInstance, Props} from './ReactFiberConfig';
13
14 import type {OffscreenState} from './ReactFiberActivityComponent';
15
@@ -25,6 +25,7 @@ import {
25 removeRootViewTransitionClone,
26 cancelRootViewTransitionName,
27 restoreRootViewTransitionName,
28 + cancelViewTransitionName,
29 applyViewTransitionName,
30 appendChild,
31 commitUpdate,
@@ -39,6 +40,7 @@ import {
40 popMutationContext,
41 pushMutationContext,
42 viewTransitionMutationContext,
43 + trackHostMutation,
44 } from './ReactFiberMutationTracking';
45 import {
46 MutationMask,
@@ -48,6 +50,7 @@ import {
50 Visibility,
51 ViewTransitionNamedStatic,
52 ViewTransitionStatic,
53 + AffectedParentLayout,
54 } from './ReactFiberFlags';
55 import {
56 HostComponent,
@@ -61,9 +64,14 @@ import {
64 import {
65 restoreEnterOrExitViewTransitions,
66 restoreNestedViewTransitions,
67 + restoreUpdateViewTransitionForGesture,
68 appearingViewTransitions,
69 commitEnterViewTransitions,
70 measureNestedViewTransitions,
71 + measureUpdateViewTransition,
72 + viewTransitionCancelableChildren,
73 + pushViewTransitionCancelableScope,
74 + popViewTransitionCancelableScope,
75 } from './ReactFiberCommitViewTransitions';
76 import {
77 getViewTransitionName,
@@ -72,6 +80,10 @@ import {
80
81 let didWarnForRootClone = false;
82
83 +// Used during the apply phase to track whether a parent ViewTransition component
84 +// might have been affected by any mutations / relayouts below.
85 +let viewTransitionContextChanged: boolean = false;
86 +
87 function detectMutationOrInsertClones(finishedWork: Fiber): boolean {
88 return true;
89 }
@@ -421,6 +433,7 @@ function recursivelyInsertNewFiber(
433 // For insertions we don't need to clone. It's already new state node.
434 if (visitPhase !== INSERT_APPEARING_PAIR) {
435 appendChild(hostParentClone, instance);
436 + trackHostMutation();
437 recursivelyInsertNew(
438 finishedWork,
439 instance,
@@ -450,6 +463,7 @@ function recursivelyInsertNewFiber(
463 // For insertions we don't need to clone. It's already new state node.
464 if (visitPhase !== INSERT_APPEARING_PAIR) {
465 appendChild(hostParentClone, textInstance);
466 + trackHostMutation();
467 }
468 break;
469 }
@@ -575,6 +589,7 @@ function recursivelyInsertClonesFromExistingTree(
589 }
590 if (visitPhase === CLONE_EXIT || visitPhase === CLONE_UNHIDE) {
591 unhideInstance(clone, child.memoizedProps);
592 + trackHostMutation();
593 }
594 break;
595 }
@@ -590,6 +605,7 @@ function recursivelyInsertClonesFromExistingTree(
605 appendChild(hostParentClone, clone);
606 if (visitPhase === CLONE_EXIT || visitPhase === CLONE_UNHIDE) {
607 unhideTextInstance(clone, child.memoizedProps);
608 + trackHostMutation();
609 }
610 break;
611 }
@@ -679,6 +695,10 @@ function recursivelyInsertClones(
695 for (let i = 0; i < deletions.length; i++) {
696 const childToDelete = deletions[i];
697 trackEnterViewTransitions(childToDelete);
698 + // Normally we would only mark something as triggering a mutation if there was
699 + // actually a HostInstance below here. If this tree didn't contain a HostInstances
700 + // we shouldn't trigger a mutation even though a virtual component was deleted.
701 + trackHostMutation();
702 }
703 }
704
@@ -801,6 +821,7 @@ function insertDestinationClonesOfFiber(
821 clone = cloneMutableInstance(instance, true);
822 if (finishedWork.flags & ContentReset) {
823 resetTextContent(clone);
824 + trackHostMutation();
825 }
826 } else {
827 // If we have children we'll clone them as we walk the tree so we just
@@ -825,6 +846,7 @@ function insertDestinationClonesOfFiber(
846 );
847 appendChild(hostParentClone, clone);
848 unhideInstance(clone, finishedWork.memoizedProps);
849 + trackHostMutation();
850 } else {
851 recursivelyInsertClones(finishedWork, clone, null, visitPhase);
852 appendChild(hostParentClone, clone);
@@ -851,10 +873,12 @@ function insertDestinationClonesOfFiber(
873 const newText: string = finishedWork.memoizedProps;
874 const oldText: string = current.memoizedProps;
875 commitTextUpdate(clone, newText, oldText);
876 + trackHostMutation();
877 }
878 appendChild(hostParentClone, clone);
879 if (visitPhase === CLONE_EXIT || visitPhase === CLONE_UNHIDE) {
880 unhideTextInstance(clone, finishedWork.memoizedProps);
881 + trackHostMutation();
882 }
883 break;
884 }
@@ -885,6 +909,10 @@ function insertDestinationClonesOfFiber(
909 } else if (current !== null && current.memoizedState === null) {
910 // Was previously mounted as visible but is now hidden.
911 trackEnterViewTransitions(current);
912 + // Normally we would only mark something as triggering a mutation if there was
913 + // actually a HostInstance below here. If this tree didn't contain a HostInstances
914 + // we shouldn't trigger a mutation even though a virtual component was hidden.
915 + trackHostMutation();
916 }
917 break;
918 }
@@ -991,13 +1019,6 @@ function measureExitViewTransitions(placement: Fiber): void {
1019 }
1020 }
1021
994 -function measureUpdateViewTransition(
995 - current: Fiber,
996 - finishedWork: Fiber,
997 -): void {
998 - // TODO
999 -}
1000 -
1022 function recursivelyApplyViewTransitions(parentFiber: Fiber) {
1023 const deletions = parentFiber.deletions;
1024 if (deletions !== null) {
@@ -1037,15 +1058,6 @@ function applyViewTransitionsOnFiber(finishedWork: Fiber) {
1058 // because the fiber tag is more specific. An exception is any flag related
1059 // to reconciliation, because those can be set on all fiber types.
1060 switch (finishedWork.tag) {
1040 - case HostComponent: {
1041 - // const instance: Instance = finishedWork.stateNode;
1042 - // TODO: Apply name and measure.
1043 - recursivelyApplyViewTransitions(finishedWork);
1044 - break;
1045 - }
1046 - case HostText: {
1047 - break;
1048 - }
1061 case HostPortal: {
1062 // TODO: Consider what should happen to Portals. For now we exclude them.
1063 break;
@@ -1063,12 +1075,59 @@ function applyViewTransitionsOnFiber(finishedWork: Fiber) {
1075 }
1076 break;
1077 }
1066 - case ViewTransitionComponent:
1067 - measureUpdateViewTransition(current, finishedWork);
1078 + case ViewTransitionComponent: {
1079 + const prevContextChanged = viewTransitionContextChanged;
1080 + const prevCancelableChildren = pushViewTransitionCancelableScope();
1081 + viewTransitionContextChanged = false;
1082 + recursivelyApplyViewTransitions(finishedWork);
1083 +
1084 + if (viewTransitionContextChanged) {
1085 + finishedWork.flags |= Update;
1086 + }
1087 +
1088 + const inViewport = measureUpdateViewTransition(
1089 + current,
1090 + finishedWork,
1091 + true,
1092 + );
1093 +
1094 + if ((finishedWork.flags & Update) === NoFlags || !inViewport) {
1095 + // If this boundary didn't update, then we may be able to cancel its children.
1096 + // We bubble them up to the parent set to be determined later if we can cancel.
1097 + // Similarly, if old and new state was outside the viewport, we can skip it
1098 + // even if it did update.
1099 + if (prevCancelableChildren === null) {
1100 + // Bubbling up this whole set to the parent.
1101 + } else {
1102 + // Merge with parent set.
1103 + // $FlowFixMe[method-unbinding]
1104 + prevCancelableChildren.push.apply(
1105 + prevCancelableChildren,
1106 + viewTransitionCancelableChildren,
1107 + );
1108 + popViewTransitionCancelableScope(prevCancelableChildren);
1109 + }
1110 + // TODO: If this doesn't end up canceled, because a parent animates,
1111 + // then we should probably issue an event since this instance is part of it.
1112 + } else {
1113 + // TODO: Schedule gesture events.
1114 + // If this boundary did update, we cannot cancel its children so those are dropped.
1115 + popViewTransitionCancelableScope(prevCancelableChildren);
1116 + }
1117 +
1118 + if ((finishedWork.flags & AffectedParentLayout) !== NoFlags) {
1119 + // This boundary changed size in a way that may have caused its parent to
1120 + // relayout. We need to bubble this information up to the parent.
1121 + viewTransitionContextChanged = true;
1122 + } else {
1123 + // Otherwise, we restore it to whatever the parent had found so far.
1124 + viewTransitionContextChanged = prevContextChanged;
1125 + }
1126 +
1127 const viewTransitionState: ViewTransitionState = finishedWork.stateNode;
1128 viewTransitionState.clones = null; // Reset
1070 - recursivelyApplyViewTransitions(finishedWork);
1129 break;
1130 + }
1131 default: {
1132 recursivelyApplyViewTransitions(finishedWork);
1133 break;
@@ -1082,13 +1141,38 @@ export function applyDepartureTransitions(
1141 finishedWork: Fiber,
1142 ): void {
1143 // First measure and apply view-transition-names to the "new" states.
1144 + viewTransitionContextChanged = false;
1145 + pushViewTransitionCancelableScope();
1146 +
1147 recursivelyApplyViewTransitions(finishedWork);
1148 +
1149 // Then remove the clones.
1150 const rootClone = root.gestureClone;
1151 if (rootClone !== null) {
1152 root.gestureClone = null;
1153 removeRootViewTransitionClone(root.containerInfo, rootClone);
1154 }
1155 +
1156 + if (!viewTransitionContextChanged) {
1157 + // If we didn't leak any resizing out to the root, we don't have to transition
1158 + // the root itself. This means that we can now safely cancel any cancellations
1159 + // that bubbled all the way up.
1160 + const cancelableChildren = viewTransitionCancelableChildren;
1161 + if (cancelableChildren !== null) {
1162 + for (let i = 0; i < cancelableChildren.length; i += 3) {
1163 + cancelViewTransitionName(
1164 + ((cancelableChildren[i]: any): Instance),
1165 + ((cancelableChildren[i + 1]: any): string),
1166 + ((cancelableChildren[i + 2]: any): Props),
1167 + );
1168 + }
1169 + }
1170 + // We also cancel the root itself. First we restore the name to the documentElement
1171 + // and then we cancel it.
1172 + restoreRootViewTransitionName(root.containerInfo);
1173 + cancelRootViewTransitionName(root.containerInfo);
1174 + }
1175 + popViewTransitionCancelableScope(null);
1176 }
1177
1178 function recursivelyRestoreViewTransitions(parentFiber: Fiber) {
@@ -1130,15 +1214,6 @@ function restoreViewTransitionsOnFiber(finishedWork: Fiber) {
1214 // because the fiber tag is more specific. An exception is any flag related
1215 // to reconciliation, because those can be set on all fiber types.
1216 switch (finishedWork.tag) {
1133 - case HostComponent: {
1134 - // const instance: Instance = finishedWork.stateNode;
1135 - // TODO: Restore the name.
1136 - recursivelyRestoreViewTransitions(finishedWork);
1137 - break;
1138 - }
1139 - case HostText: {
1140 - break;
1141 - }
1217 case HostPortal: {
1218 // TODO: Consider what should happen to Portals. For now we exclude them.
1219 break;
@@ -1157,8 +1232,7 @@ function restoreViewTransitionsOnFiber(finishedWork: Fiber) {
1232 break;
1233 }
1234 case ViewTransitionComponent:
1160 - const viewTransitionState: ViewTransitionState = finishedWork.stateNode;
1161 - viewTransitionState.clones = null; // Reset
1235 + restoreUpdateViewTransitionForGesture(current, finishedWork);
1236 recursivelyRestoreViewTransitions(finishedWork);
1237 break;
1238 default: {
packages/react-reconciler/src/ReactFiberCommitHostEffects.js
+1
@@ -199,6 +199,7 @@ export function commitShowHideHostTextInstance(node: Fiber, isHidden: boolean) {
199 unhideTextInstance(instance, node.memoizedProps);
200 }
201 }
202 + trackHostMutation();
203 } catch (error) {
204 captureCommitPhaseError(node, node.return, error);
205 }
packages/react-reconciler/src/ReactFiberCommitViewTransitions.js
+38 -13
@@ -31,6 +31,7 @@ import {
31 applyViewTransitionName,
32 restoreViewTransitionName,
33 measureInstance,
34 + measureClonedInstance,
35 hasInstanceChanged,
36 hasInstanceAffectedParent,
37 wasInstanceInViewport,
@@ -564,16 +565,23 @@ export function restoreUpdateViewTransition(
565 current: Fiber,
566 finishedWork: Fiber,
567 ): void {
567 - finishedWork.memoizedState = null;
568 restoreViewTransitionOnHostInstances(current.child, true);
569 restoreViewTransitionOnHostInstances(finishedWork.child, true);
570 }
571
572 +export function restoreUpdateViewTransitionForGesture(
573 + current: Fiber,
574 + finishedWork: Fiber,
575 +): void {
576 + // For gestures we don't need to reset "finishedWork" because those would
577 + // have all been clones that got deleted.
578 + restoreViewTransitionOnHostInstances(current.child, true);
579 +}
580 +
581 export function restoreNestedViewTransitions(changedParent: Fiber): void {
582 let child = changedParent.child;
583 while (child !== null) {
584 if (child.tag === ViewTransitionComponent) {
576 - child.memoizedState = null;
585 restoreViewTransitionOnHostInstances(child.child, false);
586 } else if ((child.subtreeFlags & ViewTransitionStatic) !== NoFlags) {
587 restoreNestedViewTransitions(child);
@@ -774,13 +782,16 @@ function measureViewTransitionHostInstancesRecursive(
782 export function measureUpdateViewTransition(
783 current: Fiber,
784 finishedWork: Fiber,
785 + gesture: boolean,
786 ): boolean {
778 - const props: ViewTransitionProps = finishedWork.memoizedProps;
779 - const newName = getViewTransitionName(props, finishedWork.stateNode);
780 - const oldName = getViewTransitionName(
781 - current.memoizedProps,
782 - current.stateNode,
783 - );
787 + // If this was a gesture then which Fiber was used for the "old" vs "new" state is reversed.
788 + // We still need to treat "finishedWork" as the Fiber that contains the flags for this commmit.
789 + const oldFiber = gesture ? finishedWork : current;
790 + const newFiber = gesture ? current : finishedWork;
791 + const props: ViewTransitionProps = newFiber.memoizedProps;
792 + const state: ViewTransitionState = newFiber.stateNode;
793 + const newName = getViewTransitionName(props, state);
794 + const oldName = getViewTransitionName(oldFiber.memoizedProps, state);
795 const updateClassName: ?string = getViewTransitionClassName(
796 props.className,
797 props.update,
@@ -807,7 +818,9 @@ export function measureUpdateViewTransition(
818 if (layoutClassName === 'none') {
819 // If we did not update, then all changes are considered a layout. We'll
820 // attempt to cancel.
810 - cancelViewTransitionHostInstances(finishedWork.child, oldName, true);
821 + // This should use the Fiber that got names applied in the snapshot phase
822 + // since those are the ones we're trying to cancel.
823 + cancelViewTransitionHostInstances(oldFiber.child, oldName, true);
824 return false;
825 }
826 // We didn't update but we might still apply layout so we measure each
@@ -816,10 +829,21 @@ export function measureUpdateViewTransition(
829 }
830 // If nothing changed due to a mutation, or children changing size
831 // and the measurements end up unchanged, we should restore it to not animate.
819 - const previousMeasurements = current.memoizedState;
832 + let previousMeasurements: null | Array<InstanceMeasurement>;
833 + if (gesture) {
834 + const clones = state.clones;
835 + if (clones === null) {
836 + previousMeasurements = null;
837 + } else {
838 + previousMeasurements = clones.map(measureClonedInstance);
839 + }
840 + } else {
841 + previousMeasurements = oldFiber.memoizedState;
842 + oldFiber.memoizedState = null; // Clear it. We won't need it anymore.
843 + }
844 const inViewport = measureViewTransitionHostInstances(
821 - finishedWork,
822 - finishedWork.child,
845 + finishedWork, // This is always finishedWork since it's used to assign flags.
846 + newFiber.child, // This either current or finishedWork depending on if was a gesture.
847 newName,
848 oldName,
849 className,
@@ -857,10 +881,11 @@ export function measureNestedViewTransitions(
881 if (clones === null) {
882 previousMeasurements = null;
883 } else {
860 - previousMeasurements = clones.map(measureInstance);
884 + previousMeasurements = clones.map(measureClonedInstance);
885 }
886 } else {
887 previousMeasurements = child.memoizedState;
888 + child.memoizedState = null; // Clear it. We won't need it anymore.
889 }
890 const inViewport = measureViewTransitionHostInstances(
891 child,
packages/react-reconciler/src/ReactFiberCommitWork.js
+7 -11
@@ -2474,7 +2474,6 @@ function commitAfterMutationEffectsOnFiber(
2474 // the root itself. This means that we can now safely cancel any cancellations
2475 // that bubbled all the way up.
2476 const cancelableChildren = viewTransitionCancelableChildren;
2477 - popViewTransitionCancelableScope(null);
2477 if (cancelableChildren !== null) {
2478 for (let i = 0; i < cancelableChildren.length; i += 3) {
2479 cancelViewTransitionName(
@@ -2487,6 +2486,7 @@ function commitAfterMutationEffectsOnFiber(
2486 // We also cancel the root itself.
2487 cancelRootViewTransitionName(root.containerInfo);
2488 }
2489 + popViewTransitionCancelableScope(null);
2490 break;
2491 }
2492 case HostComponent: {
@@ -2528,7 +2528,11 @@ function commitAfterMutationEffectsOnFiber(
2528 finishedWork.flags |= Update;
2529 }
2530
2531 - const inViewport = measureUpdateViewTransition(current, finishedWork);
2531 + const inViewport = measureUpdateViewTransition(
2532 + current,
2533 + finishedWork,
2534 + false,
2535 + );
2536
2537 if ((finishedWork.flags & Update) === NoFlags || !inViewport) {
2538 // If this boundary didn't update, then we may be able to cancel its children.
@@ -3618,15 +3622,7 @@ function commitPassiveMountOnFiber(
3622 if (current === null) {
3623 // This is a new mount. We should have handled this as part of the
3624 // Placement effect or it is deeper inside a entering transition.
3621 - } else if (
3622 - (finishedWork.subtreeFlags &
3623 - (Placement |
3624 - Update |
3625 - ChildDeletion |
3626 - ContentReset |
3627 - Visibility)) !==
3628 - NoFlags
3629 - ) {
3625 + } else {
3626 // Something mutated within this subtree. This might have caused
3627 // something to cross-fade if we didn't already cancel it.
3628 // If not, restore it.
packages/react-reconciler/src/ReactFiberConfigWithNoMutation.js
+1
@@ -46,6 +46,7 @@ export const cloneRootViewTransitionContainer = shim;
46 export const removeRootViewTransitionClone = shim;
47 export type InstanceMeasurement = null;
48 export const measureInstance = shim;
49 +export const measureClonedInstance = shim;
50 export const wasInstanceInViewport = shim;
51 export const hasInstanceChanged = shim;
52 export const hasInstanceAffectedParent = shim;
packages/react-reconciler/src/forks/ReactFiberConfig.custom.js
+1
@@ -149,6 +149,7 @@ export const cloneRootViewTransitionContainer =
149 export const removeRootViewTransitionClone =
150 $$$config.removeRootViewTransitionClone;
151 export const measureInstance = $$$config.measureInstance;
152 +export const measureClonedInstance = $$$config.measureClonedInstance;
153 export const wasInstanceInViewport = $$$config.wasInstanceInViewport;
154 export const hasInstanceChanged = $$$config.hasInstanceChanged;
155 export const hasInstanceAffectedParent = $$$config.hasInstanceAffectedParent;
packages/react-test-renderer/src/ReactFiberConfigTestHost.js
+4
@@ -389,6 +389,10 @@ export function measureInstance(instance: Instance): InstanceMeasurement {
389 return null;
390 }
391
392 +export function measureClonedInstance(instance: Instance): InstanceMeasurement {
393 + return null;
394 +}
395 +
396 export function wasInstanceInViewport(
397 measurement: InstanceMeasurement,
398 ): boolean {