From 5aa6f21d212db4acc2eb413875c110327a344f4c Mon Sep 17 00:00:00 2001 From: Jack Pope Date: Mon, 26 Jan 2026 16:45:53 -0500 Subject: [PATCH] Notify fragment instance of added/removed text nodes --- .../src/client/ReactFiberConfigDOM.js | 24 ++++++++++++------- .../src/ReactFiberConfigFabric.js | 23 ++++++++++++++---- .../src/ReactFiberCommitHostEffects.js | 8 +++++-- .../src/ReactFiberCommitWork.js | 13 ++++++++-- 4 files changed, 51 insertions(+), 17 deletions(-) diff --git a/packages/react-dom-bindings/src/client/ReactFiberConfigDOM.js b/packages/react-dom-bindings/src/client/ReactFiberConfigDOM.js index 09653552aaf..a59cd6334bf 100644 --- a/packages/react-dom-bindings/src/client/ReactFiberConfigDOM.js +++ b/packages/react-dom-bindings/src/client/ReactFiberConfigDOM.js @@ -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); } } } diff --git a/packages/react-native-renderer/src/ReactFiberConfigFabric.js b/packages/react-native-renderer/src/ReactFiberConfigFabric.js index f5a4361fd41..cc7f0d0c36a 100644 --- a/packages/react-native-renderer/src/ReactFiberConfigFabric.js +++ b/packages/react-native-renderer/src/ReactFiberConfigFabric.js @@ -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) { diff --git a/packages/react-reconciler/src/ReactFiberCommitHostEffects.js b/packages/react-reconciler/src/ReactFiberCommitHostEffects.js index 5c7ccf39878..3626561e1b4 100644 --- a/packages/react-reconciler/src/ReactFiberCommitHostEffects.js +++ b/packages/react-reconciler/src/ReactFiberCommitHostEffects.js @@ -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, ): 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 diff --git a/packages/react-reconciler/src/ReactFiberCommitWork.js b/packages/react-reconciler/src/ReactFiberCommitWork.js index 43db744007d..f5e8c567490 100644 --- a/packages/react-reconciler/src/ReactFiberCommitWork.js +++ b/packages/react-reconciler/src/ReactFiberCommitWork.js @@ -61,6 +61,7 @@ import { enableFragmentRefs, enableEagerAlternateStateNodeCleanup, enableDefaultTransitionIndicator, + enableFragmentRefsTextNodes, } from 'shared/ReactFeatureFlags'; import { FunctionComponent, @@ -1532,7 +1533,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 @@ -3009,7 +3014,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); }