Repository: facebook/react
Pull request: Notify FragmentInstance of added/removed text

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.




Changed files:
filename: packages/react-dom-bindings/src/client/ReactFiberConfigDOM.js
patch: @@ -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);
     }
   }
 }

filename: packages/react-native-renderer/src/ReactFiberConfigFabric.js
patch: @@ -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) {

filename: packages/react-reconciler/src/ReactFiberCommitHostEffects.js
patch: @@ -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

filename: packages/react-reconciler/src/ReactFiberCommitWork.js
patch: @@ -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);
       }