Fragment Refs はこの問題を、<Fragment> に ref を渡すことで解決する機能です。この記事では useRef / useEffect の基本的な使い方は知っている前提で、Fragment Refs 固有の部分に絞って整理します。
React 19.3 から <Fragment>(<>...</> の完全形)が ref を受け取れるようになりました。渡した ref の current には FragmentInstance というオブジェクトが入ります。
import { Fragment, useRef, useEffect } from "react";function Component() { const fragmentRef = useRef<React.FragmentInstance>(null); // [!code highlight] useEffect(() => { const fragmentInstance = fragmentRef.current; fragmentInstance?.focus(); }, []); return ( <Fragment ref={fragmentRef}> {/* [!code highlight] */} {posts.map((post) => ( <Heading key={post.id}>{post.title}</Heading> ))} </Fragment> );}FragmentInstance は実際の DOM ノードではなく、<Fragment> の直下にある子要素たちをひとまとまりのグループとして扱うためのハンドルです。DOM の構造自体は変更しません。
ref を公開していないコンポーネントに対しても、外側から <Fragment> で包むだけで振る舞いを追加できます。複数の Card をまとめて可視性監視する InView を例に、Fragment Refs が無かった場合の実装と比べてみました。
Fragment Refs が無い場合、複数の子要素をまとめて1つの IntersectionObserver で観測するには、ref を持たせるためのラッパー <div> が必要になります。
function InView({ onChange, children,}: { onChange: (visible: boolean) => void; children: React.ReactNode;}) { const wrapperRef = useRef<HTMLDivElement>(null); // [!code --] const fragmentRef = useRef<React.FragmentInstance>(null); // [!code ++] useEffect(() => { const visibleElements = new Set<Element>(); // [!code ++] const observer = new IntersectionObserver((entries) => { onChange(entries[0]?.isIntersecting ?? false); // [!code --] entries.forEach((e) => { // [!code ++] if (e.isIntersecting) { // [!code ++] visibleElements.add(e.target); // [!code ++] } else { // [!code ++] visibleElements.delete(e.target); // [!code ++] } // [!code ++] }); // [!code ++] onChange(visibleElements.size > 0); // [!code ++] }); const target = wrapperRef.current; // [!code --] if (!target) return; // [!code --] const fragmentInstance = fragmentRef.current; // [!code ++] if (!fragmentInstance) return; // [!code ++] observer.observe(target); // [!code --] fragmentInstance.observeUsing(observer); // [!code ++] return () => { observer.unobserve(target); // [!code --] fragmentInstance.unobserveUsing(observer); // [!code ++] }; }, [onChange]); return ( <div ref={wrapperRef}>{/* [!code --] */} <Fragment ref={fragmentRef}>{/* [!code ++] */} {children} </div>{/* [!code --] */} </Fragment>{/* [!code ++] */} );}<InView onChange={setIsVisible}> <Card title="First section" /> <Card title="Second section" /></InView>差分にすると、変わっている点が2つあることが分かります。
ref を持たせるためだけの <div> が不要になり、<Fragment> に置き換わっている(レイアウトへの影響が消える)<div>(=子要素をまとめた1つの矩形)が交差したか」を見るだけで済むが、After は observeUsing が子要素それぞれに IntersectionObserver を紐付けるため、entries を毎回 Set で集計して「1つでも交差している子要素があるか」を自分で組み立てる必要があるつまり Fragment Refs は「ラッパー要素を消せる」というメリットと引き換えに、「グループとしての判定は自分で書く」という一手間が増えています。単に見た目のDOM構造がスッキリするだけでなく、可視性の集計ロジックの持ち方まで変わる点は、実装してみるまで気づきにくいポイントでした。
FragmentInstance が持つメソッドは、次の4系統に分かれています。
| 系統 | メソッド | 用途 |
|---|---|---|
| イベント | addEventListener / removeEventListener / dispatchEvent | 直下の子要素に対するイベント管理 |
| フォーカス | focus / focusLast / blur | ネストした子要素をまたいだフォーカス移動(深さ優先) |
| Observer | observeUsing / unobserveUsing | IntersectionObserver / ResizeObserver の接続 |
| 計測・スクロール | getClientRects / getRootNode / compareDocumentPosition / scrollIntoView | 直下の子要素の測定・スクロール |
いずれも「グループとして」動作する点が共通していて、DOM ツリーの構造そのものを変えることはありません。
Fragment Refs を実際に手を動かして試してみて、「ラッパー要素を足さずに済む」「相手のコンポーネントを変更しなくて済む」という2点のありがたさが実感できました。特に外部ライブラリのコンポーネントに IntersectionObserver や focus 制御を後付けしたい場面では、これまでラッパー <div> を挟むしかなかった分、選択肢が増えた印象です。
グループ単位で動くぶん、可視性判定のように「複数の結果を自分で集計する」一手間が増える場面もありましたが、ラッパー要素を増やさずに済む恩恵の方が大きいと感じました。