Handle fabric root level fragment with compareDocumentPosition (#34533)
The root instance doesn't have a canonical property so we were not returning a public instance that we can call compareDocumentPosition on when a Fragment had no other host parent in Fabric. In this case we need to get the ReactNativeElement from the ReactNativeDocument. I've also added test coverage for this case in DOM for consistency, though it was already working there because we use DOM elements as root. This same test will be copied to RN using Fantom.
Jack Pope committed
Sep 23, 2025 at 10:56 UTC
83c88ad470d680060f807ef81ed4c14b3b71fd3b
2 files changed
+121
-6
packages/react-dom/src/__tests__/ReactDOMFragmentRefs-test.js
+110
@@ -1613,6 +1613,116 @@ describe('FragmentRefs', () => {
1613
);
1614
});
1615
1616
+ // @gate enableFragmentRefs
1617
+ it('compares a root-level Fragment', async () => {
1618
+ const fragmentRef = React.createRef();
1619
+ const emptyFragmentRef = React.createRef();
1620
+ const childRef = React.createRef();
1621
+ const siblingPrecedingRef = React.createRef();
1622
+ const siblingFollowingRef = React.createRef();
1623
+ const root = ReactDOMClient.createRoot(container);
1624
+
1625
+ function Test() {
1626
+ return (
1627
+ <Fragment>
1628
+ <div ref={siblingPrecedingRef} />
1629
+ <Fragment ref={fragmentRef}>
1630
+ <div ref={childRef} />
1631
+ </Fragment>
1632
+ <Fragment ref={emptyFragmentRef} />
1633
+ <div ref={siblingFollowingRef} />
1634
+ </Fragment>
1635
+ );
1636
+ }
1637
+
1638
+ await act(() => root.render(<Test />));
1639
+
1640
+ const fragmentInstance = fragmentRef.current;
1641
+ if (fragmentInstance == null) {
1642
+ throw new Error('Expected fragment instance to be non-null');
1643
+ }
1644
+ const emptyFragmentInstance = emptyFragmentRef.current;
1645
+ if (emptyFragmentInstance == null) {
1646
+ throw new Error('Expected empty fragment instance to be non-null');
1647
+ }
1648
+
1649
+ expectPosition(
1650
+ fragmentInstance.compareDocumentPosition(childRef.current),
1651
+ {
1652
+ preceding: false,
1653
+ following: false,
1654
+ contains: false,
1655
+ containedBy: true,
1656
+ disconnected: false,
1657
+ implementationSpecific: false,
1658
+ },
1659
+ );
1660
+
1661
+ expectPosition(
1662
+ fragmentInstance.compareDocumentPosition(siblingPrecedingRef.current),
1663
+ {
1664
+ preceding: true,
1665
+ following: false,
1666
+ contains: false,
1667
+ containedBy: false,
1668
+ disconnected: false,
1669
+ implementationSpecific: false,
1670
+ },
1671
+ );
1672
+
1673
+ expectPosition(
1674
+ fragmentInstance.compareDocumentPosition(siblingFollowingRef.current),
1675
+ {
1676
+ preceding: false,
1677
+ following: true,
1678
+ contains: false,
1679
+ containedBy: false,
1680
+ disconnected: false,
1681
+ implementationSpecific: false,
1682
+ },
1683
+ );
1684
+
1685
+ expectPosition(
1686
+ emptyFragmentInstance.compareDocumentPosition(childRef.current),
1687
+ {
1688
+ preceding: true,
1689
+ following: false,
1690
+ contains: false,
1691
+ containedBy: false,
1692
+ disconnected: false,
1693
+ implementationSpecific: true,
1694
+ },
1695
+ );
1696
+
1697
+ expectPosition(
1698
+ emptyFragmentInstance.compareDocumentPosition(
1699
+ siblingPrecedingRef.current,
1700
+ ),
1701
+ {
1702
+ preceding: true,
1703
+ following: false,
1704
+ contains: false,
1705
+ containedBy: false,
1706
+ disconnected: false,
1707
+ implementationSpecific: true,
1708
+ },
1709
+ );
1710
+
1711
+ expectPosition(
1712
+ emptyFragmentInstance.compareDocumentPosition(
1713
+ siblingFollowingRef.current,
1714
+ ),
1715
+ {
1716
+ preceding: false,
1717
+ following: true,
1718
+ contains: false,
1719
+ containedBy: false,
1720
+ disconnected: false,
1721
+ implementationSpecific: true,
1722
+ },
1723
+ );
1724
+ });
1725
+
1726
describe('with portals', () => {
1727
// @gate enableFragmentRefs
1728
it('handles portaled elements', async () => {
packages/react-native-renderer/src/ReactFiberConfigFabric.js
+11
-6
@@ -25,7 +25,6 @@ import type {Fiber} from 'react-reconciler/src/ReactInternalTypes';
25
import {HostText} from 'react-reconciler/src/ReactWorkTags';
26
import {
27
getFragmentParentHostFiber,
28
- getInstanceFromHostFiber,
28
traverseFragmentInstance,
29
} from 'react-reconciler/src/ReactFiberTreeReflection';
30
@@ -303,6 +302,13 @@ export function getPublicInstance(instance: Instance): null | PublicInstance {
302
return instance.canonical.publicInstance;
303
}
304
305
+ // Handle root containers
306
+ if (instance.containerInfo != null) {
307
+ if (instance.containerInfo.publicInstance != null) {
308
+ return instance.containerInfo.publicInstance;
309
+ }
310
+ }
311
+
312
// For compatibility with the legacy renderer, in case it's used with Fabric
313
// in the same app.
314
// $FlowExpectedError[prop-missing]
@@ -347,9 +353,8 @@ export function getPublicInstanceFromInternalInstanceHandle(
353
}
354
355
function getPublicInstanceFromHostFiber(fiber: Fiber): PublicInstance {
350
- const instance = getInstanceFromHostFiber<Instance>(fiber);
351
- const publicInstance = getPublicInstance(instance);
352
- if (publicInstance == null) {
356
+ const publicInstance = getPublicInstance(fiber.stateNode);
357
+ if (publicInstance === null) {
358
throw new Error('Expected to find a host node. This is a bug in React.');
359
}
360
return publicInstance;
@@ -698,11 +703,11 @@ FragmentInstance.prototype.compareDocumentPosition = function (
703
if (parentHostFiber === null) {
704
return Node.DOCUMENT_POSITION_DISCONNECTED;
705
}
701
- const parentHostInstance = getPublicInstanceFromHostFiber(parentHostFiber);
706
const children: Array<Fiber> = [];
707
traverseFragmentInstance(this._fragmentFiber, collectChildren, children);
708
if (children.length === 0) {
705
- return compareDocumentPositionForEmptyFragment(
709
+ const parentHostInstance = getPublicInstanceFromHostFiber(parentHostFiber);
710
+ return compareDocumentPositionForEmptyFragment<PublicInstance>(
711
this._fragmentFiber,
712
parentHostInstance,
713
otherNode,