@samitouri / QOS-React-1 / commits / 78f5c504b7

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