@samitouri / QOS-React / commits / 1835b3f7d9

New children notify fragment instances in Fabric (#33093)

When a new child of a fragment instance is inserted, we need to notify the instance to keep any relevant tracking up to date. For example, we automatically observe the new child with any active IntersectionObserver. For mutable renderers (DOM), we reuse the existing traversal in `commitPlacement` that does the insertions for HostComponents. Immutable renderers (Fabric) exit this path before the traversal though, so currently we can't notify the fragment instances. Here I've created a separate traversal in `commitPlacement`, specifically for immutable renders when `enableFragmentRefs` is on.

Jack Pope committed May 21, 2025 at 15:47 UTC 1835b3f7d9c0541259a8812c5dfaf3d77f0721eb
4 files changed +109 -26
packages/react-dom-bindings/src/client/ReactFiberConfigDOM.js
+3 -3
@@ -3073,19 +3073,19 @@ export function updateFragmentInstanceFiber(
3073 }
3074
3075 export function commitNewChildToFragmentInstance(
3076 - childElement: Instance,
3076 + childInstance: Instance,
3077 fragmentInstance: FragmentInstanceType,
3078 ): void {
3079 const eventListeners = fragmentInstance._eventListeners;
3080 if (eventListeners !== null) {
3081 for (let i = 0; i < eventListeners.length; i++) {
3082 const {type, listener, optionsOrUseCapture} = eventListeners[i];
3083 - childElement.addEventListener(type, listener, optionsOrUseCapture);
3083 + childInstance.addEventListener(type, listener, optionsOrUseCapture);
3084 }
3085 }
3086 if (fragmentInstance._observers !== null) {
3087 fragmentInstance._observers.forEach(observer => {
3088 - observer.observe(childElement);
3088 + observer.observe(childInstance);
3089 });
3090 }
3091 }
packages/react-native-renderer/src/ReactFiberConfigFabric.js
+7 -2
@@ -695,12 +695,17 @@ export function updateFragmentInstanceFiber(
695 }
696
697 export function commitNewChildToFragmentInstance(
698 - child: Fiber,
698 + childInstance: Instance,
699 fragmentInstance: FragmentInstanceType,
700 ): void {
701 + const publicInstance = getPublicInstance(childInstance);
702 if (fragmentInstance._observers !== null) {
703 + if (publicInstance == null) {
704 + throw new Error('Expected to find a host node. This is a bug in React.');
705 + }
706 fragmentInstance._observers.forEach(observer => {
703 - observeChild(child, observer);
707 + // $FlowFixMe[incompatible-call] Element types are behind a flag in RN
708 + observer.observe(publicInstance);
709 });
710 }
711 }
packages/react-native-renderer/src/__tests__/ReactFabricFragmentRefs-test.internal.js
+42
@@ -80,4 +80,46 @@ describe('Fabric FragmentRefs', () => {
80
81 expect(fragmentRef && fragmentRef._fragmentFiber).toBeTruthy();
82 });
83 +
84 + describe('observers', () => {
85 + // @gate enableFragmentRefs
86 + it('observes children, newly added children', async () => {
87 + let logs = [];
88 + const observer = {
89 + observe: entry => {
90 + // Here we reference internals because we don't need to mock the native observer
91 + // We only need to test that each child node is observed on insertion
92 + logs.push(entry.__internalInstanceHandle.pendingProps.nativeID);
93 + },
94 + };
95 + function Test({showB}) {
96 + const fragmentRef = React.useRef(null);
97 + React.useEffect(() => {
98 + fragmentRef.current.observeUsing(observer);
99 + const lastRefValue = fragmentRef.current;
100 + return () => {
101 + lastRefValue.unobserveUsing(observer);
102 + };
103 + }, []);
104 + return (
105 + <View nativeID="parent">
106 + <React.Fragment ref={fragmentRef}>
107 + <View nativeID="A" />
108 + {showB && <View nativeID="B" />}
109 + </React.Fragment>
110 + </View>
111 + );
112 + }
113 +
114 + await act(() => {
115 + ReactFabric.render(<Test showB={false} />, 11, null, true);
116 + });
117 + expect(logs).toEqual(['A']);
118 + logs = [];
119 + await act(() => {
120 + ReactFabric.render(<Test showB={true} />, 11, null, true);
121 + });
122 + expect(logs).toEqual(['B']);
123 + });
124 + });
125 });
packages/react-reconciler/src/ReactFiberCommitHostEffects.js
+57 -21
@@ -255,8 +255,16 @@ export function commitShowHideHostTextInstance(node: Fiber, isHidden: boolean) {
255
256 export function commitNewChildToFragmentInstances(
257 fiber: Fiber,
258 - parentFragmentInstances: Array<FragmentInstanceType>,
258 + parentFragmentInstances: null | Array<FragmentInstanceType>,
259 ): void {
260 + if (
261 + fiber.tag !== HostComponent ||
262 + // Only run fragment insertion effects for initial insertions
263 + fiber.alternate !== null ||
264 + parentFragmentInstances === null
265 + ) {
266 + return;
267 + }
268 for (let i = 0; i < parentFragmentInstances.length; i++) {
269 const fragmentInstance = parentFragmentInstances[i];
270 commitNewChildToFragmentInstance(fiber.stateNode, fragmentInstance);
@@ -384,14 +392,7 @@ function insertOrAppendPlacementNodeIntoContainer(
392 } else {
393 appendChildToContainer(parent, stateNode);
394 }
387 - // TODO: Enable HostText for RN
388 - if (
389 - enableFragmentRefs &&
390 - tag === HostComponent &&
391 - // Only run fragment insertion effects for initial insertions
392 - node.alternate === null &&
393 - parentFragmentInstances !== null
394 - ) {
395 + if (enableFragmentRefs) {
396 commitNewChildToFragmentInstances(node, parentFragmentInstances);
397 }
398 trackHostMutation();
@@ -449,14 +450,7 @@ function insertOrAppendPlacementNode(
450 } else {
451 appendChild(parent, stateNode);
452 }
452 - // TODO: Enable HostText for RN
453 - if (
454 - enableFragmentRefs &&
455 - tag === HostComponent &&
456 - // Only run fragment insertion effects for initial insertions
457 - node.alternate === null &&
458 - parentFragmentInstances !== null
459 - ) {
453 + if (enableFragmentRefs) {
454 commitNewChildToFragmentInstances(node, parentFragmentInstances);
455 }
456 trackHostMutation();
@@ -494,10 +488,6 @@ function insertOrAppendPlacementNode(
488 }
489
490 function commitPlacement(finishedWork: Fiber): void {
497 - if (!supportsMutation) {
498 - return;
499 - }
500 -
491 // Recursively insert all host nodes into the parent.
492 let hostParentFiber;
493 let parentFragmentInstances = null;
@@ -517,6 +507,17 @@ function commitPlacement(finishedWork: Fiber): void {
507 }
508 parentFiber = parentFiber.return;
509 }
510 +
511 + if (!supportsMutation) {
512 + if (enableFragmentRefs) {
513 + commitImmutablePlacementNodeToFragmentInstances(
514 + finishedWork,
515 + parentFragmentInstances,
516 + );
517 + }
518 + return;
519 + }
520 +
521 if (hostParentFiber == null) {
522 throw new Error(
523 'Expected to find a host parent. This error is likely caused by a bug ' +
@@ -581,6 +582,41 @@ function commitPlacement(finishedWork: Fiber): void {
582 }
583 }
584
585 +function commitImmutablePlacementNodeToFragmentInstances(
586 + finishedWork: Fiber,
587 + parentFragmentInstances: null | Array<FragmentInstanceType>,
588 +): void {
589 + if (!enableFragmentRefs) {
590 + return;
591 + }
592 + const isHost = finishedWork.tag === HostComponent;
593 + if (isHost) {
594 + commitNewChildToFragmentInstances(finishedWork, parentFragmentInstances);
595 + return;
596 + } else if (finishedWork.tag === HostPortal) {
597 + // If the insertion itself is a portal, then we don't want to traverse
598 + // down its children. Instead, we'll get insertions from each child in
599 + // the portal directly.
600 + return;
601 + }
602 +
603 + const child = finishedWork.child;
604 + if (child !== null) {
605 + commitImmutablePlacementNodeToFragmentInstances(
606 + child,
607 + parentFragmentInstances,
608 + );
609 + let sibling = child.sibling;
610 + while (sibling !== null) {
611 + commitImmutablePlacementNodeToFragmentInstances(
612 + sibling,
613 + parentFragmentInstances,
614 + );
615 + sibling = sibling.sibling;
616 + }
617 + }
618 +}
619 +
620 export function commitHostPlacement(finishedWork: Fiber) {
621 try {
622 if (__DEV__) {