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__) {