---
title: "React Fragment Refs: FragmentInstance in 19.3 | Contrive"
description: "React 19.3 Fragment refs give you a FragmentInstance for events, focus, observers and scrolling across a Fragment's DOM children, with no wrapper div."
keywords:
 - react fragment refs
 - react 19.3 fragment ref
 - fragmentinstance
 - react fragment ref observeusing
 - react ref without wrapper div
 - react 19.3 fragment
 - react scrollintoview fragment
canonical_url: "https://contrivesol.com/react-19-3-fragment-refs/"
date_published: "2026-10-22"
date_modified: "2026-10-22"
type: post
author: Rohan Jalil
---

# React Fragment Refs in 19.3: How FragmentInstance Works


**In React 19.3 you can put a ref on a `<Fragment>`. The ref doesn’t hold a DOM node. It holds a `FragmentInstance`, an object that lets you add event listeners, move focus, attach observers, measure and scroll across the Fragment’s DOM children as a group, without adding a wrapper element to the page.**


Fragment refs became stable in [React 19.3](https://react.dev/blog/2026/09/09/react-19-3), released on September 9, 2026, alongside View Transitions. The same release added an API for components that should only render in the browser, which we covered in [our breakdown of how React 19.3’s browser() API handles browser-only rendering](/react-19-3-browser-api-ssr/). Here we stick to Fragment refs: what each method does, which children it reaches, and where it behaves differently from the DOM method it’s named after.


## The problem Fragment refs solve


Before 19.3, if you wanted to observe or listen to whatever a component rendered, you needed a DOM element to hold the ref. There were two ways to get one, and React’s release post names the downside of each.


Wrapping the children in an extra `<div>` is the quick fix, but that div can interfere with your styling or layout. A flex or grid parent suddenly has one child instead of five, or a `:first-child` selector starts matching the wrapper.


The other way is to make the child component expose a `ref` prop. That means editing the component, which you can’t do when it comes from a library you don’t control.


Fragment refs replace both with a limited set of commonly used DOM methods that work with any component, whatever it renders. Take “limited” literally. A `FragmentInstance` is not an element, so you can’t style it, give it a class or use it as a layout box. If you need a real box, you still need a real element.


## Attaching a ref to a Fragment


The `<>...</>` shorthand can’t take a ref, so import [`Fragment`](https://react.dev/reference/react/Fragment) from `react` and write it out in full:


```
import { Fragment, useRef } from 'react';

function ProductGrid({ products }) {
 const fragmentRef = useRef(null);

 return (
 <Fragment ref={fragmentRef}>
 {products.map((product) => (
 <ProductCard key={product.id} product={product} />
 ))}
 </Fragment>
 );
}
```


The `ref` prop takes either a `useRef` object or a ref callback, the same as on a regular element. What you get back is a `FragmentInstance`, not a DOM node.


Nothing is added to the DOM. React targets the Fragment’s first level of host (DOM) children and passes element-like calls through to them. Most methods act on each of those children. The main exceptions are the focus methods, which search deeper, and `dispatchEvent`, which fires on the Fragment itself (both covered below). React’s release post groups the methods like this:


- **Events:** `addEventListener`, `removeEventListener` and `dispatchEvent` manage events for first-level children.
- **Focus:** `focus`, `focusLast` and `blur` move focus across nested children, depth-first.
- **Observers:** `observeUsing` and `unobserveUsing` connect an `IntersectionObserver` or a `ResizeObserver`.
- **Measuring and scrolling:** `getClientRects`, `getRootNode`, `compareDocumentPosition` and `scrollIntoView` work on first-level children.


## The depth rule: first-level children vs depth-first search


Methods that target children, such as `addEventListener`, `observeUsing` and `getClientRects`, only reach the Fragment’s first-level host children. They don’t directly target elements nested inside another DOM element.


A first-level host child is the first real DOM element under the Fragment, even when a nested component renders it:


```
function Card({ title }) {
 return (
 <div className="card">
 <span>{title}</span>
 </div>
 );
}

<Fragment ref={ref}>
 <Card title="Starter" />
 <Card title="Team" />
</Fragment>
```


Here the two `div.card` elements are the first-level host children, and the `span` elements inside them are not targeted. `Card` isn’t a DOM node, so React looks straight through it to the div.


You still get events from the spans. A listener on each `div.card` receives events that bubble up from inside it, which is ordinary DOM behavior. What you can’t do through the Fragment is observe or measure a span on its own.


The focus methods work differently. `focus()` searches all nested children depth-first until it finds a focusable element, where `element.focus()` on a DOM node only tries that node. An input three levels down inside a child component is reachable.


## Event listeners on a group of children


`addEventListener` adds the listener to every first-level DOM child of the Fragment. When children are added or removed later, the `FragmentInstance` adds or removes the listener to match, so you don’t re-subscribe when a list grows.


One use is spotting unsaved edits across field components you didn’t write. The `input` event bubbles, so one listener on the Fragment catches typing in any field inside it:


```
import { Fragment, useEffect, useRef } from 'react';

function DirtyTracker({ onDirty, children }) {
 const fragmentRef = useRef(null);

 useEffect(() => {
 const fragment = fragmentRef.current;
 const controller = new AbortController();

 fragment.addEventListener('input', () => onDirty(true), {
 signal: controller.signal,
 });

 return () => controller.abort();
 }, );

 return <Fragment ref={fragmentRef}>{children}</Fragment>;
}
```


Listeners registered with `options.signal` are removed when the signal is aborted, so this cleanup works. Calling `removeEventListener` in the cleanup function works too.


React doesn’t apply these listeners to hidden `<Activity>` trees. It applies them automatically when the boundary switches to visible.


`dispatchEvent` dispatches an event on the Fragment itself: the listeners you added are called, and the event can bubble on to the Fragment’s DOM parent. It returns `false` if a listener called `preventDefault()` and `true` otherwise.


```
const allowed = fragmentRef.current.dispatchEvent(
 new CustomEvent('panel-close', { bubbles: true, cancelable: true })
);

if (allowed) {
 closePanel();
}
```


## Focus management without forwarded refs


`focus()` focuses the first focusable element, searching depth-first. `focusLast()` searches nested children depth-first, then iterates in reverse to find the last focusable node. `blur()` removes focus if the active element is inside the Fragment and does nothing if it isn’t. In a Fragment that contains only text, all three do nothing.


The usual case is focusing the first field of a form component that doesn’t forward a ref:


```
import { Fragment, useCallback } from 'react';
import { VendorAddressForm } from 'some-ui-library';

function CheckoutStep() {
 const focusFirstField = useCallback((fragment) => {
 if (fragment) {
 fragment.focus();
 }
 }, []);

 return (
 <Fragment ref={focusFirstField}>
 <VendorAddressForm />
 </Fragment>
 );
}
```


Keep the `useCallback`. Without it the ref callback is a new function on every render, and `focus()` runs again each time `CheckoutStep` re-renders. That pulls the cursor back to the first field while the user is typing somewhere else.


In a custom dialog, `focus()` and `focusLast()` give you both ends of a focus wrap, so you don’t have to query for focusable selectors yourself.


## IntersectionObserver and ResizeObserver with observeUsing


`observeUsing` starts observing all first-level DOM children of the Fragment with the observer you pass in. A visibility wrapper takes a few lines:


```
import { Fragment, useEffect, useRef } from 'react';

function InView({ onEnter, children }) {
 const fragmentRef = useRef(null);

 useEffect(() => {
 const fragment = fragmentRef.current;
 const observer = new IntersectionObserver((entries) => {
 if (entries.some((entry) => entry.isIntersecting)) {
 onEnter();
 }
 });

 fragment.observeUsing(observer);

 return () => {
 fragment.unobserveUsing(observer);
 observer.disconnect();
 };
 }, );

 return <Fragment ref={fragmentRef}>{children}</Fragment>;
}
```


`onEnter` runs on every delivery where at least one child is intersecting, so it fires again each time the group scrolls back into view. For a once-per-mount impression, set a flag in a ref the first time and check it before calling `onEnter`.


Each child component contributes its outer element, not the elements inside it. `observeUsing` doesn’t work on text nodes, and React logs a development warning if the Fragment contains only text.


Children that mount after you call `observeUsing` are added to the observers already attached. When a child is removed, React keeps it in an `IntersectionObserver` until the next delivery, so an observer like the one in `InView` still receives the exit entry.


If you track many groups, you don’t need one observer per Fragment. Each first-level DOM child of a Fragment with a ref has a `reactFragments` property, a `Set` of the `FragmentInstance` objects that contain it. One shared observer can route each entry back to its owner:


```
const handlers = new Map(); // FragmentInstance -> callback

const sharedObserver = new IntersectionObserver((entries) => {
 for (const entry of entries) {
 for (const fragment of entry.target.reactFragments ?? []) {
 handlers.get(fragment)?.(entry);
 }
 }
});

// In each component's Effect:
// handlers.set(fragment, onEntry);
// fragment.observeUsing(sharedObserver);
// cleanup: fragment.unobserveUsing(sharedObserver); handlers.delete(fragment);
```


You lose one thing with this pattern. When a child is removed, React defers only the `IntersectionObserver` unobserve. It takes the Fragment out of that child’s `reactFragments` set straight away, so the late exit entry arrives with nothing to route to and the loop above skips it. If you need to know when a removed child left, track that in the component’s own state, not through the shared observer.


## Measuring, ordering and scrolling


`getClientRects()` returns a flat array of `DOMRect` objects for the bounding rectangles of all first-level DOM children. Text children are measured with the Range API. For the box around a whole group, combine the rects:


```
useLayoutEffect(() => {
 const rects = fragmentRef.current.getClientRects();
 if (rects.length === 0) return; // empty Fragment: nothing to measure

 const top = Math.min(...rects.map((r) => r.top));
 const bottom = Math.max(...rects.map((r) => r.bottom));
 setGroupHeight(bottom - top);
}, []);
```


Don’t drop the early return. With no children, `Math.min()` and `Math.max()` called with no arguments return `Infinity` and `-Infinity`, and the height comes out as `-Infinity`.


The code uses [`useLayoutEffect`](https://react.dev/reference/react/useLayoutEffect) because what runs inside it, and any state updates it schedules, are processed before the browser repaints. It also holds up the repaint, so use it only when you need the numbers before the user sees the frame. Otherwise `useEffect` is enough.


`getRootNode()` returns a `Document`, a `ShadowRoot`, or the `FragmentInstance` itself when there is no parent DOM node. Reach for it when your component might render inside a shadow root.


`compareDocumentPosition(node)` returns the usual position bitmask. Empty Fragments, and Fragments whose children render through a portal, include `Node.DOCUMENT_POSITION_IMPLEMENTATION_SPECIFIC` in the result, so check that bit before you trust the ordering.


`scrollIntoView` is where the API departs most from the DOM version. It accepts only the boolean `alignToTop`. With `true` or no argument, it aligns the first child with the top of the scrollable ancestor. With `false`, it aligns the last child with the bottom. It doesn’t accept an options object, and passing one throws an error. If the Fragment has no children, it scrolls the nearest sibling or parent into view instead.


```
sectionRef.current.scrollIntoView(); // first child to the top
sectionRef.current.scrollIntoView(false); // last child to the bottom

// Throws: options objects are not supported
// sectionRef.current.scrollIntoView({ behavior: 'smooth' });
```


## Where to call FragmentInstance methods


Call them from Effects, ref callbacks or event handlers, which only run in the browser. Effects never run during server rendering. Don’t call them while rendering. A Fragment ref is like any other ref: you read it after React has updated the DOM, and your render output shouldn’t depend on it.


## Upgrading and TypeScript


You need `react` and `react-dom` 19.3 or later. As of October 22, 2026, the `latest` on npm is [19.3.1](https://github.com/facebook/react/releases/tag/v19.3.1), released October 14, 2026. Its only change is to the Server Components transport protocol, nothing related to Fragment refs.


For TypeScript, `@types/react` 19.3.0 types the Fragment `ref` prop as `Ref<FragmentInstance>`, and `@types/react-dom` 19.3.0 declares DOM methods on `FragmentInstance`, such as `scrollIntoView(alignToTop?: boolean)` and `observeUsing`. Upgrade both type packages, since the methods come from the react-dom one.


Not every documented method is declared yet. As of October 22, 2026, `compareDocumentPosition` is missing from that interface, so calling it in TypeScript needs a cast until the types catch up. Check the installed `index.d.ts` for the methods you call.


If you maintain a component library, a component that renders a Fragment can now pass a consumer’s ref through to it. The consumer gets a `FragmentInstance` and you don’t add a wrapper element.


Fragment refs are a small API, and most of the payoff is code you get to delete: wrapper divs that only existed to hold a ref, and components edited only to forward one. If you are planning a React upgrade or a new build on the full JavaScript stack, our [MERN development team](/web-development/mern/) can help you work out where these APIs fit.


## Frequently asked questions


### What is a Fragment ref in React?


It’s a ref passed to `<Fragment>`. Instead of a DOM node, it gives you a `FragmentInstance` that operates on the Fragment’s DOM children as a group without changing the DOM structure.


### Can I put a ref on the <>…</> shorthand?


No. To pass a ref you have to import `Fragment` from `react` and use `<Fragment ref={...}>`.


### Do Fragment ref event listeners reach nested elements?


They are attached only to first-level host children. Events that bubble up from deeper elements still reach those listeners, but you can’t observe or measure a nested element directly through the Fragment.


### Does FragmentInstance.scrollIntoView support smooth scrolling?


Not through the call. It takes only the `alignToTop` boolean, and passing an options object such as `{ behavior: 'smooth' }` throws an error.


### Which React version has stable Fragment refs?


React 19.3, released September 9, 2026. As of October 22, 2026, version 19.3.1 is the npm `latest`.


### How long have Fragment refs been in development?


The [first implementation PR](https://github.com/facebook/react/pull/32465) was merged into the React repository in March 2025. In April 2025 the React team said the API was [still being researched](https://react.dev/blog/2025/04/23/react-labs-view-transitions-activity-and-more), and it was presented as a Canary feature at [React Conf 2025](https://react.dev/blog/2025/10/16/react-conf-2025-recap) before becoming stable in 19.3.


### Is a FragmentInstance a DOM element?


No. It exposes a limited set of commonly used DOM methods. You can’t style it or use it as a layout box, so if you need one, render a real element.