react/packages/react-reconciler/src/ReactFiberTreeReflection.js
Jack Pope 408d055a3b
Add Fragment Refs to Fabric with intersection observer support (#33056)
Adds Fragment Ref support to RN through the Fabric config, starting with
`observeUsing`/`unobserveUsing`. This is mostly a copy from the
implementation on DOM, and some of it can likely be shared in the future
but keeping it separate for now and we can refactor as we add more
features.

Added a basic test with Fabric, but testing specific methods requires so
much mocking that it doesn't seem valuable here.

I built Fabric and ran on the Catalyst app internally to test with
intersection observers end to end.
2025-04-30 10:47:18 -04:00

396 lines
11 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 {Fiber} from './ReactInternalTypes';
import type {
Container,
ActivityInstance,
SuspenseInstance,
Instance,
} from './ReactFiberConfig';
import type {ActivityState} from './ReactFiberActivityComponent';
import type {SuspenseState} from './ReactFiberSuspenseComponent';
import {
HostComponent,
HostHoistable,
HostSingleton,
HostRoot,
HostPortal,
HostText,
ActivityComponent,
SuspenseComponent,
OffscreenComponent,
} from './ReactWorkTags';
import {NoFlags, Placement, Hydrating} from './ReactFiberFlags';
export function getNearestMountedFiber(fiber: Fiber): null | Fiber {
let node = fiber;
let nearestMounted: null | Fiber = fiber;
if (!fiber.alternate) {
// If there is no alternate, this might be a new tree that isn't inserted
// yet. If it is, then it will have a pending insertion effect on it.
let nextNode: Fiber = node;
do {
node = nextNode;
if ((node.flags & (Placement | Hydrating)) !== NoFlags) {
// This is an insertion or in-progress hydration. The nearest possible
// mounted fiber is the parent but we need to continue to figure out
// if that one is still mounted.
nearestMounted = node.return;
}
// $FlowFixMe[incompatible-type] we bail out when we get a null
nextNode = node.return;
} while (nextNode);
} else {
while (node.return) {
node = node.return;
}
}
if (node.tag === HostRoot) {
// TODO: Check if this was a nested HostRoot when used with
// renderContainerIntoSubtree.
return nearestMounted;
}
// If we didn't hit the root, that means that we're in an disconnected tree
// that has been unmounted.
return null;
}
export function getSuspenseInstanceFromFiber(
fiber: Fiber,
): null | SuspenseInstance {
if (fiber.tag === SuspenseComponent) {
let suspenseState: SuspenseState | null = fiber.memoizedState;
if (suspenseState === null) {
const current = fiber.alternate;
if (current !== null) {
suspenseState = current.memoizedState;
}
}
if (suspenseState !== null) {
return suspenseState.dehydrated;
}
}
return null;
}
export function getActivityInstanceFromFiber(
fiber: Fiber,
): null | ActivityInstance {
if (fiber.tag === ActivityComponent) {
let activityState: ActivityState | null = fiber.memoizedState;
if (activityState === null) {
const current = fiber.alternate;
if (current !== null) {
activityState = current.memoizedState;
}
}
if (activityState !== null) {
return activityState.dehydrated;
}
}
// TODO: Implement this on ActivityComponent.
return null;
}
export function getContainerFromFiber(fiber: Fiber): null | Container {
return fiber.tag === HostRoot
? (fiber.stateNode.containerInfo: Container)
: null;
}
function assertIsMounted(fiber: Fiber) {
if (getNearestMountedFiber(fiber) !== fiber) {
throw new Error('Unable to find node on an unmounted component.');
}
}
export function findCurrentFiberUsingSlowPath(fiber: Fiber): Fiber | null {
const alternate = fiber.alternate;
if (!alternate) {
// If there is no alternate, then we only need to check if it is mounted.
const nearestMounted = getNearestMountedFiber(fiber);
if (nearestMounted === null) {
throw new Error('Unable to find node on an unmounted component.');
}
if (nearestMounted !== fiber) {
return null;
}
return fiber;
}
// If we have two possible branches, we'll walk backwards up to the root
// to see what path the root points to. On the way we may hit one of the
// special cases and we'll deal with them.
let a: Fiber = fiber;
let b: Fiber = alternate;
while (true) {
const parentA = a.return;
if (parentA === null) {
// We're at the root.
break;
}
const parentB = parentA.alternate;
if (parentB === null) {
// There is no alternate. This is an unusual case. Currently, it only
// happens when a Suspense component is hidden. An extra fragment fiber
// is inserted in between the Suspense fiber and its children. Skip
// over this extra fragment fiber and proceed to the next parent.
const nextParent = parentA.return;
if (nextParent !== null) {
a = b = nextParent;
continue;
}
// If there's no parent, we're at the root.
break;
}
// If both copies of the parent fiber point to the same child, we can
// assume that the child is current. This happens when we bailout on low
// priority: the bailed out fiber's child reuses the current child.
if (parentA.child === parentB.child) {
let child = parentA.child;
while (child) {
if (child === a) {
// We've determined that A is the current branch.
assertIsMounted(parentA);
return fiber;
}
if (child === b) {
// We've determined that B is the current branch.
assertIsMounted(parentA);
return alternate;
}
child = child.sibling;
}
// We should never have an alternate for any mounting node. So the only
// way this could possibly happen is if this was unmounted, if at all.
throw new Error('Unable to find node on an unmounted component.');
}
if (a.return !== b.return) {
// The return pointer of A and the return pointer of B point to different
// fibers. We assume that return pointers never criss-cross, so A must
// belong to the child set of A.return, and B must belong to the child
// set of B.return.
a = parentA;
b = parentB;
} else {
// The return pointers point to the same fiber. We'll have to use the
// default, slow path: scan the child sets of each parent alternate to see
// which child belongs to which set.
//
// Search parent A's child set
let didFindChild = false;
let child = parentA.child;
while (child) {
if (child === a) {
didFindChild = true;
a = parentA;
b = parentB;
break;
}
if (child === b) {
didFindChild = true;
b = parentA;
a = parentB;
break;
}
child = child.sibling;
}
if (!didFindChild) {
// Search parent B's child set
child = parentB.child;
while (child) {
if (child === a) {
didFindChild = true;
a = parentB;
b = parentA;
break;
}
if (child === b) {
didFindChild = true;
b = parentB;
a = parentA;
break;
}
child = child.sibling;
}
if (!didFindChild) {
throw new Error(
'Child was not found in either parent set. This indicates a bug ' +
'in React related to the return pointer. Please file an issue.',
);
}
}
}
if (a.alternate !== b) {
throw new Error(
"Return fibers should always be each others' alternates. " +
'This error is likely caused by a bug in React. Please file an issue.',
);
}
}
// If the root is not a host container, we're in a disconnected tree. I.e.
// unmounted.
if (a.tag !== HostRoot) {
throw new Error('Unable to find node on an unmounted component.');
}
if (a.stateNode.current === a) {
// We've determined that A is the current branch.
return fiber;
}
// Otherwise B has to be current branch.
return alternate;
}
export function findCurrentHostFiber(parent: Fiber): Fiber | null {
const currentParent = findCurrentFiberUsingSlowPath(parent);
return currentParent !== null
? findCurrentHostFiberImpl(currentParent)
: null;
}
function findCurrentHostFiberImpl(node: Fiber): Fiber | null {
// Next we'll drill down this component to find the first HostComponent/Text.
const tag = node.tag;
if (
tag === HostComponent ||
tag === HostHoistable ||
tag === HostSingleton ||
tag === HostText
) {
return node;
}
let child = node.child;
while (child !== null) {
const match = findCurrentHostFiberImpl(child);
if (match !== null) {
return match;
}
child = child.sibling;
}
return null;
}
export function findCurrentHostFiberWithNoPortals(parent: Fiber): Fiber | null {
const currentParent = findCurrentFiberUsingSlowPath(parent);
return currentParent !== null
? findCurrentHostFiberWithNoPortalsImpl(currentParent)
: null;
}
function findCurrentHostFiberWithNoPortalsImpl(node: Fiber): Fiber | null {
// Next we'll drill down this component to find the first HostComponent/Text.
const tag = node.tag;
if (
tag === HostComponent ||
tag === HostHoistable ||
tag === HostSingleton ||
tag === HostText
) {
return node;
}
let child = node.child;
while (child !== null) {
if (child.tag !== HostPortal) {
const match = findCurrentHostFiberWithNoPortalsImpl(child);
if (match !== null) {
return match;
}
}
child = child.sibling;
}
return null;
}
export function isFiberSuspenseAndTimedOut(fiber: Fiber): boolean {
const memoizedState = fiber.memoizedState;
return (
fiber.tag === SuspenseComponent &&
memoizedState !== null &&
memoizedState.dehydrated === null
);
}
export function doesFiberContain(
parentFiber: Fiber,
childFiber: Fiber,
): boolean {
let node: null | Fiber = childFiber;
const parentFiberAlternate = parentFiber.alternate;
while (node !== null) {
if (node === parentFiber || node === parentFiberAlternate) {
return true;
}
node = node.return;
}
return false;
}
export function traverseFragmentInstance<I, A, B, C>(
fragmentFiber: Fiber,
fn: (I, A, B, C) => boolean,
a: A,
b: B,
c: C,
): void {
traverseFragmentInstanceChildren(fragmentFiber.child, fn, a, b, c);
}
function traverseFragmentInstanceChildren<I, A, B, C>(
child: Fiber | null,
fn: (I, A, B, C) => boolean,
a: A,
b: B,
c: C,
): void {
while (child !== null) {
if (child.tag === HostComponent) {
if (fn(child.stateNode, a, b, c)) {
return;
}
} else if (
child.tag === OffscreenComponent &&
child.memoizedState !== null
) {
// Skip hidden subtrees
} else {
traverseFragmentInstanceChildren(child.child, fn, a, b, c);
}
child = child.sibling;
}
}
export function getFragmentParentHostInstance(fiber: Fiber): null | Instance {
let parent = fiber.return;
while (parent !== null) {
if (parent.tag === HostRoot) {
return parent.stateNode.containerInfo;
}
if (parent.tag === HostComponent) {
return parent.stateNode;
}
parent = parent.return;
}
return null;
}