Skip to content

Commit 2bb495c

Browse files
committed
Clean up and expand fragment ref docs
1 parent abe931a commit 2bb495c

1 file changed

Lines changed: 262 additions & 60 deletions

File tree

src/content/reference/react/Fragment.md

Lines changed: 262 additions & 60 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ title: <Fragment> (<>...</>)
66

77
`<Fragment>`, often used via `<>...</>` syntax, lets you group elements without a wrapper node.
88

9-
<Canary> Fragments can also accept refs, which enable interacting with underlying DOM nodes without adding wrapper elements. See reference and usage below.</Canary>
9+
<Canary>Fragments can also accept refs, which enable interacting with underlying DOM nodes without adding wrapper elements.</Canary>
1010

1111
```js
1212
<>
@@ -30,41 +30,224 @@ Wrap elements in `<Fragment>` to group them together in situations where you nee
3030
#### Props {/*props*/}
3131

3232
- **optional** `key`: Fragments declared with the explicit `<Fragment>` syntax may have [keys.](/learn/rendering-lists#keeping-list-items-in-order-with-key)
33-
- <CanaryBadge /> **optional** `ref`: A ref object (e.g. from [`useRef`](/reference/react/useRef)) or [callback function](/reference/react-dom/components/common#ref-callback). React provides a `FragmentInstance` as the ref value that implements methods for interacting with the DOM nodes wrapped by the Fragment.
33+
- <CanaryBadge /> **optional** `ref`: A ref object (e.g. from [`useRef`](/reference/react/useRef)) or [callback function](/reference/react-dom/components/common#ref-callback). React provides a `FragmentInstance` as the ref value that implements methods for interacting with the DOM nodes wrapped by the Fragment.
3434

35-
### <CanaryBadge /> FragmentInstance {/*fragmentinstance*/}
35+
#### Caveats {/*caveats*/}
3636

37-
When you pass a ref to a fragment, React provides a `FragmentInstance` object with methods for interacting with the DOM nodes wrapped by the fragment:
37+
* If you want to pass `key` to a Fragment, you can't use the `<>...</>` syntax. You have to explicitly import `Fragment` from `'react'` and render `<Fragment key={yourKey}>...</Fragment>`.
3838

39-
**Event handling methods:**
40-
- `addEventListener(type, listener, options?)`: Adds an event listener to all first-level DOM children of the Fragment.
41-
- `removeEventListener(type, listener, options?)`: Removes an event listener from all first-level DOM children of the Fragment.
42-
- `dispatchEvent(event)`: Dispatches an event to a virtual child of the Fragment to call any added listeners and can bubble to the DOM parent.
39+
* React does not [reset state](/learn/preserving-and-resetting-state) when you go from rendering `<><Child /></>` to `[<Child />]` or back, or when you go from rendering `<><Child /></>` to `<Child />` and back. This only works a single level deep: for example, going from `<><><Child /></></>` to `<Child />` resets the state. See the precise semantics [here.](https://gist.github.com/clemmy/b3ef00f9507909429d8aa0d3ee4f986b)
4340

44-
**Layout methods:**
45-
- `compareDocumentPosition(otherNode)`: Compares the document position of the Fragment with another node.
46-
- If the Fragment has children, the native `compareDocumentPosition` value is returned.
47-
- Empty Fragments will attempt to compare positioning within the React tree and include `Node.DOCUMENT_POSITION_IMPLEMENTATION_SPECIFIC`.
48-
- Elements that have a different relationship in the React tree and DOM tree due to portaling or other insertions are `Node.DOCUMENT_POSITION_IMPLEMENTATION_SPECIFIC`.
49-
- `getClientRects()`: Returns a flat array of `DOMRect` objects representing the bounding rectangles of all children.
50-
- `getRootNode()`: Returns the root node containing the Fragment's parent DOM node.
41+
* <CanaryBadge /> If you want to pass `ref` to a Fragment, you can't use the `<>...</>` syntax. You have to explicitly import `Fragment` from `'react'` and render `<Fragment ref={yourRef}>...</Fragment>`.
5142

52-
**Focus management methods:**
53-
- `focus(options?)`: Focuses the first focusable DOM node in the Fragment. Focus is attempted on nested children depth-first.
54-
- `focusLast(options?)`: Focuses the last focusable DOM node in the Fragment. Focus is attempted on nested children depth-first.
55-
- `blur()`: Removes focus if `document.activeElement` is within the Fragment.
43+
---
5644

57-
**Observer methods:**
58-
- `observeUsing(observer)`: Starts observing the Fragment's DOM children with an IntersectionObserver or ResizeObserver.
59-
- `unobserveUsing(observer)`: Stops observing the Fragment's DOM children with the specified observer.
45+
### <CanaryBadge /> `FragmentInstance` {/*fragmentinstance*/}
6046

61-
#### Caveats {/*caveats*/}
47+
When you pass a `ref` to a Fragment, React provides a `FragmentInstance` object. It implements methods for interacting with the first-level DOM children wrapped by the Fragment.
48+
49+
```js
50+
import { Fragment, useRef } from 'react';
51+
52+
function MyComponent() {
53+
const ref = useRef(null);
54+
return (
55+
<Fragment ref={ref}>
56+
<div id="A" />
57+
<Wrapper>
58+
<div id="B">
59+
<div id="C" />
60+
</div>
61+
</Wrapper>
62+
<div id="D" />
63+
</Fragment>
64+
);
65+
}
66+
```
67+
68+
In the example above, the `FragmentInstance` targets `A`, `B`, and `D`—the first-level host (DOM) children. `C` is not targeted because it is nested inside `B`. If a new host sibling is dynamically added alongside `A`, `B`, or `D`, the `FragmentInstance` automatically tracks it and applies existing event listeners and observers.
69+
70+
#### `addEventListener(type, listener, options?)` {/*addeventlistener*/}
71+
72+
Adds an event listener to all first-level DOM children of the Fragment.
73+
74+
```js
75+
fragmentRef.current.addEventListener('click', handleClick);
76+
```
77+
78+
##### Parameters {/*addeventlistener-parameters*/}
79+
80+
* `type`: A string representing the event type to listen for (e.g. `'click'`, `'focus'`).
81+
* `listener`: The event handler function.
82+
* **optional** `options`: An options object or boolean for capture, matching the [DOM `addEventListener` API.](https://developer.mozilla.org/en-US/docs/Web/API/EventTarget/addEventListener)
83+
84+
##### Returns {/*addeventlistener-returns*/}
85+
86+
`addEventListener` does not return anything (`undefined`).
87+
88+
#### `removeEventListener(type, listener, options?)` {/*removeeventlistener*/}
89+
90+
Removes an event listener from all first-level DOM children of the Fragment.
91+
92+
```js
93+
fragmentRef.current.removeEventListener('click', handleClick);
94+
```
95+
96+
##### Parameters {/*removeeventlistener-parameters*/}
97+
98+
* `type`: The event type string.
99+
* `listener`: The event handler function to remove.
100+
* **optional** `options`: An options object or boolean, matching the [DOM `removeEventListener` API.](https://developer.mozilla.org/en-US/docs/Web/API/EventTarget/removeEventListener)
101+
102+
##### Returns {/*removeeventlistener-returns*/}
103+
104+
`removeEventListener` does not return anything (`undefined`).
105+
106+
#### `dispatchEvent(event)` {/*dispatchevent*/}
107+
108+
Dispatches an event on the Fragment. Added event listeners are called, and the event can bubble to the Fragment's DOM parent.
109+
110+
```js
111+
fragmentRef.current.dispatchEvent(new Event('custom', { bubbles: true }));
112+
```
113+
114+
##### Parameters {/*dispatchevent-parameters*/}
115+
116+
* `event`: An [`Event`](https://developer.mozilla.org/en-US/docs/Web/API/Event) object to dispatch. If `bubbles` is `true`, the event bubbles to the Fragment's parent DOM node.
117+
118+
##### Returns {/*dispatchevent-returns*/}
119+
120+
`true` if the event was not cancelled, `false` if `preventDefault()` was called.
121+
122+
#### `focus(options?)` {/*focus*/}
123+
124+
Focuses the first focusable DOM node in the Fragment. Unlike calling `element.focus()` on a DOM element, this method searches *all* nested children depth-first until it finds a focusable element—not just the element itself or its direct children.
125+
126+
```js
127+
fragmentRef.current.focus();
128+
```
129+
130+
##### Parameters {/*focus-parameters*/}
131+
132+
* **optional** `options`: A [`FocusOptions`](https://developer.mozilla.org/en-US/docs/Web/API/HTMLElement/focus#options) object (e.g. `{ preventScroll: true }`).
133+
134+
##### Returns {/*focus-returns*/}
135+
136+
`focus` does not return anything (`undefined`).
137+
138+
#### `focusLast(options?)` {/*focuslast*/}
139+
140+
Focuses the last focusable DOM node in the Fragment. Searches nested children depth-first, then iterates in reverse.
141+
142+
```js
143+
fragmentRef.current.focusLast();
144+
```
145+
146+
##### Parameters {/*focuslast-parameters*/}
147+
148+
* **optional** `options`: A [`FocusOptions`](https://developer.mozilla.org/en-US/docs/Web/API/HTMLElement/focus#options) object.
149+
150+
##### Returns {/*focuslast-returns*/}
151+
152+
`focusLast` does not return anything (`undefined`).
153+
154+
#### `blur()` {/*blur*/}
155+
156+
Removes focus from the active element if it is within the Fragment. If `document.activeElement` is not within the Fragment, `blur` does nothing.
157+
158+
```js
159+
fragmentRef.current.blur();
160+
```
161+
162+
##### Returns {/*blur-returns*/}
163+
164+
`blur` does not return anything (`undefined`).
165+
166+
#### `observeUsing(observer)` {/*observeusing*/}
167+
168+
Starts observing all first-level DOM children of the Fragment with the provided observer.
62169

63-
- If you want to pass `key` to a Fragment, you can't use the `<>...</>` syntax. You have to explicitly import `Fragment` from `'react'` and render `<Fragment key={yourKey}>...</Fragment>`.
170+
```js
171+
const observer = new IntersectionObserver(callback, options);
172+
fragmentRef.current.observeUsing(observer);
173+
```
174+
175+
##### Parameters {/*observeusing-parameters*/}
176+
177+
* `observer`: An [`IntersectionObserver`](https://developer.mozilla.org/en-US/docs/Web/API/IntersectionObserver) or [`ResizeObserver`](https://developer.mozilla.org/en-US/docs/Web/API/ResizeObserver) instance.
178+
179+
##### Returns {/*observeusing-returns*/}
180+
181+
`observeUsing` does not return anything (`undefined`).
182+
183+
#### `unobserveUsing(observer)` {/*unobserveusing*/}
184+
185+
Stops observing the Fragment's DOM children with the specified observer.
186+
187+
```js
188+
fragmentRef.current.unobserveUsing(observer);
189+
```
190+
191+
##### Parameters {/*unobserveusing-parameters*/}
192+
193+
* `observer`: The same `IntersectionObserver` or `ResizeObserver` instance previously passed to [`observeUsing`](#observeusing).
194+
195+
##### Returns {/*unobserveusing-returns*/}
196+
197+
`unobserveUsing` does not return anything (`undefined`).
198+
199+
#### `getClientRects()` {/*getclientrects*/}
200+
201+
Returns a flat array of [`DOMRect`](https://developer.mozilla.org/en-US/docs/Web/API/DOMRect) objects representing the bounding rectangles of all first-level DOM children.
202+
203+
```js
204+
const rects = fragmentRef.current.getClientRects();
205+
```
206+
207+
##### Returns {/*getclientrects-returns*/}
208+
209+
An `Array<DOMRect>` containing the bounding rectangles of all children.
210+
211+
#### `getRootNode(options?)` {/*getrootnode*/}
212+
213+
Returns the root node containing the Fragment's parent DOM node, matching the behavior of [`Node.getRootNode()`](https://developer.mozilla.org/en-US/docs/Web/API/Node/getRootNode).
214+
215+
```js
216+
const root = fragmentRef.current.getRootNode();
217+
```
218+
219+
##### Parameters {/*getrootnode-parameters*/}
220+
221+
* **optional** `options`: An object with a `composed` boolean property, matching the [DOM `getRootNode` API.](https://developer.mozilla.org/en-US/docs/Web/API/Node/getRootNode#options)
222+
223+
##### Returns {/*getrootnode-returns*/}
224+
225+
A `Document`, `ShadowRoot`, or the `FragmentInstance` itself if there is no parent DOM node.
226+
227+
#### `compareDocumentPosition(otherNode)` {/*comparedocumentposition*/}
228+
229+
Compares the document position of the Fragment with another node, returning a bitmask matching the behavior of [`Node.compareDocumentPosition()`](https://developer.mozilla.org/en-US/docs/Web/API/Node/compareDocumentPosition).
230+
231+
```js
232+
const position = fragmentRef.current.compareDocumentPosition(otherElement);
233+
```
234+
235+
##### Parameters {/*comparedocumentposition-parameters*/}
236+
237+
* `otherNode`: The DOM node to compare against.
238+
239+
##### Returns {/*comparedocumentposition-returns*/}
64240

65-
- React does not [reset state](/learn/preserving-and-resetting-state) when you go from rendering `<><Child /></>` to `[<Child />]` or back, or when you go from rendering `<><Child /></>` to `<Child />` and back. This only works a single level deep: for example, going from `<><><Child /></></>` to `<Child />` resets the state. See the precise semantics [here.](https://gist.github.com/clemmy/b3ef00f9507909429d8aa0d3ee4f986b)
241+
A bitmask of [position flags](https://developer.mozilla.org/en-US/docs/Web/API/Node/compareDocumentPosition#return_value). Empty Fragments and Fragments with children rendered through a [portal](/reference/react-dom/createPortal) include `Node.DOCUMENT_POSITION_IMPLEMENTATION_SPECIFIC` in the result.
66242

67-
- <CanaryBadge /> If you want to pass `ref` to a Fragment, you can't use the `<>...</>` syntax. You have to explicitly import `Fragment` from `'react'` and render `<Fragment ref={yourRef}>...</Fragment>`.
243+
#### `FragmentInstance` Caveats {/*fragmentinstance-caveats*/}
244+
245+
* Methods that target children (such as `addEventListener`, `observeUsing`, and `getClientRects`) operate on *first-level host (DOM) children* of the Fragment. They do not directly target children nested inside another DOM element.
246+
* When host children are dynamically added or removed, the `FragmentInstance` automatically updates. New children receive any previously added event listeners and active observers.
247+
* `focus` and `focusLast` search nested children depth-first for focusable elements, unlike event and observer methods which only target first-level host children.
248+
* `observeUsing` does not work on text nodes. React logs a warning in development if the Fragment contains only text children.
249+
* `focus`, `focusLast`, and `blur` have no effect when the Fragment contains only text children.
250+
* React does not apply event listeners added via `addEventListener` to hidden [`<Activity>`](/reference/react/Activity) trees. When an `Activity` boundary switches from hidden to visible, listeners are applied automatically.
68251

69252
---
70253

@@ -242,34 +425,61 @@ function PostBody({ body }) {
242425
243426
---
244427
245-
### <CanaryBadge /> Using Fragment refs for DOM interaction {/*using-fragment-refs-for-dom-interaction*/}
428+
### <CanaryBadge /> Adding event listeners without a wrapper element {/*adding-event-listeners-without-wrapper*/}
246429
247-
Fragment refs allow you to interact with the DOM nodes wrapped by a Fragment without adding extra wrapper elements. This is useful for event handling, visibility tracking, focus management, and replacing deprecated patterns like `ReactDOM.findDOMNode()`.
430+
Fragment `ref`s let you add event listeners to a group of elements without adding a wrapper DOM node. Use a [ref callback](/reference/react-dom/components/common#ref-callback) to attach and clean up listeners:
248431
249-
```js
432+
```js {4-7}
250433
import { Fragment } from 'react';
251434

252-
function ClickableFragment({ children, onClick }) {
435+
function ClickableGroup({ children, onClick }) {
253436
return (
254437
<Fragment ref={fragmentInstance => {
255-
fragmentInstance.addEventListener('click', handleClick);
256-
return () => fragmentInstance.removeEventListener('click', handleClick);
438+
fragmentInstance.addEventListener('click', onClick);
439+
return () => fragmentInstance.removeEventListener('click', onClick);
257440
}}>
258441
{children}
259442
</Fragment>
260443
);
261444
}
262445
```
446+
447+
The `addEventListener` call applies the listener to every first-level DOM child of the Fragment. When children are dynamically added or removed, the `FragmentInstance` automatically adds or removes the listener.
448+
449+
<DeepDive>
450+
451+
#### Which children does a Fragment ref target? {/*which-children-does-a-fragment-ref-target*/}
452+
453+
A `FragmentInstance` targets the **first-level host (DOM) children** of the Fragment. Consider this tree:
454+
455+
```js
456+
<Fragment ref={ref}>
457+
<div id="A" />
458+
<Wrapper>
459+
<div id="B">
460+
<div id="C" />
461+
</div>
462+
</Wrapper>
463+
<div id="D" />
464+
</Fragment>
465+
```
466+
467+
`Wrapper` is a React component, so the `FragmentInstance` looks through it to find DOM nodes. The targeted children are `A`, `B`, and `D`. `C` is not targeted because it is nested inside the DOM element `B`.
468+
469+
Methods like `addEventListener`, `observeUsing`, and `getClientRects` operate on these first-level DOM children. `focus` and `focusLast` are different—they search *all* nested children depth-first to find focusable elements.
470+
471+
</DeepDive>
472+
263473
---
264474
265-
### <CanaryBadge /> Tracking visibility with Fragment refs {/*tracking-visibility-with-fragment-refs*/}
475+
### <CanaryBadge /> Observing visibility without a wrapper element {/*observing-visibility-without-wrapper*/}
266476
267-
Fragment refs are useful for visibility tracking and intersection observation. This enables you to monitor when content becomes visible without requiring the child Components to expose refs:
477+
Use `observeUsing` to attach an `IntersectionObserver` to all first-level DOM children of a Fragment. This lets you track visibility without requiring child components to expose `ref`s or adding a wrapper element:
268478
269-
```js {19,21,31-34}
479+
```js {6-14,17}
270480
import { Fragment, useRef, useLayoutEffect } from 'react';
271481

