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.
Jack Pope committed
Feb 11, 2026 at 09:26 UTC
78f5c504b732aec0eb12514bc2cf3f27a8143dd2
4 files changed
+51
-17
packages/react-dom-bindings/src/client/ReactFiberConfigDOM.js
+16
-8
@@ -3544,40 +3544,48 @@ export function updateFragmentInstanceFiber(
3544
}
3545
3546
export function commitNewChildToFragmentInstance(
3547
- childInstance: InstanceWithFragmentHandles,
3547
+ childInstance: InstanceWithFragmentHandles | Text,
3548
fragmentInstance: FragmentInstanceType,
3549
): void {
3550
+ if (childInstance.nodeType === TEXT_NODE) {
3551
+ return;
3552
+ }
3553
+ const instance: InstanceWithFragmentHandles = (childInstance: any);
3554
const eventListeners = fragmentInstance._eventListeners;
3555
if (eventListeners !== null) {
3556
for (let i = 0; i < eventListeners.length; i++) {
3557
const {type, listener, optionsOrUseCapture} = eventListeners[i];
3554
- childInstance.addEventListener(type, listener, optionsOrUseCapture);
3558
+ instance.addEventListener(type, listener, optionsOrUseCapture);
3559
}
3560
}
3561
if (fragmentInstance._observers !== null) {
3562
fragmentInstance._observers.forEach(observer => {
3559
- observer.observe(childInstance);
3563
+ observer.observe(instance);
3564
});
3565
}
3566
if (enableFragmentRefsInstanceHandles) {
3563
- addFragmentHandleToInstance(childInstance, fragmentInstance);
3567
+ addFragmentHandleToInstance(instance, fragmentInstance);
3568
}
3569
}
3570
3571
export function deleteChildFromFragmentInstance(
3568
- childInstance: InstanceWithFragmentHandles,
3572
+ childInstance: InstanceWithFragmentHandles | Text,
3573
fragmentInstance: FragmentInstanceType,
3574
): void {
3575
+ if (childInstance.nodeType === TEXT_NODE) {
3576
+ return;
3577
+ }
3578
+ const instance: InstanceWithFragmentHandles = (childInstance: any);
3579
const eventListeners = fragmentInstance._eventListeners;
3580
if (eventListeners !== null) {
3581
for (let i = 0; i < eventListeners.length; i++) {
3582
const {type, listener, optionsOrUseCapture} = eventListeners[i];
3575
- childInstance.removeEventListener(type, listener, optionsOrUseCapture);
3583
+ instance.removeEventListener(type, listener, optionsOrUseCapture);
3584
}
3585
}
3586
if (enableFragmentRefsInstanceHandles) {
3579
- if (childInstance.unstable_reactFragments != null) {
3580
- childInstance.unstable_reactFragments.delete(fragmentInstance);
3587
+ if (instance.unstable_reactFragments != null) {
3588
+ instance.unstable_reactFragments.delete(fragmentInstance);
3589
}
3590
}
3591
}
packages/react-native-renderer/src/ReactFiberConfigFabric.js
+18
-5
@@ -40,7 +40,10 @@ import {
40
type PublicTextInstance,
41
type PublicRootInstance,
42
} from 'react-native/Libraries/ReactPrivate/ReactNativePrivateInterface';
43
-import {enableFragmentRefsInstanceHandles} from 'shared/ReactFeatureFlags';
43
+import {
44
+ enableFragmentRefsInstanceHandles,
45
+ enableFragmentRefsTextNodes,
46
+} from 'shared/ReactFeatureFlags';
47
48
const {
49
createNode,
@@ -847,10 +850,15 @@ export function updateFragmentInstanceFiber(
850
}
851
852
export function commitNewChildToFragmentInstance(
850
- childInstance: Instance,
853
+ childInstance: Instance | TextInstance,
854
fragmentInstance: FragmentInstanceType,
855
): void {
853
- const publicInstance = getPublicInstance(childInstance);
856
+ // Text nodes are not observable
857
+ if (enableFragmentRefsTextNodes && childInstance.canonical == null) {
858
+ return;
859
+ }
860
+ const instance: Instance = (childInstance: any);
861
+ const publicInstance = getPublicInstance(instance);
862
if (fragmentInstance._observers !== null) {
863
if (publicInstance == null) {
864
throw new Error('Expected to find a host node. This is a bug in React.');
@@ -869,11 +877,16 @@ export function commitNewChildToFragmentInstance(
877
}
878
879
export function deleteChildFromFragmentInstance(
872
- childInstance: Instance,
880
+ childInstance: Instance | TextInstance,
881
fragmentInstance: FragmentInstanceType,
882
): void {
883
+ // Text nodes are not observable
884
+ if (enableFragmentRefsTextNodes && childInstance.canonical == null) {
885
+ return;
886
+ }
887
+ const instance: Instance = (childInstance: any);
888
const publicInstance = ((getPublicInstance(
876
- childInstance,
889
+ instance,
890
): any): PublicInstanceWithFragmentHandles);
891
if (enableFragmentRefsInstanceHandles) {
892
if (publicInstance.unstable_reactFragments != null) {
packages/react-reconciler/src/ReactFiberCommitHostEffects.js
+6
-2
@@ -64,7 +64,10 @@ import {captureCommitPhaseError} from './ReactFiberWorkLoop';
64
import {trackHostMutation} from './ReactFiberMutationTracking';
65
66
import {runWithFiberInDEV} from './ReactCurrentFiber';
67
-import {enableFragmentRefs} from 'shared/ReactFeatureFlags';
67
+import {
68
+ enableFragmentRefs,
69
+ enableFragmentRefsTextNodes,
70
+} from 'shared/ReactFeatureFlags';
71
72
export function commitHostMount(finishedWork: Fiber) {
73
const type = finishedWork.type;
@@ -258,7 +261,8 @@ export function commitNewChildToFragmentInstances(
261
parentFragmentInstances: null | Array<FragmentInstanceType>,
262
): void {
263
if (
261
- fiber.tag !== HostComponent ||
264
+ (fiber.tag !== HostComponent &&
265
+ !(enableFragmentRefsTextNodes && fiber.tag === HostText)) ||
266
// Only run fragment insertion effects for initial insertions
267
fiber.alternate !== null ||
268
parentFragmentInstances === null
packages/react-reconciler/src/ReactFiberCommitWork.js
+11
-2
@@ -62,6 +62,7 @@ import {
62
enableFragmentRefs,
63
enableEagerAlternateStateNodeCleanup,
64
enableDefaultTransitionIndicator,
65
+ enableFragmentRefsTextNodes,
66
} from 'shared/ReactFeatureFlags';
67
import {
68
FunctionComponent,
@@ -1533,7 +1534,11 @@ function commitDeletionEffectsOnFiber(
1534
if (!offscreenSubtreeWasHidden) {
1535
safelyDetachRef(deletedFiber, nearestMountedAncestor);
1536
}
1536
- if (enableFragmentRefs && deletedFiber.tag === HostComponent) {
1537
+ if (
1538
+ enableFragmentRefs &&
1539
+ (deletedFiber.tag === HostComponent ||
1540
+ (enableFragmentRefsTextNodes && deletedFiber.tag === HostText))
1541
+ ) {
1542
commitFragmentInstanceDeletionEffects(deletedFiber);
1543
}
1544
// Intentional fallthrough to next branch
@@ -3028,7 +3033,11 @@ export function disappearLayoutEffects(finishedWork: Fiber) {
3033
// TODO (Offscreen) Check: flags & RefStatic
3034
safelyDetachRef(finishedWork, finishedWork.return);
3035
3031
- if (enableFragmentRefs && finishedWork.tag === HostComponent) {
3036
+ if (
3037
+ enableFragmentRefs &&
3038
+ (finishedWork.tag === HostComponent ||
3039
+ (enableFragmentRefsTextNodes && finishedWork.tag === HostText))
3040
+ ) {
3041
commitFragmentInstanceDeletionEffects(finishedWork);
3042
}
3043