Notify FragmentInstance of added/removed text (#35637)

Follow up to https://github.com/facebook/react/pull/35630

We don't currently have any operations that depend on the updating of
text nodes added or removed after Fragment mount. But for the sake of
completeness and extending the ability to any other host configs, this
change calls `commitNewChildToFragmentInstance` and
`deleteChildFromFragmentInstance` on HostText fibers.

Both DOM and Fabric configs early return because we cannot attach event
listeners or observers to text. In the future, there could be some
stateful Fragment feature that uses text that could extend this.
This commit is contained in:
Jack Pope
2026-02-11 09:26:22 -05:00
committed by GitHub
parent e49335e961
commit 78f5c504b7
4 changed files with 51 additions and 17 deletions

View File

@@ -3544,40 +3544,48 @@ export function updateFragmentInstanceFiber(
}
export function commitNewChildToFragmentInstance(
childInstance: InstanceWithFragmentHandles,
childInstance: InstanceWithFragmentHandles | Text,
fragmentInstance: FragmentInstanceType,
): void {
if (childInstance.nodeType === TEXT_NODE) {
return;
}
const instance: InstanceWithFragmentHandles = (childInstance: any);
const eventListeners = fragmentInstance._eventListeners;
if (eventListeners !== null) {
for (let i = 0; i < eventListeners.length; i++) {
const {type, listener, optionsOrUseCapture} = eventListeners[i];
childInstance.addEventListener(type, listener, optionsOrUseCapture);
instance.addEventListener(type, listener, optionsOrUseCapture);
}
}
if (fragmentInstance._observers !== null) {
fragmentInstance._observers.forEach(observer => {
observer.observe(childInstance);
observer.observe(instance);
});
}
if (enableFragmentRefsInstanceHandles) {
addFragmentHandleToInstance(childInstance, fragmentInstance);
addFragmentHandleToInstance(instance, fragmentInstance);
}
}
export function deleteChildFromFragmentInstance(
childInstance: InstanceWithFragmentHandles,
childInstance: InstanceWithFragmentHandles | Text,
fragmentInstance: FragmentInstanceType,
): void {
if (childInstance.nodeType === TEXT_NODE) {
return;
}
const instance: InstanceWithFragmentHandles = (childInstance: any);
const eventListeners = fragmentInstance._eventListeners;
if (eventListeners !== null) {
for (let i = 0; i < eventListeners.length; i++) {
const {type, listener, optionsOrUseCapture} = eventListeners[i];
childInstance.removeEventListener(type, listener, optionsOrUseCapture);
instance.removeEventListener(type, listener, optionsOrUseCapture);
}
}
if (enableFragmentRefsInstanceHandles) {
if (childInstance.unstable_reactFragments != null) {
childInstance.unstable_reactFragments.delete(fragmentInstance);
if (instance.unstable_reactFragments != null) {
instance.unstable_reactFragments.delete(fragmentInstance);
}
}
}

View File

@@ -40,7 +40,10 @@ import {
type PublicTextInstance,
type PublicRootInstance,
} from 'react-native/Libraries/ReactPrivate/ReactNativePrivateInterface';
import {enableFragmentRefsInstanceHandles} from 'shared/ReactFeatureFlags';
import {
enableFragmentRefsInstanceHandles,
enableFragmentRefsTextNodes,
} from 'shared/ReactFeatureFlags';
const {
createNode,
@@ -847,10 +850,15 @@ export function updateFragmentInstanceFiber(
}
export function commitNewChildToFragmentInstance(
childInstance: Instance,
childInstance: Instance | TextInstance,
fragmentInstance: FragmentInstanceType,
): void {
const publicInstance = getPublicInstance(childInstance);
// Text nodes are not observable
if (enableFragmentRefsTextNodes && childInstance.canonical == null) {
return;
}
const instance: Instance = (childInstance: any);
const publicInstance = getPublicInstance(instance);
if (fragmentInstance._observers !== null) {
if (publicInstance == null) {
throw new Error('Expected to find a host node. This is a bug in React.');
@@ -869,11 +877,16 @@ export function commitNewChildToFragmentInstance(
}
export function deleteChildFromFragmentInstance(
childInstance: Instance,
childInstance: Instance | TextInstance,
fragmentInstance: FragmentInstanceType,
): void {
// Text nodes are not observable
if (enableFragmentRefsTextNodes && childInstance.canonical == null) {
return;
}
const instance: Instance = (childInstance: any);
const publicInstance = ((getPublicInstance(
childInstance,
instance,
): any): PublicInstanceWithFragmentHandles);
if (enableFragmentRefsInstanceHandles) {
if (publicInstance.unstable_reactFragments != null) {

View File

@@ -64,7 +64,10 @@ import {captureCommitPhaseError} from './ReactFiberWorkLoop';
import {trackHostMutation} from './ReactFiberMutationTracking';
import {runWithFiberInDEV} from './ReactCurrentFiber';
import {enableFragmentRefs} from 'shared/ReactFeatureFlags';
import {
enableFragmentRefs,
enableFragmentRefsTextNodes,
} from 'shared/ReactFeatureFlags';
export function commitHostMount(finishedWork: Fiber) {
const type = finishedWork.type;
@@ -258,7 +261,8 @@ export function commitNewChildToFragmentInstances(
parentFragmentInstances: null | Array<FragmentInstanceType>,
): void {
if (
fiber.tag !== HostComponent ||
(fiber.tag !== HostComponent &&
!(enableFragmentRefsTextNodes && fiber.tag === HostText)) ||
// Only run fragment insertion effects for initial insertions
fiber.alternate !== null ||
parentFragmentInstances === null

View File

@@ -62,6 +62,7 @@ import {
enableFragmentRefs,
enableEagerAlternateStateNodeCleanup,
enableDefaultTransitionIndicator,
enableFragmentRefsTextNodes,
} from 'shared/ReactFeatureFlags';
import {
FunctionComponent,
@@ -1533,7 +1534,11 @@ function commitDeletionEffectsOnFiber(
if (!offscreenSubtreeWasHidden) {
safelyDetachRef(deletedFiber, nearestMountedAncestor);
}
if (enableFragmentRefs && deletedFiber.tag === HostComponent) {
if (
enableFragmentRefs &&
(deletedFiber.tag === HostComponent ||
(enableFragmentRefsTextNodes && deletedFiber.tag === HostText))
) {
commitFragmentInstanceDeletionEffects(deletedFiber);
}
// Intentional fallthrough to next branch
@@ -3028,7 +3033,11 @@ export function disappearLayoutEffects(finishedWork: Fiber) {
// TODO (Offscreen) Check: flags & RefStatic
safelyDetachRef(finishedWork, finishedWork.return);
if (enableFragmentRefs && finishedWork.tag === HostComponent) {
if (
enableFragmentRefs &&
(finishedWork.tag === HostComponent ||
(enableFragmentRefsTextNodes && finishedWork.tag === HostText))
) {
commitFragmentInstanceDeletionEffects(finishedWork);
}