272-
function VisibilityObserverFragment({ threshold = 0.5, onVisibilityChange, children }) {
482+
function VisibleGroup({ onVisibilityChange, children }) {
273483
const fragmentRef = useRef(null);
274484

275485
useLayoutEffect(() => {
@@ -282,47 +492,39 @@ function VisibilityObserverFragment({ threshold = 0.5, onVisibilityChange, child
282492

283493
fragmentRef.current.observeUsing(observer);
284494
return () => fragmentRef.current.unobserveUsing(observer);
285-
}, [threshold, onVisibilityChange]);
495+
}, [onVisibilityChange]);
286496

287497
return (
288498
<Fragment ref={fragmentRef}>
289499
{children}
290500
</Fragment>
291501
);
292502
}
293-
294-
function MyComponent() {
295-
const handleVisibilityChange = (isVisible) => {
296-
console.log('Component is', isVisible ? 'visible' : 'hidden');
297-
};
298-
299-
return (
300-
<VisibilityObserverFragment onVisibilityChange={handleVisibilityChange}>
301-
<SomeThirdPartyComponent />
302-
<AnotherComponent />
303-
</VisibilityObserverFragment>
304-
);
305-
}
306503
```
307504
308-
This pattern is an alternative to Effect-based visibility logging, which is an anti-pattern in most cases. Relying on Effects alone does not guarantee that the rendered Component is observable by the user.
309-
310505
---
311506
312-
### <CanaryBadge /> Focus management with Fragment refs {/*focus-management-with-fragment-refs*/}
507+
### <CanaryBadge /> Managing focus across a group of elements {/*managing-focus-across-elements*/}
313508
314-
Fragment refs provide focus management methods that work across all DOM nodes within the Fragment:
509+
Fragment `ref`s provide `focus`, `focusLast`, and `blur` methods that operate across all DOM nodes within the Fragment:
315510
316-
```js
511+
```js {5-6}
317512
import { Fragment, useRef } from 'react';
318513

319-
function FocusFragment({ children }) {
514+
function FormFields({ children }) {
515+
const fragmentRef = useRef(null);
516+
// Focus the first focusable input in the group:
517+
const focusFirst = () => fragmentRef.current.focus();
518+
320519
return (
321-
<Fragment ref={(fragmentInstance) => fragmentInstance?.focus()}>
322-
{children}
323-
</Fragment>
520+
<>
521+
<button onClick={focusFirst}>Focus first field</button>
522+
<Fragment ref={fragmentRef}>
523+
{children}
524+
</Fragment>
525+
</>
324526
);
325527
}
326528
```
327529
328-
The `focus()` method focuses the first focusable element within the Fragment, while `focusLast()` focuses the last focusable element.
530+
`focus()` finds the first focusable element by searching depth-first through all nested children. `focusLast()` does the same in reverse. `blur()` removes focus if the currently focused element is within the Fragment.

0 commit comments

Comments
 (0)