[{"data":1,"prerenderedAt":-1},["ShallowReactive",2],{"verticals":3,"quiz-react-weekly-fragment-refs":44,"search-suggestions":60,"quiz-article-react-weekly-fragment-refs":107},[4,20,32],{"id":5,"slug":6,"name":7,"tagline":8,"description":9,"accentFrom":10,"accentTo":11,"icon":12,"defaultLocale":13,"locales":14,"features":16,"position":19},"019fe637-3d33-714b-b57f-23e163ffca0c","dev","Web Development","Read it. Run it. Prove it.","A post a day on modern web development — most with an editable playground and a quiz that explains every answer. Free, no account needed.","violet-500","cyan-400","◇","en",[13,15],"fa",{"courses":17,"paths":17,"articles":17,"exams":18,"flashcards":18,"packages":17,"community":17,"certificates":17,"teams":17,"commerce":17},true,false,0,{"id":21,"slug":22,"name":23,"tagline":24,"description":25,"accentFrom":26,"accentTo":10,"icon":27,"defaultLocale":13,"locales":28,"features":30,"position":31},"019fe637-3dc2-754c-8657-0f175bfee7c6","lang","Languages","Learn a language the way you learn a codebase.","Grammar explained the way good documentation explains an API — one idea at a time, each with a quiz.","amber-400","⌘",[13,15,29],"es",{"courses":18,"paths":18,"articles":17,"exams":18,"flashcards":17,"packages":18,"community":17,"certificates":17,"teams":18,"commerce":18},2,{"id":33,"slug":34,"name":35,"tagline":36,"description":37,"accentFrom":38,"accentTo":39,"icon":40,"defaultLocale":13,"locales":41,"features":42,"position":43},"7b3c16f2-931d-410e-802e-e1fa4edab7de","soft","Soft Skills","The half of the job nobody wrote documentation for.","Weekly, on the parts of working life that decide more than your code does — first weeks, meetings, interviews, promotions, and the people around you. Written from what actually happens, and recorded as a podcast you can listen to on the walk.","emerald-400","teal-300","◉",[13],{"courses":18,"paths":18,"articles":17,"exams":18,"flashcards":18,"packages":18,"community":17,"certificates":18,"teams":18,"commerce":18},3,{"id":45,"slug":46,"kind":47,"title":48,"description":49,"config":50,"verticalId":5,"vertical":55,"course":52,"_count":56,"access":57,"attempts":59,"questionCount":51},"01a117e0-ad6d-734f-931d-06270f1e7f67","react-weekly-fragment-refs","PRACTICE_QUIZ","React Fragment Refs — test yourself","Eight questions on React 19.3's Fragment Refs: what they solve, the FragmentInstance API, and the restrictions most likely to surprise you.",{"questionCount":51,"timeLimitSec":52,"shuffleQuestions":18,"shuffleOptions":17,"negativeMarking":19,"passScorePct":53,"maxAttempts":52,"revealAnswers":54,"allowFlagging":18,"allowBacktracking":17},8,null,70,"AFTER_SUBMIT",{"slug":6,"name":7},{"questions":51},{"allowed":17,"reason":58},"FREE",[],[61,65,69,73,77,81,85,89,93,97,100,104],{"slug":62,"name":63,"articles":64},"webdev","Webdev",131,{"slug":66,"name":67,"articles":68},"javascript","Javascript",107,{"slug":70,"name":71,"articles":72},"frontend","Frontend",82,{"slug":74,"name":75,"articles":76},"tutorial","Tutorial",50,{"slug":78,"name":79,"articles":80},"css","Css",42,{"slug":82,"name":83,"articles":84},"typescript","Typescript",18,{"slug":86,"name":87,"articles":88},"performance","Performance",17,{"slug":90,"name":91,"articles":92},"react","React",16,{"slug":94,"name":95,"articles":96},"browser","Browser",12,{"slug":98,"name":99,"articles":96},"node","Node",{"slug":101,"name":102,"articles":103},"html","Html",9,{"slug":105,"name":106,"articles":103},"accessibility","Accessibility",{"id":108,"slug":46,"title":109,"subtitle":52,"excerpt":110,"coverUrl":111,"locale":13,"readingMinutes":112,"publishedAt":113,"viewCount":114,"likeCount":19,"commentCount":19,"author":115,"vertical":120,"topic":121,"tags":122,"_count":127,"playground":129,"body":131,"bodyMd":482,"seo":483,"translationGroupId":485,"series":486,"podcastUrl":52,"verticalId":5,"thread":513,"assessments":515,"translations":518,"quiz":520},"01a117e0-acb2-740f-9391-db919729cf21","React 19.3 Fragment Refs: Skip the Wrapper Div for a Ref","React 19.3 Fragment Refs let you ref a sibling group with no wrapper div. Learn the FragmentInstance API, its limits, and get a copy-paste cheat sheet.","\u002Fmedia\u002Fcovers\u002Freact-weekly-fragment-refs.png",13,"2026-10-10T12:12:38.989Z",46,{"id":116,"name":117,"username":118,"avatarUrl":52,"headline":119},"019fe637-3c25-7088-9034-39c9f15dc3c8","Parsa Jiravand","parsa","Frontend engineer · building bestpractic",{"slug":6,"name":7,"accentFrom":10,"accentTo":11},{"slug":90,"name":91},[123,124,125,126],{"slug":90,"name":91,"color":52},{"slug":66,"name":67,"color":52},{"slug":74,"name":75,"color":52},{"slug":62,"name":63,"color":52},{"assessments":128},1,{"slug":46,"title":130},"React Fragment Refs — interactive playground",{"blocks":132,"version":128},[133,137,140,143,146,151,154,163,166,169,172,187,190,193,199,202,206,209,212,215,218,221,224,227,231,234,237,240,244,247,250,253,257,260,263,266,269,273,276,279,282,285,289,292,296,299,308,311,314,317,320,323,327,330,333,336,340,343,346,349,352,355,358,362,396,402,405,413,416,419,422,425,428,431,434,437,440,443,446,449,452,455,458,464,467,470,473,476],{"id":134,"html":135,"type":136},"b1","\u003Cp>You&#39;re building a dashboard. Three stat cards need to render as siblings inside a CSS grid that expects each one to land in its own column — and you need a single ref on the group of three, so an \u003Ccode>IntersectionObserver\u003C\u002Fcode> can tell you when the cluster scrolls into view. There&#39;s no component boundary to hang that ref on, so you reach for the oldest trick in the book: wrap the three cards in a \u003Ccode>&lt;div&gt;\u003C\u002Fcode>.\u003C\u002Fp>","paragraph",{"id":138,"html":139,"type":136},"b2","\u003Cp>The grid breaks. Your three cards, each meant to be its own grid item, are now squashed inside the one cell the wrapper \u003Ccode>&lt;div&gt;\u003C\u002Fcode> occupies.\u003C\u002Fp>",{"id":141,"html":142,"type":136},"b3","\u003Cp>\u003Cstrong>React 19.3 (released September 9, 2026) ships a direct fix for this: a ref on a \u003Ccode>&lt;Fragment&gt;\u003C\u002Fcode>.\u003C\u002Fstrong> No wrapper, no extra DOM node, and a real object back — a \u003Ccode>FragmentInstance\u003C\u002Fcode> — that can do more than a typical element ref ever could. This article covers what Fragment Refs actually are, the exact API you get, and the restrictions that will bite you if you don&#39;t know about them up front.\u003C\u002Fp>",{"id":144,"html":145,"type":136},"b4","\u003Cp>This article is written against \u003Cstrong>React 19.3.0\u003C\u002Fstrong> (verified against the current npm \u003Ccode>latest\u003C\u002Fcode> dist-tag and the official React blog&#39;s 19.3 release post) and assumes React 19-era function components and hooks throughout. Fragment Refs are a DOM-rendering feature, so they need \u003Ccode>react-dom\u003C\u002Fcode> 19.3 or later alongside \u003Ccode>react\u003C\u002Fcode> 19.3 or later.\u003C\u002Fp>",{"id":147,"html":148,"text":149,"type":150,"level":31},"b5","What you&#39;ll learn","What you'll learn","heading",{"id":152,"html":153,"type":136},"b6","\u003Cp>By the end of this article you&#39;ll be able to:\u003C\u002Fp>",{"id":155,"type":156,"items":157,"ordered":18},"b7","list",[158,159,160,161,162],"Explain exactly why a wrapper \u003Ccode>&lt;div&gt;\u003C\u002Fcode> added only to hold a ref is a real structural change, not a free one","Attach a ref to a \u003Ccode>&lt;Fragment&gt;\u003C\u002Fcode> and read what comes back — a \u003Ccode>FragmentInstance\u003C\u002Fcode>, not a DOM node","Use the \u003Ccode>FragmentInstance\u003C\u002Fcode> methods that act on a sibling group as a unit: \u003Ccode>addEventListener\u003C\u002Fcode>, \u003Ccode>observeUsing\u003C\u002Fcode>, \u003Ccode>focus\u003C\u002Fcode>\u002F\u003Ccode>focusLast\u003C\u002Fcode>, \u003Ccode>getClientRects\u003C\u002Fcode>, \u003Ccode>scrollIntoView\u003C\u002Fcode>","Tell which methods only see \u003Cstrong>first-level DOM children\u003C\u002Fstrong> and which search every nested child, so you don&#39;t get a silent no-op","Decide when a Fragment Ref is the right tool and when a real wrapper element still is",{"id":164,"html":165,"text":165,"type":150,"level":31},"b8","Who this is for",{"id":167,"html":168,"type":136},"b9","\u003Cp>You&#39;ve called \u003Ccode>useRef\u003C\u002Fcode> to grab a DOM node, and you&#39;ve written a wrapper \u003Ccode>&lt;div&gt;\u003C\u002Fcode> at least once for no reason other than &quot;something needs to hold the ref.&quot; No class components or Server Components knowledge required.\u003C\u002Fp>",{"id":170,"html":171,"text":171,"type":150,"level":31},"b10","Table of contents",{"id":173,"type":156,"items":174,"ordered":18},"b11",[175,176,177,178,179,180,181,182,183,184,185,186],"\u003Ca href=\"#the-problem-a-ref-has-nowhere-to-live\">The problem: a ref has nowhere to live\u003C\u002Fa>","\u003Ca href=\"#the-mental-model\">The mental model\u003C\u002Fa>","\u003Ca href=\"#stage-1-a-ref-on-a-fragment\">Stage 1: a ref on a Fragment\u003C\u002Fa>","\u003Ca href=\"#stage-2-group-level-event-listening\">Stage 2: group-level event listening\u003C\u002Fa>","\u003Ca href=\"#stage-3-observing-the-group-with-observeusing\">Stage 3: observing the group with observeUsing\u003C\u002Fa>","\u003Ca href=\"#stage-4-focus-management-across-the-group\">Stage 4: focus management across the group\u003C\u002Fa>","\u003Ca href=\"#stage-5-measuring-and-scrolling-the-group\">Stage 5: measuring and scrolling the group\u003C\u002Fa>","\u003Ca href=\"#edge-cases-and-gotchas\">Edge cases and gotchas\u003C\u002Fa>","\u003Ca href=\"#best-practices-when-to-reach-for-this\">Best practices: when to reach for this\u003C\u002Fa>","\u003Ca href=\"#faq\">FAQ\u003C\u002Fa>","\u003Ca href=\"#cheat-sheet\">Cheat sheet\u003C\u002Fa>","\u003Ca href=\"#key-takeaways\">Key takeaways\u003C\u002Fa>",{"id":188,"html":189,"text":189,"type":150,"level":31},"b12","The problem: a ref has nowhere to live",{"id":191,"html":192,"type":136},"b13","\u003Cp>Here&#39;s the dashboard component, written the way most of us would write it before 19.3:\u003C\u002Fp>",{"id":194,"code":195,"type":196,"language":197,"highlight":198},"b14","\u002F\u002F The wrong way first: wrap the group just to hold a ref.\nfunction StatGroup({ stats }) {\n  const groupRef = useRef(null);\n\n  useEffect(() => {\n    const observer = new IntersectionObserver(([entry]) => {\n      console.log(\"group visible:\", entry.isIntersecting);\n    });\n    observer.observe(groupRef.current);\n    return () => observer.disconnect();\n  }, []);\n\n  return (\n    \u003Cdiv ref={groupRef} className=\"stat-group\">\n      {stats.map((s) => (\n        \u003CStatCard key={s.id} {...s} \u002F>\n      ))}\n    \u003C\u002Fdiv>\n  );\n}","code","jsx",[],{"id":200,"html":201,"type":136},"b15","\u003Cp>That \u003Ccode>&lt;div className=&quot;stat-group&quot;&gt;\u003C\u002Fcode> is the only reason this code compiles — \u003Ccode>useRef\u003C\u002Fcode> needs a real DOM node to attach to, and a plain \u003Ccode>&lt;&gt;...&lt;\u002F&gt;\u003C\u002Fcode> fragment can&#39;t take a ref. But the parent that renders \u003Ccode>&lt;StatGroup \u002F&gt;\u003C\u002Fcode> is a CSS grid:\u003C\u002Fp>",{"id":203,"code":204,"type":196,"language":78,"highlight":205},"b16",".dashboard {\n  display: grid;\n  grid-template-columns: repeat(6, 1fr);\n  gap: 12px;\n}",[],{"id":207,"html":208,"type":136},"b17","\u003Cp>It expects every stat card to be its own grid item, interleaved with other dashboard widgets. Instead, the wrapper \u003Ccode>&lt;div&gt;\u003C\u002Fcode> becomes \u003Cem>one\u003C\u002Fem> grid item — the three cards it contains collapse into that single cell instead of occupying three. You can reach for \u003Ccode>display: contents\u003C\u002Fcode> on the wrapper to make it disappear from layout, but that has its own cost: Safari has shipped multiple bugs around \u003Ccode>display: contents\u003C\u002Fcode> and focus\u002Faccessibility trees, and the node still shows up in \u003Ccode>:nth-child\u003C\u002Fcode> counts and \u003Ccode>querySelector\u003C\u002Fcode> results in the parent — it just doesn&#39;t participate visually. You&#39;ve traded a layout bug for an invisible structural one.\u003C\u002Fp>",{"id":210,"html":211,"type":136},"b18","\u003Cp>The real problem isn&#39;t the \u003Ccode>IntersectionObserver\u003C\u002Fcode> call. It&#39;s that \u003Cstrong>the only way to get a ref has always been to add a DOM node\u003C\u002Fstrong>, whether or not your layout wanted one.\u003C\u002Fp>",{"id":213,"html":214,"text":214,"type":150,"level":31},"b19","The mental model",{"id":216,"html":217,"type":136},"b20","\u003Cp>\u003Cstrong>The mental model:\u003C\u002Fstrong> a \u003Ccode>Fragment\u003C\u002Fcode> has never rendered a DOM node — that&#39;s its entire purpose, grouping children for React&#39;s own bookkeeping without touching the real DOM tree. A ref on a \u003Ccode>Fragment\u003C\u002Fcode> doesn&#39;t change that. What changes is that React now hands you a \u003Ccode>FragmentInstance\u003C\u002Fcode>: a stable object that knows which real DOM nodes belong to this group and can act on all of them as a unit.\u003C\u002Fp>",{"id":219,"html":220,"type":136},"b21","\u003Cp>Think of it less like a new element you could select with CSS, and more like \u003Cstrong>a remote control for &quot;all the first-level DOM children of this Fragment.&quot;\u003C\u002Fstrong> You press a button on the remote (\u003Ccode>addEventListener\u003C\u002Fcode>, \u003Ccode>observeUsing\u003C\u002Fcode>, \u003Ccode>focus\u003C\u002Fcode>) and it reaches every child in the group — but it never adds a node for the remote itself to live on.\u003C\u002Fp>",{"id":222,"html":223,"text":223,"type":150,"level":31},"b22","Stage 1: a ref on a Fragment",{"id":225,"html":226,"type":136},"b23","\u003Cp>The \u003Ccode>&lt;&gt;...&lt;\u002F&gt;\u003C\u002Fcode> shorthand can&#39;t take a ref — you have to import \u003Ccode>Fragment\u003C\u002Fcode> explicitly:\u003C\u002Fp>",{"id":228,"code":229,"type":196,"language":197,"highlight":230},"b24","import { Fragment, useRef, useEffect } from \"react\";\n\nfunction StatGroup({ stats }) {\n  const groupRef = useRef(null);\n\n  useEffect(() => {\n    console.log(groupRef.current); \u002F\u002F a FragmentInstance, not a DOM node\n  }, []);\n\n  return (\n    \u003CFragment ref={groupRef}>\n      {stats.map((s) => (\n        \u003CStatCard key={s.id} {...s} \u002F>\n      ))}\n    \u003C\u002FFragment>\n  );\n}",[],{"id":232,"html":233,"type":136},"b25","\u003Cp>\u003Cstrong>Key concept:\u003C\u002Fstrong> \u003Ccode>groupRef.current\u003C\u002Fcode> is now a \u003Ccode>FragmentInstance\u003C\u002Fcode> object. There is still zero extra DOM in the tree — the three \u003Ccode>StatCard\u003C\u002Fcode> elements render as direct grid children exactly as if \u003Ccode>StatGroup\u003C\u002Fcode> didn&#39;t exist at all.\u003C\u002Fp>",{"id":235,"html":236,"text":236,"type":150,"level":31},"b26","Stage 2: group-level event listening",{"id":238,"html":239,"type":136},"b27","\u003Cp>A \u003Ccode>FragmentInstance\u003C\u002Fcode> exposes \u003Ccode>addEventListener\u003C\u002Fcode>, \u003Ccode>removeEventListener\u003C\u002Fcode>, and \u003Ccode>dispatchEvent\u003C\u002Fcode> — but they attach to \u003Cstrong>every first-level DOM child of the Fragment\u003C\u002Fstrong>, not to a single node:\u003C\u002Fp>",{"id":241,"code":242,"type":196,"language":197,"highlight":243},"b28","useEffect(() => {\n  const instance = groupRef.current;\n  const onClick = (e) => console.log(\"clicked inside the group:\", e.target);\n  instance.addEventListener(\"click\", onClick);\n  return () => instance.removeEventListener(\"click\", onClick);\n}, []);",[],{"id":245,"html":246,"type":136},"b29","\u003Cp>\u003Cstrong>Key concept:\u003C\u002Fstrong> this is one listener call standing in for &quot;a listener on each of the three cards,&quot; without ever touching \u003Ccode>StatCard\u003C\u002Fcode>&#39;s own code. That&#39;s the real payoff for a library component that doesn&#39;t want to force every consumer to wire up delegation by hand.\u003C\u002Fp>",{"id":248,"html":249,"text":249,"type":150,"level":31},"b30","Stage 3: observing the group with observeUsing",{"id":251,"html":252,"type":136},"b31","\u003Cp>This is the method that solves the dashboard problem from the top of the article — a real \u003Ccode>IntersectionObserver\u003C\u002Fcode>, pointed at the whole group, with no wrapper node to observe:\u003C\u002Fp>",{"id":254,"code":255,"type":196,"language":197,"highlight":256},"b32","useEffect(() => {\n  const instance = groupRef.current;\n  const observer = new IntersectionObserver(([entry]) => {\n    setVisible(entry.isIntersecting);\n  });\n  instance.observeUsing(observer);\n  return () => instance.unobserveUsing(observer);\n}, []);",[],{"id":258,"html":259,"type":136},"b33","\u003Cp>\u003Ccode>observeUsing\u003C\u002Fcode> accepts either an \u003Ccode>IntersectionObserver\u003C\u002Fcode> or a \u003Ccode>ResizeObserver\u003C\u002Fcode> and attaches it to every first-level DOM child — React handles fanning the single observer out to all three cards and reporting back.\u003C\u002Fp>",{"id":261,"html":262,"type":136},"b34","\u003Cp>\u003Cstrong>Key concept:\u003C\u002Fstrong> the grid from the opening example is now untouched. All three \u003Ccode>StatCard\u003C\u002Fcode>s are direct grid items, and the observer still fires on the group as a whole.\u003C\u002Fp>",{"id":264,"html":265,"text":265,"type":150,"level":31},"b35","Stage 4: focus management across the group",{"id":267,"html":268,"type":136},"b36","\u003Cp>\u003Ccode>focus()\u003C\u002Fcode> and \u003Ccode>focusLast()\u003C\u002Fcode> behave differently from the methods above — they search \u003Cstrong>all nested children, depth-first\u003C\u002Fstrong>, not just the first level:\u003C\u002Fp>",{"id":270,"code":271,"type":196,"language":197,"highlight":272},"b37","function Toolbar({ children }) {\n  const groupRef = useRef(null);\n  return (\n    \u003C>\n      \u003Cbutton onClick={() => groupRef.current.focus()}>Skip to toolbar\u003C\u002Fbutton>\n      \u003CFragment ref={groupRef}>{children}\u003C\u002FFragment>\n    \u003C\u002F>\n  );\n}",[],{"id":274,"html":275,"type":136},"b38","\u003Cp>\u003Ccode>focus()\u003C\u002Fcode> walks into however deeply nested the real focusable element is — a \u003Ccode>&lt;button&gt;\u003C\u002Fcode> three components down still gets found — and moves focus there. \u003Ccode>blur()\u003C\u002Fcode> removes focus from the active element, but only if that element is currently inside the Fragment; otherwise it&#39;s a no-op.\u003C\u002Fp>",{"id":277,"html":278,"type":136},"b39","\u003Cp>\u003Cstrong>Key concept:\u003C\u002Fstrong> this is the one family of methods that doesn&#39;t share the &quot;first-level only&quot; restriction the rest of the API has. Don&#39;t assume the restriction below applies uniformly — it doesn&#39;t.\u003C\u002Fp>",{"id":280,"html":281,"text":281,"type":150,"level":31},"b40","Stage 5: measuring and scrolling the group",{"id":283,"html":284,"type":136},"b41","\u003Cp>\u003Ccode>getClientRects()\u003C\u002Fcode> returns a flat array of \u003Ccode>DOMRect\u003C\u002Fcode>s — one or more per first-level child — so you can measure a sibling group as a unit without a wrapper to call \u003Ccode>getBoundingClientRect()\u003C\u002Fcode> on:\u003C\u002Fp>",{"id":286,"code":287,"type":196,"language":197,"highlight":288},"b42","const rects = groupRef.current.getClientRects();\nconst totalWidth = Math.max(...rects.map((r) => r.right)) - Math.min(...rects.map((r) => r.left));",[],{"id":290,"html":291,"type":136},"b43","\u003Cp>\u003Ccode>scrollIntoView(alignToTop?)\u003C\u002Fcode> scrolls the group&#39;s children into view — but its signature is narrower than the DOM method of the same name on a normal element:\u003C\u002Fp>",{"id":293,"code":294,"type":196,"language":197,"highlight":295},"b44","groupRef.current.scrollIntoView();      \u002F\u002F scrolls the first child to the top\ngroupRef.current.scrollIntoView(false); \u002F\u002F scrolls the last child to the bottom\ngroupRef.current.scrollIntoView({ behavior: \"smooth\" }); \u002F\u002F throws — see below",[],{"id":297,"html":298,"text":298,"type":150,"level":31},"b45","Edge cases and gotchas",{"id":300,"type":156,"items":301,"ordered":18},"b46",[302,303,304,305,306,307],"\u003Cstrong>Most methods only see first-level DOM children.\u003C\u002Fstrong> \u003Ccode>addEventListener\u003C\u002Fcode>, \u003Ccode>observeUsing\u003C\u002Fcode>, and \u003Ccode>getClientRects\u003C\u002Fcode> operate on the first real DOM nodes React finds walking down from the Fragment — through any nested components or fragments in between, but stopping the instant it hits an actual DOM element. If one of those children itself wraps more DOM (say, a \u003Ccode>&lt;Badge&gt;\u003C\u002Fcode> that renders \u003Ccode>&lt;div&gt;&lt;span&gt;…&lt;\u002Fspan&gt;&lt;\u002Fdiv&gt;\u003C\u002Fcode>), the inner \u003Ccode>&lt;span&gt;\u003C\u002Fcode> is one level too deep for a \u003Cem>direct\u003C\u002Fem> attachment — though a real click on it still bubbles up through the DOM to the \u003Ccode>&lt;div&gt;\u003C\u002Fcode> as normal, unless something calls \u003Ccode>stopPropagation()\u003C\u002Fcode> along the way.","\u003Cstrong>\u003Ccode>focus\u003C\u002Fcode>\u002F\u003Ccode>focusLast\u003C\u002Fcode> are the exception\u003C\u002Fstrong> — they search every nested child depth-first, specifically because &quot;find the first focusable thing in this group&quot; needs to look arbitrarily deep.","\u003Cstrong>\u003Ccode>observeUsing\u003C\u002Fcode> doesn&#39;t work on text nodes.\u003C\u002Fstrong> If the Fragment&#39;s only children are text, React logs a development warning rather than silently doing nothing.","\u003Cstrong>\u003Ccode>scrollIntoView\u003C\u002Fcode> takes a boolean, not an options object.\u003C\u002Fstrong> Passing \u003Ccode>{ behavior: &quot;smooth&quot; }\u003C\u002Fcode> the way you would to a DOM element&#39;s \u003Ccode>scrollIntoView\u003C\u002Fcode> throws an error — this is the one gotcha most likely to surface in code review, because the two APIs share a name but not a signature.","\u003Cstrong>Hidden \u003Ccode>Activity\u003C\u002Fcode> trees don&#39;t receive fragment listeners.\u003C\u002Fstrong> If the group is inside a hidden \u003Ccode>Activity\u003C\u002Fcode> boundary, \u003Ccode>addEventListener\u003C\u002Fcode> calls don&#39;t apply until the boundary becomes visible, at which point React applies them automatically.","\u003Cstrong>You cannot use the \u003Ccode>&lt;&gt;...&lt;\u002F&gt;\u003C\u002Fcode> shorthand.\u003C\u002Fstrong> Passing \u003Ccode>ref\u003C\u002Fcode> to the shorthand form isn&#39;t supported; you must \u003Ccode>import { Fragment } from &quot;react&quot;\u003C\u002Fcode> and write \u003Ccode>&lt;Fragment ref={...}&gt;\u003C\u002Fcode> explicitly. It&#39;s an easy habit to break, since the shorthand is the default almost everywhere else in a modern codebase.",{"id":309,"html":310,"text":310,"type":150,"level":31},"b47","Best practices: when to reach for this",{"id":312,"html":313,"type":136},"b48","\u003Cp>\u003Cstrong>Reach for a Fragment Ref when\u003C\u002Fstrong> a component renders a sibling group it doesn&#39;t want to visually wrap — a list&#39;s items, a group of grid cells, children of a component whose caller controls the surrounding layout — and the group still needs group-level DOM capability: delegated events, one \u003Ccode>IntersectionObserver\u003C\u002Fcode>\u002F\u003Ccode>ResizeObserver\u003C\u002Fcode>, keyboard-focus entry, or a combined bounding measurement.\u003C\u002Fp>",{"id":315,"html":316,"type":136},"b49","\u003Cp>\u003Cstrong>Don&#39;t reach for it when\u003C\u002Fstrong> the group itself needs actual styling — a background, a border, padding around the whole cluster. A Fragment still renders nothing to the DOM, so there&#39;s nothing there for CSS to select. That case genuinely needs a real wrapper element; a Fragment Ref isn&#39;t a style-free wrapper, it&#39;s a ref-only one.\u003C\u002Fp>",{"id":318,"html":319,"type":136},"b50","\u003Cp>\u003Cstrong>Don&#39;t reach for it for a single child\u003C\u002Fstrong>, either — just put the ref directly on that one element. The whole feature exists for the \u003Cem>group\u003C\u002Fem> case a single ref never covered.\u003C\u002Fp>",{"id":321,"html":322,"text":322,"type":150,"level":31},"b51","FAQ",{"id":324,"html":325,"text":326,"type":150,"level":43},"b52","Can I pass a ref to the \u003Ccode>&lt;&gt;...&lt;\u002F&gt;\u003C\u002Fcode> shorthand?","Can I pass a ref to the \u003C>...\u003C\u002F> shorthand?",{"id":328,"html":329,"type":136},"b53","\u003Cp>No. React requires the explicit form — \u003Ccode>import { Fragment } from &quot;react&quot;\u003C\u002Fcode> and \u003Ccode>&lt;Fragment ref={yourRef}&gt;...&lt;\u002FFragment&gt;\u003C\u002Fcode> — specifically so the shorthand stays free of the extra import for the common case that doesn&#39;t need a ref.\u003C\u002Fp>",{"id":331,"html":332,"text":332,"type":150,"level":43},"b54","Does a Fragment Ref add anything to the DOM?",{"id":334,"html":335,"type":136},"b55","\u003Cp>No. The \u003Ccode>FragmentInstance\u003C\u002Fcode> operates on the children&#39;s real DOM nodes as a group, without changing that DOM&#39;s structure at all. If you inspect the page, there&#39;s no new element — the children render exactly where they would without the ref.\u003C\u002Fp>",{"id":337,"html":338,"text":339,"type":150,"level":43},"b56","Will a click on a deeply nested element still reach a Fragment&#39;s addEventListener?","Will a click on a deeply nested element still reach a Fragment's addEventListener?",{"id":341,"html":342,"type":136},"b57","\u003Cp>Only if nothing stops it from bubbling. The listener attaches directly to the first real DOM element React finds under each child — not to anything nested one DOM level deeper inside that child&#39;s own markup. A plain DOM click event still bubbles upward in the usual way, so it typically reaches the attachment point regardless, unless an intervening handler calls \u003Ccode>stopPropagation()\u003C\u002Fcode>.\u003C\u002Fp>",{"id":344,"html":345,"text":345,"type":150,"level":43},"b58","Do I need anything beyond react-dom 19.3?",{"id":347,"html":348,"type":136},"b59","\u003Cp>No extra package. Fragment Refs ship as part of \u003Ccode>react\u003C\u002Fcode> and \u003Ccode>react-dom\u003C\u002Fcode> 19.3.0 together — there&#39;s no separate opt-in flag.\u003C\u002Fp>",{"id":350,"html":351,"text":351,"type":150,"level":43},"b60","Does the React Compiler change how Fragment Refs behave?",{"id":353,"html":354,"type":136},"b61","\u003Cp>No. The compiler auto-memoizes component output based on the Rules of React; it doesn&#39;t touch ref semantics. A \u003Ccode>FragmentInstance\u003C\u002Fcode> behaves identically whether or not the component that creates it is compiled.\u003C\u002Fp>",{"id":356,"html":357,"text":357,"type":150,"level":31},"b62","Cheat sheet",{"id":359,"code":360,"type":196,"language":197,"highlight":361},"b63","import { Fragment, useRef, useEffect } from \"react\";\n\nfunction Group({ children }) {\n  const ref = useRef(null); \u002F\u002F ref.current → a FragmentInstance\n\n  useEffect(() => {\n    const instance = ref.current;\n\n    \u002F\u002F Group-level click delegation (first-level DOM children only)\n    const onClick = (e) => {\u002F* ... *\u002F};\n    instance.addEventListener(\"click\", onClick);\n\n    \u002F\u002F One observer for the whole group (IntersectionObserver or ResizeObserver)\n    const observer = new IntersectionObserver(\u002F* ... *\u002F);\n    instance.observeUsing(observer);\n\n    return () => {\n      instance.removeEventListener(\"click\", onClick);\n      instance.unobserveUsing(observer);\n    };\n  }, []);\n\n  return \u003CFragment ref={ref}>{children}\u003C\u002FFragment>;\n}",[],{"id":363,"head":364,"rows":368,"type":395},"b64",[365,366,367],"Method","Targets","Notes",[369,373,376,379,383,387,391],[370,371,372],"\u003Ccode>addEventListener(type, fn, opts?)\u003C\u002Fcode>","first-level DOM children","mirrors removeEventListener\u002FdispatchEvent",[374,371,375],"\u003Ccode>observeUsing(observer)\u003C\u002Fcode>","IntersectionObserver or ResizeObserver; pair with \u003Ccode>unobserveUsing\u003C\u002Fcode>",[377,371,378],"\u003Ccode>getClientRects()\u003C\u002Fcode>","returns a flat \u003Ccode>DOMRect[]\u003C\u002Fcode>, one+ per child",[380,381,382],"\u003Ccode>focus(opts?)\u003C\u002Fcode> \u002F \u003Ccode>focusLast(opts?)\u003C\u002Fcode>","\u003Cstrong>all\u003C\u002Fstrong> nested children, depth-first","finds the first\u002Flast focusable element",[384,385,386],"\u003Ccode>blur()\u003C\u002Fcode>","the active element, if inside the group","no-op otherwise",[388,389,390],"\u003Ccode>scrollIntoView(alignToTop?)\u003C\u002Fcode>","the group&#39;s children","\u003Cstrong>boolean only\u003C\u002Fstrong> — an options object throws",[392,393,394],"\u003Ccode>getRootNode(opts?)\u003C\u002Fcode> \u002F \u003Ccode>compareDocumentPosition(node)\u003C\u002Fcode>","DOM tree queries","mirror the native \u003Ccode>Node\u003C\u002Fcode> methods","table",{"id":397,"type":156,"items":398,"ordered":18},"b65",[399,400,401],"Must use \u003Ccode>&lt;Fragment ref={...}&gt;\u003C\u002Fcode> from \u003Ccode>import { Fragment } from &quot;react&quot;\u003C\u002Fcode> — not \u003Ccode>&lt;&gt;...&lt;\u002F&gt;\u003C\u002Fcode>.","A Fragment Ref never adds a DOM node. No node, no CSS hook — style the children themselves.","Requires \u003Ccode>react\u003C\u002Fcode> and \u003Ccode>react-dom\u003C\u002Fcode> \u003Cstrong>19.3.0\u003C\u002Fstrong> or later.",{"id":403,"html":404,"text":404,"type":150,"level":31},"b66","Key takeaways",{"id":406,"type":156,"items":407,"ordered":18},"b67",[408,409,410,411,412],"A wrapper \u003Ccode>&lt;div&gt;\u003C\u002Fcode> added only to hold a ref is a real structural change — it becomes a layout participant whether you wanted one or not.","\u003Cstrong>Fragment Refs\u003C\u002Fstrong> (\u003Ccode>&lt;Fragment ref={...}&gt;\u003C\u002Fcode>, React 19.3+) give you a \u003Ccode>FragmentInstance\u003C\u002Fcode> with zero extra DOM, for group-level event listening, observing, focus, measuring, and scrolling.","Most methods — \u003Ccode>addEventListener\u003C\u002Fcode>, \u003Ccode>observeUsing\u003C\u002Fcode>, \u003Ccode>getClientRects\u003C\u002Fcode> — only see \u003Cstrong>first-level DOM children\u003C\u002Fstrong>; \u003Ccode>focus\u003C\u002Fcode>\u002F\u003Ccode>focusLast\u003C\u002Fcode> search every nested child, depth-first.","\u003Ccode>scrollIntoView\u003C\u002Fcode> takes a boolean, not the options object the native DOM method accepts — passing one throws.","Reach for this when the group needs ref-powered behavior but not a visual wrapper; reach for a real element when it needs actual CSS.",{"id":414,"html":415,"type":136},"b68","\u003C!-- playground:start -->",{"id":417,"html":418,"text":418,"type":150,"level":31},"b69","🎮 Try it yourself",{"id":420,"html":421,"type":136},"b70","\u003Cp>\u003Cstrong>\u003Ca href=\"https:\u002F\u002Fbestpractic.org\u002Fblog\u002Freact-weekly-fragment-refs\u002Fplayground\">▶️ Open the interactive playground →\u003C\u002Fa>\u003C\u002Fstrong>\u003C\u002Fp>",{"id":423,"html":424,"type":136},"b71","\u003Cp>\u003Cem>Runs right in your browser — poke at it and watch the concept react live.\u003C\u002Fem>\u003C\u002Fp>",{"id":426,"html":427,"type":136},"b72","\u003C!-- playground:end -->",{"id":429,"html":430,"type":136},"b73","\u003C!-- quiz:start -->",{"id":432,"html":433,"text":433,"type":150,"level":31},"b74","🧠 Test yourself",{"id":435,"html":436,"type":136},"b75","\u003Cp>Think it clicked? \u003Cstrong>\u003Ca href=\"https:\u002F\u002Fbestpractic.org\u002Fblog\u002Freact-weekly-fragment-refs\u002Fquiz\">Take the 8-question quiz →\u003C\u002Fa>\u003C\u002Fstrong>\u003C\u002Fp>",{"id":438,"html":439,"type":136},"b76","\u003Cp>\u003Cem>Instant feedback, a hint on every question, and an explanation for each answer — right or wrong.\u003C\u002Fem>\u003C\u002Fp>",{"id":441,"html":442,"type":136},"b77","\u003C!-- quiz:end -->",{"id":444,"html":445,"type":136},"b78","\u003Cp>That dashboard from the opening scene ships today with its \u003Ccode>IntersectionObserver\u003C\u002Fcode> intact and its six-column grid untouched — the fix was never the observer, it was the \u003Ccode>&lt;div&gt;\u003C\u002Fcode> standing in for a ref no element needed to carry. If you&#39;ve shipped your own wrapper-for-a-ref before, the chances are decent a \u003Ccode>Fragment ref\u003C\u002Fcode> would have made it disappear entirely.\u003C\u002Fp>",{"id":447,"html":448,"type":136},"b79","\u003Cp>If \u003Ccode>rerender-vs-remount\u003C\u002Fcode> taught you that \u003Ccode>key\u003C\u002Fcode> controls which \u003Ca href=\"https:\u002F\u002Fdev.to\u002Fparsajiravand\u002Freact-re-render-vs-remount-what-actually-triggers-each-5fok\">React instance\u003C\u002Fa> you&#39;re looking at, this is the companion lesson on the other side of the same tree: a \u003Ccode>Fragment\u003C\u002Fcode> never had an instance of its own in the DOM, and now it can still hand you one anyway. And if you&#39;re already running React 19.3 for \u003Ca href=\"https:\u002F\u002Fdev.to\u002Fparsajiravand\u002Freact-193-viewtransition-animate-state-without-losing-it-4kpp\">\u003Ccode>&lt;ViewTransition&gt;\u003C\u002Fcode>\u003C\u002Fa>, Fragment Refs shipped in the very same release — it&#39;s worth knowing both landed together.\u003C\u002Fp>",{"id":450,"html":451,"type":136},"b80","\u003Cp>What&#39;s the ugliest wrapper-\u003Ccode>div\u003C\u002Fcode>-for-a-ref you&#39;ve shipped — would a Fragment Ref have fixed it? Tell me below.\u003C\u002Fp>",{"id":453,"html":454,"type":136},"b81","\u003C!-- related:start -->",{"id":456,"html":457,"text":457,"type":150,"level":31},"b82","📚 Read next",{"id":459,"type":156,"items":460,"ordered":18},"b83",[461,462,463],"\u003Ca href=\"https:\u002F\u002Fbestpractic.org\u002Fblog\u002Freact-weekly-derived-state-bug\">React Derived State: Why That useState Is Probably a Bug\u003C\u002Fa>","\u003Ca href=\"https:\u002F\u002Fbestpractic.org\u002Fblog\u002Freact-weekly-rerender-vs-remount\">React Re-render vs Remount: What Actually Triggers Each\u003C\u002Fa>","\u003Ca href=\"https:\u002F\u002Fbestpractic.org\u002Fblog\u002Freact-weekly-viewtransition-component\">React 19.3 ViewTransition: Animate State Without Losing It\u003C\u002Fa>",{"id":465,"html":466,"type":136},"b84","\u003C!-- related:end -->",{"id":468,"type":469},"b85","divider",{"id":471,"html":472,"type":136},"b86","\u003Cp>🚀 \u003Cstrong>Want more like this?\u003C\u002Fstrong> Every guide, playground, and quiz lives on \u003Cstrong>\u003Ca href=\"https:\u002F\u002Fbestpractic.org\u002F\">bestpractic.org\u003C\u002Fa>\u003C\u002Fstrong> — open it and \u003Cstrong>\u003Ca href=\"https:\u002F\u002Fbestpractic.org\u002F\">sign up free\u003C\u002Fa>\u003C\u002Fstrong> so the next one finds you.\u003C\u002Fp>",{"id":474,"html":475,"type":136},"b87","\u003Cp>\u003Cem>Thanks for reading! Let&#39;s stay connected:\u003C\u002Fem>\u003C\u002Fp>",{"id":477,"type":156,"items":478,"ordered":18},"b88",[479,480,481],"⭐ \u003Cstrong>GitHub\u003C\u002Fstrong> — follow me and star the projects: \u003Ca href=\"https:\u002F\u002Fgithub.com\u002Fparsajiravand\">github.com\u002Fparsajiravand\u003C\u002Fa>","💬 \u003Cstrong>Discord\u003C\u002Fstrong> — join the frontend best-practices community: \u003Ca href=\"https:\u002F\u002Fdiscord.gg\u002Fd9KRhuAwQ\">discord.gg\u002Fd9KRhuAwQ\u003C\u002Fa>","📸 \u003Cstrong>Instagram\u003C\u002Fstrong> — frontend best practices, daily: \u003Ca href=\"https:\u002F\u002Fwww.instagram.com\u002Fbestpractice___\u002F\">@bestpractice___\u003C\u002Fa>","You're building a dashboard. Three stat cards need to render as siblings inside a CSS grid that expects each one to land in its own column — and you need a single ref on the group of three, so an `IntersectionObserver` can tell you when the cluster scrolls into view. There's no component boundary to hang that ref on, so you reach for the oldest trick in the book: wrap the three cards in a `\u003Cdiv>`.\n\nThe grid breaks. Your three cards, each meant to be its own grid item, are now squashed inside the one cell the wrapper `\u003Cdiv>` occupies.\n\n**React 19.3 (released September 9, 2026) ships a direct fix for this: a ref on a `\u003CFragment>`.** No wrapper, no extra DOM node, and a real object back — a `FragmentInstance` — that can do more than a typical element ref ever could. This article covers what Fragment Refs actually are, the exact API you get, and the restrictions that will bite you if you don't know about them up front.\n\nThis article is written against **React 19.3.0** (verified against the current npm `latest` dist-tag and the official React blog's 19.3 release post) and assumes React 19-era function components and hooks throughout. Fragment Refs are a DOM-rendering feature, so they need `react-dom` 19.3 or later alongside `react` 19.3 or later.\n\n## What you'll learn\n\nBy the end of this article you'll be able to:\n\n- Explain exactly why a wrapper `\u003Cdiv>` added only to hold a ref is a real structural change, not a free one\n- Attach a ref to a `\u003CFragment>` and read what comes back — a `FragmentInstance`, not a DOM node\n- Use the `FragmentInstance` methods that act on a sibling group as a unit: `addEventListener`, `observeUsing`, `focus`\u002F`focusLast`, `getClientRects`, `scrollIntoView`\n- Tell which methods only see **first-level DOM children** and which search every nested child, so you don't get a silent no-op\n- Decide when a Fragment Ref is the right tool and when a real wrapper element still is\n\n## Who this is for\n\nYou've called `useRef` to grab a DOM node, and you've written a wrapper `\u003Cdiv>` at least once for no reason other than \"something needs to hold the ref.\" No class components or Server Components knowledge required.\n\n## Table of contents\n\n- [The problem: a ref has nowhere to live](#the-problem-a-ref-has-nowhere-to-live)\n- [The mental model](#the-mental-model)\n- [Stage 1: a ref on a Fragment](#stage-1-a-ref-on-a-fragment)\n- [Stage 2: group-level event listening](#stage-2-group-level-event-listening)\n- [Stage 3: observing the group with observeUsing](#stage-3-observing-the-group-with-observeusing)\n- [Stage 4: focus management across the group](#stage-4-focus-management-across-the-group)\n- [Stage 5: measuring and scrolling the group](#stage-5-measuring-and-scrolling-the-group)\n- [Edge cases and gotchas](#edge-cases-and-gotchas)\n- [Best practices: when to reach for this](#best-practices-when-to-reach-for-this)\n- [FAQ](#faq)\n- [Cheat sheet](#cheat-sheet)\n- [Key takeaways](#key-takeaways)\n\n## The problem: a ref has nowhere to live\n\nHere's the dashboard component, written the way most of us would write it before 19.3:\n\n```jsx\n\u002F\u002F The wrong way first: wrap the group just to hold a ref.\nfunction StatGroup({ stats }) {\n  const groupRef = useRef(null);\n\n  useEffect(() => {\n    const observer = new IntersectionObserver(([entry]) => {\n      console.log(\"group visible:\", entry.isIntersecting);\n    });\n    observer.observe(groupRef.current);\n    return () => observer.disconnect();\n  }, []);\n\n  return (\n    \u003Cdiv ref={groupRef} className=\"stat-group\">\n      {stats.map((s) => (\n        \u003CStatCard key={s.id} {...s} \u002F>\n      ))}\n    \u003C\u002Fdiv>\n  );\n}\n```\n\nThat `\u003Cdiv className=\"stat-group\">` is the only reason this code compiles — `useRef` needs a real DOM node to attach to, and a plain `\u003C>...\u003C\u002F>` fragment can't take a ref. But the parent that renders `\u003CStatGroup \u002F>` is a CSS grid:\n\n```css\n.dashboard {\n  display: grid;\n  grid-template-columns: repeat(6, 1fr);\n  gap: 12px;\n}\n```\n\nIt expects every stat card to be its own grid item, interleaved with other dashboard widgets. Instead, the wrapper `\u003Cdiv>` becomes *one* grid item — the three cards it contains collapse into that single cell instead of occupying three. You can reach for `display: contents` on the wrapper to make it disappear from layout, but that has its own cost: Safari has shipped multiple bugs around `display: contents` and focus\u002Faccessibility trees, and the node still shows up in `:nth-child` counts and `querySelector` results in the parent — it just doesn't participate visually. You've traded a layout bug for an invisible structural one.\n\nThe real problem isn't the `IntersectionObserver` call. It's that **the only way to get a ref has always been to add a DOM node**, whether or not your layout wanted one.\n\n## The mental model\n\n**The mental model:** a `Fragment` has never rendered a DOM node — that's its entire purpose, grouping children for React's own bookkeeping without touching the real DOM tree. A ref on a `Fragment` doesn't change that. What changes is that React now hands you a `FragmentInstance`: a stable object that knows which real DOM nodes belong to this group and can act on all of them as a unit.\n\nThink of it less like a new element you could select with CSS, and more like **a remote control for \"all the first-level DOM children of this Fragment.\"** You press a button on the remote (`addEventListener`, `observeUsing`, `focus`) and it reaches every child in the group — but it never adds a node for the remote itself to live on.\n\n## Stage 1: a ref on a Fragment\n\nThe `\u003C>...\u003C\u002F>` shorthand can't take a ref — you have to import `Fragment` explicitly:\n\n```jsx\nimport { Fragment, useRef, useEffect } from \"react\";\n\nfunction StatGroup({ stats }) {\n  const groupRef = useRef(null);\n\n  useEffect(() => {\n    console.log(groupRef.current); \u002F\u002F a FragmentInstance, not a DOM node\n  }, []);\n\n  return (\n    \u003CFragment ref={groupRef}>\n      {stats.map((s) => (\n        \u003CStatCard key={s.id} {...s} \u002F>\n      ))}\n    \u003C\u002FFragment>\n  );\n}\n```\n\n**Key concept:** `groupRef.current` is now a `FragmentInstance` object. There is still zero extra DOM in the tree — the three `StatCard` elements render as direct grid children exactly as if `StatGroup` didn't exist at all.\n\n## Stage 2: group-level event listening\n\nA `FragmentInstance` exposes `addEventListener`, `removeEventListener`, and `dispatchEvent` — but they attach to **every first-level DOM child of the Fragment**, not to a single node:\n\n```jsx\nuseEffect(() => {\n  const instance = groupRef.current;\n  const onClick = (e) => console.log(\"clicked inside the group:\", e.target);\n  instance.addEventListener(\"click\", onClick);\n  return () => instance.removeEventListener(\"click\", onClick);\n}, []);\n```\n\n**Key concept:** this is one listener call standing in for \"a listener on each of the three cards,\" without ever touching `StatCard`'s own code. That's the real payoff for a library component that doesn't want to force every consumer to wire up delegation by hand.\n\n## Stage 3: observing the group with observeUsing\n\nThis is the method that solves the dashboard problem from the top of the article — a real `IntersectionObserver`, pointed at the whole group, with no wrapper node to observe:\n\n```jsx\nuseEffect(() => {\n  const instance = groupRef.current;\n  const observer = new IntersectionObserver(([entry]) => {\n    setVisible(entry.isIntersecting);\n  });\n  instance.observeUsing(observer);\n  return () => instance.unobserveUsing(observer);\n}, []);\n```\n\n`observeUsing` accepts either an `IntersectionObserver` or a `ResizeObserver` and attaches it to every first-level DOM child — React handles fanning the single observer out to all three cards and reporting back.\n\n**Key concept:** the grid from the opening example is now untouched. All three `StatCard`s are direct grid items, and the observer still fires on the group as a whole.\n\n## Stage 4: focus management across the group\n\n`focus()` and `focusLast()` behave differently from the methods above — they search **all nested children, depth-first**, not just the first level:\n\n```jsx\nfunction Toolbar({ children }) {\n  const groupRef = useRef(null);\n  return (\n    \u003C>\n      \u003Cbutton onClick={() => groupRef.current.focus()}>Skip to toolbar\u003C\u002Fbutton>\n      \u003CFragment ref={groupRef}>{children}\u003C\u002FFragment>\n    \u003C\u002F>\n  );\n}\n```\n\n`focus()` walks into however deeply nested the real focusable element is — a `\u003Cbutton>` three components down still gets found — and moves focus there. `blur()` removes focus from the active element, but only if that element is currently inside the Fragment; otherwise it's a no-op.\n\n**Key concept:** this is the one family of methods that doesn't share the \"first-level only\" restriction the rest of the API has. Don't assume the restriction below applies uniformly — it doesn't.\n\n## Stage 5: measuring and scrolling the group\n\n`getClientRects()` returns a flat array of `DOMRect`s — one or more per first-level child — so you can measure a sibling group as a unit without a wrapper to call `getBoundingClientRect()` on:\n\n```jsx\nconst rects = groupRef.current.getClientRects();\nconst totalWidth = Math.max(...rects.map((r) => r.right)) - Math.min(...rects.map((r) => r.left));\n```\n\n`scrollIntoView(alignToTop?)` scrolls the group's children into view — but its signature is narrower than the DOM method of the same name on a normal element:\n\n```jsx\ngroupRef.current.scrollIntoView();      \u002F\u002F scrolls the first child to the top\ngroupRef.current.scrollIntoView(false); \u002F\u002F scrolls the last child to the bottom\ngroupRef.current.scrollIntoView({ behavior: \"smooth\" }); \u002F\u002F throws — see below\n```\n\n## Edge cases and gotchas\n\n- **Most methods only see first-level DOM children.** `addEventListener`, `observeUsing`, and `getClientRects` operate on the first real DOM nodes React finds walking down from the Fragment — through any nested components or fragments in between, but stopping the instant it hits an actual DOM element. If one of those children itself wraps more DOM (say, a `\u003CBadge>` that renders `\u003Cdiv>\u003Cspan>…\u003C\u002Fspan>\u003C\u002Fdiv>`), the inner `\u003Cspan>` is one level too deep for a *direct* attachment — though a real click on it still bubbles up through the DOM to the `\u003Cdiv>` as normal, unless something calls `stopPropagation()` along the way.\n- **`focus`\u002F`focusLast` are the exception** — they search every nested child depth-first, specifically because \"find the first focusable thing in this group\" needs to look arbitrarily deep.\n- **`observeUsing` doesn't work on text nodes.** If the Fragment's only children are text, React logs a development warning rather than silently doing nothing.\n- **`scrollIntoView` takes a boolean, not an options object.** Passing `{ behavior: \"smooth\" }` the way you would to a DOM element's `scrollIntoView` throws an error — this is the one gotcha most likely to surface in code review, because the two APIs share a name but not a signature.\n- **Hidden `Activity` trees don't receive fragment listeners.** If the group is inside a hidden `Activity` boundary, `addEventListener` calls don't apply until the boundary becomes visible, at which point React applies them automatically.\n- **You cannot use the `\u003C>...\u003C\u002F>` shorthand.** Passing `ref` to the shorthand form isn't supported; you must `import { Fragment } from \"react\"` and write `\u003CFragment ref={...}>` explicitly. It's an easy habit to break, since the shorthand is the default almost everywhere else in a modern codebase.\n\n## Best practices: when to reach for this\n\n**Reach for a Fragment Ref when** a component renders a sibling group it doesn't want to visually wrap — a list's items, a group of grid cells, children of a component whose caller controls the surrounding layout — and the group still needs group-level DOM capability: delegated events, one `IntersectionObserver`\u002F`ResizeObserver`, keyboard-focus entry, or a combined bounding measurement.\n\n**Don't reach for it when** the group itself needs actual styling — a background, a border, padding around the whole cluster. A Fragment still renders nothing to the DOM, so there's nothing there for CSS to select. That case genuinely needs a real wrapper element; a Fragment Ref isn't a style-free wrapper, it's a ref-only one.\n\n**Don't reach for it for a single child**, either — just put the ref directly on that one element. The whole feature exists for the *group* case a single ref never covered.\n\n## FAQ\n\n### Can I pass a ref to the `\u003C>...\u003C\u002F>` shorthand?\n\nNo. React requires the explicit form — `import { Fragment } from \"react\"` and `\u003CFragment ref={yourRef}>...\u003C\u002FFragment>` — specifically so the shorthand stays free of the extra import for the common case that doesn't need a ref.\n\n### Does a Fragment Ref add anything to the DOM?\n\nNo. The `FragmentInstance` operates on the children's real DOM nodes as a group, without changing that DOM's structure at all. If you inspect the page, there's no new element — the children render exactly where they would without the ref.\n\n### Will a click on a deeply nested element still reach a Fragment's addEventListener?\n\nOnly if nothing stops it from bubbling. The listener attaches directly to the first real DOM element React finds under each child — not to anything nested one DOM level deeper inside that child's own markup. A plain DOM click event still bubbles upward in the usual way, so it typically reaches the attachment point regardless, unless an intervening handler calls `stopPropagation()`.\n\n### Do I need anything beyond react-dom 19.3?\n\nNo extra package. Fragment Refs ship as part of `react` and `react-dom` 19.3.0 together — there's no separate opt-in flag.\n\n### Does the React Compiler change how Fragment Refs behave?\n\nNo. The compiler auto-memoizes component output based on the Rules of React; it doesn't touch ref semantics. A `FragmentInstance` behaves identically whether or not the component that creates it is compiled.\n\n## Cheat sheet\n\n```jsx\nimport { Fragment, useRef, useEffect } from \"react\";\n\nfunction Group({ children }) {\n  const ref = useRef(null); \u002F\u002F ref.current → a FragmentInstance\n\n  useEffect(() => {\n    const instance = ref.current;\n\n    \u002F\u002F Group-level click delegation (first-level DOM children only)\n    const onClick = (e) => {\u002F* ... *\u002F};\n    instance.addEventListener(\"click\", onClick);\n\n    \u002F\u002F One observer for the whole group (IntersectionObserver or ResizeObserver)\n    const observer = new IntersectionObserver(\u002F* ... *\u002F);\n    instance.observeUsing(observer);\n\n    return () => {\n      instance.removeEventListener(\"click\", onClick);\n      instance.unobserveUsing(observer);\n    };\n  }, []);\n\n  return \u003CFragment ref={ref}>{children}\u003C\u002FFragment>;\n}\n```\n\n| Method | Targets | Notes |\n| --- | --- | --- |\n| `addEventListener(type, fn, opts?)` | first-level DOM children | mirrors removeEventListener\u002FdispatchEvent |\n| `observeUsing(observer)` | first-level DOM children | IntersectionObserver or ResizeObserver; pair with `unobserveUsing` |\n| `getClientRects()` | first-level DOM children | returns a flat `DOMRect[]`, one+ per child |\n| `focus(opts?)` \u002F `focusLast(opts?)` | **all** nested children, depth-first | finds the first\u002Flast focusable element |\n| `blur()` | the active element, if inside the group | no-op otherwise |\n| `scrollIntoView(alignToTop?)` | the group's children | **boolean only** — an options object throws |\n| `getRootNode(opts?)` \u002F `compareDocumentPosition(node)` | DOM tree queries | mirror the native `Node` methods |\n\n- Must use `\u003CFragment ref={...}>` from `import { Fragment } from \"react\"` — not `\u003C>...\u003C\u002F>`.\n- A Fragment Ref never adds a DOM node. No node, no CSS hook — style the children themselves.\n- Requires `react` and `react-dom` **19.3.0** or later.\n\n## Key takeaways\n\n- A wrapper `\u003Cdiv>` added only to hold a ref is a real structural change — it becomes a layout participant whether you wanted one or not.\n- **Fragment Refs** (`\u003CFragment ref={...}>`, React 19.3+) give you a `FragmentInstance` with zero extra DOM, for group-level event listening, observing, focus, measuring, and scrolling.\n- Most methods — `addEventListener`, `observeUsing`, `getClientRects` — only see **first-level DOM children**; `focus`\u002F`focusLast` search every nested child, depth-first.\n- `scrollIntoView` takes a boolean, not the options object the native DOM method accepts — passing one throws.\n- Reach for this when the group needs ref-powered behavior but not a visual wrapper; reach for a real element when it needs actual CSS.\n\n\u003C!-- playground:start -->\n\n## 🎮 Try it yourself\n\n**[▶️ Open the interactive playground →](https:\u002F\u002Fbestpractic.org\u002Fblog\u002Freact-weekly-fragment-refs\u002Fplayground)**\n\n_Runs right in your browser — poke at it and watch the concept react live._\n\n\u003C!-- playground:end -->\n\n\u003C!-- quiz:start -->\n\n## 🧠 Test yourself\n\nThink it clicked? **[Take the 8-question quiz →](https:\u002F\u002Fbestpractic.org\u002Fblog\u002Freact-weekly-fragment-refs\u002Fquiz)**\n\n_Instant feedback, a hint on every question, and an explanation for each answer — right or wrong._\n\n\u003C!-- quiz:end -->\n\nThat dashboard from the opening scene ships today with its `IntersectionObserver` intact and its six-column grid untouched — the fix was never the observer, it was the `\u003Cdiv>` standing in for a ref no element needed to carry. If you've shipped your own wrapper-for-a-ref before, the chances are decent a `Fragment ref` would have made it disappear entirely.\n\nIf `rerender-vs-remount` taught you that `key` controls which [React instance](https:\u002F\u002Fdev.to\u002Fparsajiravand\u002Freact-re-render-vs-remount-what-actually-triggers-each-5fok) you're looking at, this is the companion lesson on the other side of the same tree: a `Fragment` never had an instance of its own in the DOM, and now it can still hand you one anyway. And if you're already running React 19.3 for [`\u003CViewTransition>`](https:\u002F\u002Fdev.to\u002Fparsajiravand\u002Freact-193-viewtransition-animate-state-without-losing-it-4kpp), Fragment Refs shipped in the very same release — it's worth knowing both landed together.\n\nWhat's the ugliest wrapper-`div`-for-a-ref you've shipped — would a Fragment Ref have fixed it? Tell me below.\n\n\u003C!-- related:start -->\n\n## 📚 Read next\n\n- [React Derived State: Why That useState Is Probably a Bug](https:\u002F\u002Fbestpractic.org\u002Fblog\u002Freact-weekly-derived-state-bug)\n- [React Re-render vs Remount: What Actually Triggers Each](https:\u002F\u002Fbestpractic.org\u002Fblog\u002Freact-weekly-rerender-vs-remount)\n- [React 19.3 ViewTransition: Animate State Without Losing It](https:\u002F\u002Fbestpractic.org\u002Fblog\u002Freact-weekly-viewtransition-component)\n\n\u003C!-- related:end -->\n\n---\n\n🚀 **Want more like this?** Every guide, playground, and quiz lives on **[bestpractic.org](https:\u002F\u002Fbestpractic.org\u002F)** — open it and **[sign up free](https:\u002F\u002Fbestpractic.org\u002F)** so the next one finds you.\n\n*Thanks for reading! Let's stay connected:*\n\n- ⭐ **GitHub** — follow me and star the projects: [github.com\u002Fparsajiravand](https:\u002F\u002Fgithub.com\u002Fparsajiravand)\n- 💬 **Discord** — join the frontend best-practices community: [discord.gg\u002Fd9KRhuAwQ](https:\u002F\u002Fdiscord.gg\u002Fd9KRhuAwQ)\n- 📸 **Instagram** — frontend best practices, daily: [@bestpractice___](https:\u002F\u002Fwww.instagram.com\u002Fbestpractice___\u002F)",{"title":109,"canonical":484,"description":110},"https:\u002F\u002Fbestpractic.org\u002Fblog\u002Freact-weekly-fragment-refs","01a117e0-acb2-740f-9391-dc7f5dbc982c",{"name":487,"part":488,"total":488,"items":489},"React Deep Dive",6,[490,494,498,502,507,512],{"slug":491,"title":492,"publishedAt":493,"readingMinutes":112},"react-weekly-rerender-vs-remount","React Re-render vs Remount: What Actually Triggers Each","2026-08-29T12:32:41.200Z",{"slug":495,"title":496,"publishedAt":497,"readingMinutes":112},"react-weekly-compiler-memoization","React Compiler 1.0: What useMemo You Can Delete","2026-09-05T10:17:49.849Z",{"slug":499,"title":500,"publishedAt":501,"readingMinutes":112},"react-weekly-form-actions-pending-state","React Form Actions: useActionState & useFormStatus Guide","2026-09-12T10:23:52.928Z",{"slug":503,"title":504,"publishedAt":505,"readingMinutes":506},"react-weekly-derived-state-bug","React Derived State: Why That useState Is Probably a Bug","2026-09-20T16:27:07.271Z",14,{"slug":508,"title":509,"publishedAt":510,"readingMinutes":511},"react-weekly-viewtransition-component","React 19.3 ViewTransition: Animate State Without Losing It","2026-10-03T11:22:26.437Z",15,{"slug":46,"title":109,"publishedAt":113,"readingMinutes":112},{"id":514,"locked":18},"01a117e0-ad4f-70b8-a444-a9771a9a2a7b",[516],{"id":45,"slug":46,"title":48,"_count":517},{"questions":51},[519],{"locale":13,"slug":46},{"id":45,"slug":46,"title":48,"_count":521,"questionCount":51},{"questions":51}]