# Fragment Refs у React 19.3: навіщо ref одразу на групу елементів

> React 19.3 дозволяє повісити ref на <Fragment> і отримати FragmentInstance: фокус, події та observer-и для групи сусідніх елементів без зайвого wrapper-div. Розбираємо API і реальні кейси.

- Автор: Юра Скиба (https://cookiesoftware.io)
- Опубліковано: 2026-10-01
- Категорія: React і Frontend
- Canonical: https://cookiesoftware.io/blog/react-fragment-refs

---

**Коротка відповідь.** У React 19.3 `<Fragment>` приймає `ref` і повертає `FragmentInstance` – об'єкт, що працює з DOM-вузлами дітей фрагмента як із групою: `focus()`, `addEventListener()`, `observeUsing(observer)`, `getClientRects()` – без зміни структури DOM. Це закриває давню дірку: щоб керувати групою сусідніх елементів, більше не треба загортати їх у зайвий `div` чи вручну збирати refs з кожної дитини. Нижче – що саме вміє API, повний приклад accessible toolbar і чесний список ситуацій, де Fragment ref не потрібен.

## Проблема зайвого wrapper-div

Класична ситуація: компонент рендерить групу сусідніх елементів, а тобі потрібно щось зробити з ними як із цілим – перевести фокус, повісити обробник, виміряти позицію.

До 19.3 варіантів було три, і всі з компромісами:

- **Wrapper-div.** Працює, але ламає розмітку: зайвий вузол у flex/grid-контейнері змінює поведінку лейаута, а в списках і таблицях – ще й семантику для скрінрідерів.
- **Ref на кожну дитину.** Шум у коді, а якщо діти – чужі компоненти, які не прокидають `ref`, то й глухий кут.
- **Обхід DOM руками** через `parentElement`/`querySelectorAll`. Крихко: React нічого не гарантує про сусідів поза своїм деревом.

Fragment Refs – четвертий варіант без цих компромісів.

## Що дає ref на Fragment

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

function PostHeadings({ posts }: { posts: Post[] }) {
  const fragmentRef = useRef<FragmentInstance | null>(null);

  useEffect(() => {
    // Фокус піде по дітях фрагмента depth-first
    fragmentRef.current?.focus();
  }, []);

  return (
    <Fragment ref={fragmentRef}>
      {posts.map((post) => (
        <Heading key={post.id}>{post.title}</Heading>
      ))}
    </Fragment>
  );
}
```

`FragmentInstance` оперує DOM-ом дітей **як групою, не змінюючи структуру**. За офіційним анонсом React 19.3, API складається з трьох блоків:

- **Події:** `addEventListener()`, `removeEventListener()`, `dispatchEvent()` – для first-level дітей фрагмента.
- **Фокус:** `focus()` (depth-first по вкладених дітях), `focusLast()`, `blur()`.
- **Спостереження і вимірювання:** `observeUsing(observer)` / `unobserveUsing(observer)` для `IntersectionObserver` або `ResizeObserver`, `getClientRects()`, `scrollIntoView()`, `getRootNode()`, `compareDocumentPosition()`.

Ключове обмеження, про яке анонс каже прямо: інструменти працюють із **first-level дітьми** фрагмента. Це група сусідів, а не рекурсивний контроль над усім піддеревом.

## Приклад: toolbar без зайвого div

Тулбар за [ARIA-патерном](https://www.w3.org/WAI/ARIA/apg/patterns/toolbar/) має одну тонкість: при поверненні фокуса в тулбар фокус має отримати кнопка, а сам контейнер – ні. Коли кнопки рендеряться різними компонентами, раніше доводилось або загортати все в div і шукати кнопки `querySelector`-ом, або тягнути ref у кожен компонент.

З Fragment ref група лишається «пласкою» – кнопки живуть прямо у flex-контейнері сторінки:

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

function EditorToolbar() {
  const itemsRef = useRef<FragmentInstance | null>(null);

  // Користувач повертається в тулбар хоткеєм – фокусуємо
  // першу кнопку групи, не знаючи, який компонент її рендерить
  function focusToolbar() {
    itemsRef.current?.focus();
  }

  return (
    <div role="toolbar" aria-label="Форматування" onKeyDown={handleArrows}>
      <Fragment ref={itemsRef}>
        <BoldButton />
        <ItalicButton />
        <HistoryControls /> {/* рендерить дві кнопки – і це ок */}
      </Fragment>
    </div>
  );
}
```

`BoldButton` та `HistoryControls` не мусять прокидати `ref` – фрагмент бачить їхні DOM-вузли як своїх дітей. Логіку стрілок вліво/вправо (roving tabindex) усе ще пишеш сам – Fragment ref дає тобі групу вузлів, а не готовий behaviour.

Другий кейс – прямо з доків React: компонент `InView`, що повідомляє про видимість групи дітей, підключаючи `IntersectionObserver` через `observeUsing`:

```tsx
export function InView({ onChange, children }: InViewProps) {
  const fragmentRef = useRef<FragmentInstance | null>(null);

  useLayoutEffect(() => {
    const visible = new Set<Element>();
    const observer = new IntersectionObserver((entries) => {
      for (const entry of entries) {
        entry.isIntersecting
          ? visible.add(entry.target)
          : visible.delete(entry.target);
      }
      onChange(visible.size > 0);
    });
    const instance = fragmentRef.current;
    instance?.observeUsing(observer);
    return () => instance?.unobserveUsing(observer);
  }, [onChange]);

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

Аналітика видимості секцій, lazy-ефекти, «прочитаність» блоків статті – усе без обгортки, яка в grid-розмітці зламала б колонки.

## Де Fragment ref не потрібен

Новий API – не привід викидати звичайні refs:

- **Один елемент – один ref.** Якщо ціль одна, `useRef` на елемент простіший і прозоріший.
- **Потрібен контейнер семантично.** Для `role="toolbar"`, `role="listbox"` чи скрол-контейнера однаково потрібен реальний DOM-вузол – фрагмент його не замінює, він лише прибирає *зайві* обгортки.
- **Глибокий контроль піддерева.** Fragment ref працює з first-level дітьми; для вкладених структур потрібна інша архітектура (контекст, композиція).
- **Стилізація.** Фрагменту не можна дати клас чи стиль – це досі про DOM-вузли.

## Міграція з workaround-ів

Якщо у твоєму коді є патерни нижче – вони кандидати на спрощення:

1. `<div style={{ display: 'contents' }}>` заради ref – пряма заміна на `<Fragment ref>`: display:contents якраз і намагався «прибрати» обгортку з лейаута, але лишав вузол у DOM (і має нюанси з доступністю в старих браузерах).
2. Колекція refs через `useRef(new Map())` + колбек-ref на кожній дитині – якщо все, що ти робив, це фокус/події/observer на групі, Fragment ref робить це без Map.
3. `containerRef.current.querySelectorAll('button')` – крихкий обхід замінюється на `addEventListener`/`focus` рівня групи.

Тестування: поведінка лишається DOM-поведінкою, тож тести «як користувач» не зміняться – [фокус і події перевіряються так само](/blog/react-testing-strategy), як і до міграції. Це хороший маркер, що ти тестував поведінку, а не implementation details.

## Типові помилки

- **Пхати Fragment ref усюди.** Якщо ref-а раніше не було і все працювало – він не потрібен і зараз.
- **Чекати рекурсії.** Методи працюють по first-level дітях; вкладені фрагменти – окремі інстанси.
- **Забувати cleanup для observer-ів.** `unobserveUsing` у cleanup-функції ефекту обов'язковий, як і зі звичайним observer-ом.
- **Керувати фокусом без потреби.** Автофокус групи при кожному рендері – шлях до роздратованих користувачів клавіатури. Фокус переводять у відповідь на дію користувача; детальніше – у [чеклісті доступності](/blog/react-accessibility-audit).

## Практичне завдання

Візьми будь-який свій компонент, де є wrapper-div лише заради ref (перевір: чи має він клас/роль/стилі? Якщо ні – він «технічний»). Перепиши на `<Fragment ref>`: (1) фокус першої дитини по хоткею, (2) `IntersectionObserver` через `observeUsing` з логом видимості. Запусти і порівняй DOM у девтулзах до/після – зайвий вузол має зникнути, а flex/grid-розмітка стати простішою.

Fragment Refs – з тих API, які не міняють архітектуру, але прибирають дрібне тертя, що накопичувалось роками. Разом із [View Transitions](/blog/react-view-transitions) React 19.3 вийшов релізом про «якість життя» – і це приємний тренд.
