react/packages/react-reconciler/src/ReactFiberCommitHostEffects.js
Sebastian Markbåge 3c3696d554
Measure Updated ViewTransition Boundaries (#32653)
This does the same thing for `measureUpdateViewTransition` that we did
for `measureNestedViewTransitions` in
e3cbaffef0.
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.
2025-03-17 21:38:13 -04:00

720 lines
18 KiB
JavaScript

/**
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
* @flow
*/
import type {
Instance,
TextInstance,
SuspenseInstance,
Container,
ChildSet,
FragmentInstanceType,
} from './ReactFiberConfig';
import type {Fiber, FiberRoot} from './ReactInternalTypes';
import {
HostRoot,
HostComponent,
HostHoistable,
HostSingleton,
HostText,
HostPortal,
DehydratedFragment,
Fragment,
} from './ReactWorkTags';
import {ContentReset, Placement} from './ReactFiberFlags';
import {
supportsMutation,
supportsResources,
supportsSingletons,
commitMount,
commitUpdate,
resetTextContent,
commitTextUpdate,
appendChild,
appendChildToContainer,
insertBefore,
insertInContainerBefore,
replaceContainerChildren,
hideInstance,
hideTextInstance,
unhideInstance,
unhideTextInstance,
commitHydratedContainer,
commitHydratedSuspenseInstance,
removeChildFromContainer,
removeChild,
acquireSingletonInstance,
releaseSingletonInstance,
isSingletonScope,
commitNewChildToFragmentInstance,
deleteChildFromFragmentInstance,
} from './ReactFiberConfig';
import {captureCommitPhaseError} from './ReactFiberWorkLoop';
import {trackHostMutation} from './ReactFiberMutationTracking';
import {runWithFiberInDEV} from './ReactCurrentFiber';
import {enableFragmentRefs} from 'shared/ReactFeatureFlags';
export function commitHostMount(finishedWork: Fiber) {
const type = finishedWork.type;
const props = finishedWork.memoizedProps;
const instance: Instance = finishedWork.stateNode;
try {
if (__DEV__) {
runWithFiberInDEV(
finishedWork,
commitMount,
instance,
type,
props,
finishedWork,
);
} else {
commitMount(instance, type, props, finishedWork);
}
} catch (error) {
captureCommitPhaseError(finishedWork, finishedWork.return, error);
}
}
export function commitHostUpdate(
finishedWork: Fiber,
newProps: any,
oldProps: any,
): void {
try {
if (__DEV__) {
runWithFiberInDEV(
finishedWork,
commitUpdate,
finishedWork.stateNode,
finishedWork.type,
oldProps,
newProps,
finishedWork,
);
} else {
commitUpdate(
finishedWork.stateNode,
finishedWork.type,
oldProps,
newProps,
finishedWork,
);
}
// Mutations are tracked manually from within commitUpdate.
} catch (error) {
captureCommitPhaseError(finishedWork, finishedWork.return, error);
}
}
export function commitHostTextUpdate(
finishedWork: Fiber,
newText: string,
oldText: string,
) {
const textInstance: TextInstance = finishedWork.stateNode;
try {
if (__DEV__) {
runWithFiberInDEV(
finishedWork,
commitTextUpdate,
textInstance,
oldText,
newText,
);
} else {
commitTextUpdate(textInstance, oldText, newText);
}
trackHostMutation();
} catch (error) {
captureCommitPhaseError(finishedWork, finishedWork.return, error);
}
}
export function commitHostResetTextContent(finishedWork: Fiber) {
const instance: Instance = finishedWork.stateNode;
try {
if (__DEV__) {
runWithFiberInDEV(finishedWork, resetTextContent, instance);
} else {
resetTextContent(instance);
}
trackHostMutation();
} catch (error) {
captureCommitPhaseError(finishedWork, finishedWork.return, error);
}
}
export function commitShowHideHostInstance(node: Fiber, isHidden: boolean) {
try {
const instance = node.stateNode;
if (isHidden) {
if (__DEV__) {
runWithFiberInDEV(node, hideInstance, instance);
} else {
hideInstance(instance);
}
} else {
if (__DEV__) {
runWithFiberInDEV(
node,
unhideInstance,
node.stateNode,
node.memoizedProps,
);
} else {
unhideInstance(node.stateNode, node.memoizedProps);
}
}
} catch (error) {
captureCommitPhaseError(node, node.return, error);
}
}
export function commitShowHideHostTextInstance(node: Fiber, isHidden: boolean) {
try {
const instance = node.stateNode;
if (isHidden) {
if (__DEV__) {
runWithFiberInDEV(node, hideTextInstance, instance);
} else {
hideTextInstance(instance);
}
} else {
if (__DEV__) {
runWithFiberInDEV(
node,
unhideTextInstance,
instance,
node.memoizedProps,
);
} else {
unhideTextInstance(instance, node.memoizedProps);
}
}
trackHostMutation();
} catch (error) {
captureCommitPhaseError(node, node.return, error);
}
}
export function commitNewChildToFragmentInstances(
fiber: Fiber,
parentFragmentInstances: Array<FragmentInstanceType>,
): void {
for (let i = 0; i < parentFragmentInstances.length; i++) {
const fragmentInstance = parentFragmentInstances[i];
commitNewChildToFragmentInstance(fiber.stateNode, fragmentInstance);
}
}
export function commitFragmentInstanceInsertionEffects(fiber: Fiber): void {
let parent = fiber.return;
while (parent !== null) {
if (isFragmentInstanceParent(parent)) {
const fragmentInstance: FragmentInstanceType = parent.stateNode;
commitNewChildToFragmentInstance(fiber.stateNode, fragmentInstance);
}
if (isHostParent(parent)) {
return;
}
parent = parent.return;
}
}
export function commitFragmentInstanceDeletionEffects(fiber: Fiber): void {
let parent = fiber.return;
while (parent !== null) {
if (isFragmentInstanceParent(parent)) {
const fragmentInstance: FragmentInstanceType = parent.stateNode;
deleteChildFromFragmentInstance(fiber.stateNode, fragmentInstance);
}
if (isHostParent(parent)) {
return;
}
parent = parent.return;
}
}
function isHostParent(fiber: Fiber): boolean {
return (
fiber.tag === HostComponent ||
fiber.tag === HostRoot ||
(supportsResources ? fiber.tag === HostHoistable : false) ||
(supportsSingletons
? fiber.tag === HostSingleton && isSingletonScope(fiber.type)
: false) ||
fiber.tag === HostPortal
);
}
function isFragmentInstanceParent(fiber: Fiber): boolean {
return fiber && fiber.tag === Fragment && fiber.stateNode !== null;
}
function getHostSibling(fiber: Fiber): ?Instance {
// We're going to search forward into the tree until we find a sibling host
// node. Unfortunately, if multiple insertions are done in a row we have to
// search past them. This leads to exponential search for the next sibling.
// TODO: Find a more efficient way to do this.
let node: Fiber = fiber;
siblings: while (true) {
// If we didn't find anything, let's try the next sibling.
while (node.sibling === null) {
if (node.return === null || isHostParent(node.return)) {
// If we pop out of the root or hit the parent the fiber we are the
// last sibling.
return null;
}
// $FlowFixMe[incompatible-type] found when upgrading Flow
node = node.return;
}
node.sibling.return = node.return;
node = node.sibling;
while (
node.tag !== HostComponent &&
node.tag !== HostText &&
node.tag !== DehydratedFragment
) {
// If this is a host singleton we go deeper if it's not a special
// singleton scope. If it is a singleton scope we skip over it because
// you only insert against this scope when you are already inside of it
if (
supportsSingletons &&
node.tag === HostSingleton &&
isSingletonScope(node.type)
) {
continue siblings;
}
// If it is not host node and, we might have a host node inside it.
// Try to search down until we find one.
if (node.flags & Placement) {
// If we don't have a child, try the siblings instead.
continue siblings;
}
// If we don't have a child, try the siblings instead.
// We also skip portals because they are not part of this host tree.
if (node.child === null || node.tag === HostPortal) {
continue siblings;
} else {
node.child.return = node;
node = node.child;
}
}
// Check if this host node is stable or about to be placed.
if (!(node.flags & Placement)) {
// Found it!
return node.stateNode;
}
}
}
function insertOrAppendPlacementNodeIntoContainer(
node: Fiber,
before: ?Instance,
parent: Container,
parentFragmentInstances: null | Array<FragmentInstanceType>,
): void {
const {tag} = node;
const isHost = tag === HostComponent || tag === HostText;
if (isHost) {
const stateNode = node.stateNode;
if (before) {
insertInContainerBefore(parent, stateNode, before);
} else {
appendChildToContainer(parent, stateNode);
}
// TODO: Enable HostText for RN
if (
enableFragmentRefs &&
tag === HostComponent &&
// Only run fragment insertion effects for initial insertions
node.alternate === null &&
parentFragmentInstances !== null
) {
commitNewChildToFragmentInstances(node, parentFragmentInstances);
}
trackHostMutation();
return;
} else if (tag === HostPortal) {
// If the insertion itself is a portal, then we don't want to traverse
// down its children. Instead, we'll get insertions from each child in
// the portal directly.
return;
}
if (
(supportsSingletons ? tag === HostSingleton : false) &&
isSingletonScope(node.type)
) {
// This singleton is the parent of deeper nodes and needs to become
// the parent for child insertions and appends
parent = node.stateNode;
before = null;
}
const child = node.child;
if (child !== null) {
insertOrAppendPlacementNodeIntoContainer(
child,
before,
parent,
parentFragmentInstances,
);
let sibling = child.sibling;
while (sibling !== null) {
insertOrAppendPlacementNodeIntoContainer(
sibling,
before,
parent,
parentFragmentInstances,
);
sibling = sibling.sibling;
}
}
}
function insertOrAppendPlacementNode(
node: Fiber,
before: ?Instance,
parent: Instance,
parentFragmentInstances: null | Array<FragmentInstanceType>,
): void {
const {tag} = node;
const isHost = tag === HostComponent || tag === HostText;
if (isHost) {
const stateNode = node.stateNode;
if (before) {
insertBefore(parent, stateNode, before);
} else {
appendChild(parent, stateNode);
}
// TODO: Enable HostText for RN
if (
enableFragmentRefs &&
tag === HostComponent &&
// Only run fragment insertion effects for initial insertions
node.alternate === null &&
parentFragmentInstances !== null
) {
commitNewChildToFragmentInstances(node, parentFragmentInstances);
}
trackHostMutation();
return;
} else if (tag === HostPortal) {
// If the insertion itself is a portal, then we don't want to traverse
// down its children. Instead, we'll get insertions from each child in
// the portal directly.
return;
}
if (
(supportsSingletons ? tag === HostSingleton : false) &&
isSingletonScope(node.type)
) {
// This singleton is the parent of deeper nodes and needs to become
// the parent for child insertions and appends
parent = node.stateNode;
}
const child = node.child;
if (child !== null) {
insertOrAppendPlacementNode(child, before, parent, parentFragmentInstances);
let sibling = child.sibling;
while (sibling !== null) {
insertOrAppendPlacementNode(
sibling,
before,
parent,
parentFragmentInstances,
);
sibling = sibling.sibling;
}
}
}
function commitPlacement(finishedWork: Fiber): void {
if (!supportsMutation) {
return;
}
// Recursively insert all host nodes into the parent.
let hostParentFiber;
let parentFragmentInstances = null;
let parentFiber = finishedWork.return;
while (parentFiber !== null) {
if (enableFragmentRefs && isFragmentInstanceParent(parentFiber)) {
const fragmentInstance: FragmentInstanceType = parentFiber.stateNode;
if (parentFragmentInstances === null) {
parentFragmentInstances = [fragmentInstance];
} else {
parentFragmentInstances.push(fragmentInstance);
}
}
if (isHostParent(parentFiber)) {
hostParentFiber = parentFiber;
break;
}
parentFiber = parentFiber.return;
}
if (hostParentFiber == null) {
throw new Error(
'Expected to find a host parent. This error is likely caused by a bug ' +
'in React. Please file an issue.',
);
}
switch (hostParentFiber.tag) {
case HostSingleton: {
if (supportsSingletons) {
const parent: Instance = hostParentFiber.stateNode;
const before = getHostSibling(finishedWork);
// We only have the top Fiber that was inserted but we need to recurse down its
// children to find all the terminal nodes.
insertOrAppendPlacementNode(
finishedWork,
before,
parent,
parentFragmentInstances,
);
break;
}
// Fall through
}
case HostComponent: {
const parent: Instance = hostParentFiber.stateNode;
if (hostParentFiber.flags & ContentReset) {
// Reset the text content of the parent before doing any insertions
resetTextContent(parent);
// Clear ContentReset from the effect tag
hostParentFiber.flags &= ~ContentReset;
}
const before = getHostSibling(finishedWork);
// We only have the top Fiber that was inserted but we need to recurse down its
// children to find all the terminal nodes.
insertOrAppendPlacementNode(
finishedWork,
before,
parent,
parentFragmentInstances,
);
break;
}
case HostRoot:
case HostPortal: {
const parent: Container = hostParentFiber.stateNode.containerInfo;
const before = getHostSibling(finishedWork);
insertOrAppendPlacementNodeIntoContainer(
finishedWork,
before,
parent,
parentFragmentInstances,
);
break;
}
default:
throw new Error(
'Invalid host parent fiber. This error is likely caused by a bug ' +
'in React. Please file an issue.',
);
}
}
export function commitHostPlacement(finishedWork: Fiber) {
try {
if (__DEV__) {
runWithFiberInDEV(finishedWork, commitPlacement, finishedWork);
} else {
commitPlacement(finishedWork);
}
} catch (error) {
captureCommitPhaseError(finishedWork, finishedWork.return, error);
}
}
export function commitHostRemoveChildFromContainer(
deletedFiber: Fiber,
nearestMountedAncestor: Fiber,
parentContainer: Container,
hostInstance: Instance | TextInstance,
) {
try {
if (__DEV__) {
runWithFiberInDEV(
deletedFiber,
removeChildFromContainer,
parentContainer,
hostInstance,
);
} else {
removeChildFromContainer(parentContainer, hostInstance);
}
trackHostMutation();
} catch (error) {
captureCommitPhaseError(deletedFiber, nearestMountedAncestor, error);
}
}
export function commitHostRemoveChild(
deletedFiber: Fiber,
nearestMountedAncestor: Fiber,
parentInstance: Instance,
hostInstance: Instance | TextInstance,
) {
try {
if (__DEV__) {
runWithFiberInDEV(
deletedFiber,
removeChild,
parentInstance,
hostInstance,
);
} else {
removeChild(parentInstance, hostInstance);
}
trackHostMutation();
} catch (error) {
captureCommitPhaseError(deletedFiber, nearestMountedAncestor, error);
}
}
export function commitHostRootContainerChildren(
root: FiberRoot,
finishedWork: Fiber,
) {
const containerInfo = root.containerInfo;
const pendingChildren = root.pendingChildren;
try {
if (__DEV__) {
runWithFiberInDEV(
finishedWork,
replaceContainerChildren,
containerInfo,
pendingChildren,
);
} else {
replaceContainerChildren(containerInfo, pendingChildren);
}
trackHostMutation();
} catch (error) {
captureCommitPhaseError(finishedWork, finishedWork.return, error);
}
}
export function commitHostPortalContainerChildren(
portal: {
containerInfo: Container,
pendingChildren: ChildSet,
...
},
finishedWork: Fiber,
pendingChildren: ChildSet,
) {
const containerInfo = portal.containerInfo;
try {
if (__DEV__) {
runWithFiberInDEV(
finishedWork,
replaceContainerChildren,
containerInfo,
pendingChildren,
);
} else {
replaceContainerChildren(containerInfo, pendingChildren);
}
} catch (error) {
captureCommitPhaseError(finishedWork, finishedWork.return, error);
}
}
export function commitHostHydratedContainer(
root: FiberRoot,
finishedWork: Fiber,
) {
try {
if (__DEV__) {
runWithFiberInDEV(
finishedWork,
commitHydratedContainer,
root.containerInfo,
);
} else {
commitHydratedContainer(root.containerInfo);
}
} catch (error) {
captureCommitPhaseError(finishedWork, finishedWork.return, error);
}
}
export function commitHostHydratedSuspense(
suspenseInstance: SuspenseInstance,
finishedWork: Fiber,
) {
try {
if (__DEV__) {
runWithFiberInDEV(
finishedWork,
commitHydratedSuspenseInstance,
suspenseInstance,
);
} else {
commitHydratedSuspenseInstance(suspenseInstance);
}
} catch (error) {
captureCommitPhaseError(finishedWork, finishedWork.return, error);
}
}
export function commitHostSingletonAcquisition(finishedWork: Fiber) {
const singleton = finishedWork.stateNode;
const props = finishedWork.memoizedProps;
try {
// This was a new mount, acquire the DOM instance and set initial properties
if (__DEV__) {
runWithFiberInDEV(
finishedWork,
acquireSingletonInstance,
finishedWork.type,
props,
singleton,
finishedWork,
);
} else {
acquireSingletonInstance(
finishedWork.type,
props,
singleton,
finishedWork,
);
}
} catch (error) {
captureCommitPhaseError(finishedWork, finishedWork.return, error);
}
}
export function commitHostSingletonRelease(releasingWork: Fiber) {
if (__DEV__) {
runWithFiberInDEV(
releasingWork,
releaseSingletonInstance,
releasingWork.stateNode,
);
} else {
releaseSingletonInstance(releasingWork.stateNode);
}
}