12k
All articles

Как использовать refs для Fragment в React 19.3

Используйте refs Fragment в React 19.3 для событий, фокуса, наблюдателей и измерений группы соседних элементов без обертки. Узнайте об ограничениях и случаях, когда нужен DOM-элемент.

OpenReplay Team
OpenReplay Team
Как использовать refs для Fragment в React 19.3

В React 19.3 передача ref в <Fragment> дает вам FragmentInstance. Это объект, DOM-методы которого применяются ко всем дочерним элементам Fragment как к единой группе, не добавляя на страницу лишнего элемента.

Наверняка вам приходилось оборачивать группу соседних элементов в <div> только ради того, чтобы повесить на них ref. После этого CSS grid или flex-ряд ломался, потому что карточки переставали быть прямыми потомками контейнера. Refs для Fragment позволяют подключать к группам соседних элементов обработчики событий, наблюдатели (observers) и управление фокусом, не меняя структуру DOM. В этом руководстве нет полного обзора релиза: более ранние возможности описаны в статье что нового в React 19. Здесь разбирается, как работает API, до каких дочерних элементов дотягивается каждый метод и в каких случаях обертка по-прежнему остается лучшим выбором.

Ключевые выводы

  • Refs для Fragment стабильны в React 19.3. Сокращенная запись <></> не принимает ref, поэтому нужно импортировать Fragment и писать <Fragment ref={ref}>.
  • Ref содержит FragmentInstance: это не DOM-узел и не массив. Он предоставляет двенадцать методов, разделенных на четыре группы: события, фокус, наблюдатели, а также измерение и прокрутка.
  • Методы событий, наблюдателей и измерений работают только с DOM-потомками первого уровня. focus() и focusLast() ищут вложенные элементы обходом в глубину.
  • scrollIntoView() у FragmentInstance принимает только булев параметр alignToTop и выбрасывает исключение, если передать объект параметров.

В чем проблема div-обертки?

До React 19.3 единственный способ работать с группой соседних элементов заключался в том, чтобы обернуть их в настоящий DOM-элемент и повесить ref на него. Обертка меняет дерево компоновки, а такие системы раскладки, как CSS grid и flexbox, учитывают только прямых потомков.

// Before: the div is now the only grid item
<div className="grid">
  <div ref={groupRef}><Card /><Card /><Card /></div>
</div>

// After: each card is still a grid item
<div className="grid">
  <Fragment ref={groupRef}><Card /><Card /><Card /></Fragment>
</div>

В варианте с div-оберткой карточки становятся потомками обертки, а не grid-контейнера. Вариант с Fragment оставляет DOM ровно таким, каким он был бы без всякого ref.

Как работает ref для React Fragment?

Ref для React Fragment работает так же, как любой другой ref, с одним отличием: React присваивает ему значение типа FragmentInstance. Refs для Fragment стали стабильной возможностью в релизе React 19.3, опубликованном 9 сентября 2026 года. Вам понадобится React 19.3 или новее (react и react-dom версии 19.3.0+, включая патч 19.3.1), поэтому устанавливайте совпадающие версии 19.3.x обоих пакетов. Если вы переходите со старой мажорной версии, сначала ознакомьтесь со статьей о типичных ошибках при обновлении до React 19.

В справочнике по <Fragment> перечислены двенадцать методов:

Группа методовМетодыОхватПримечания
СобытияaddEventListener, removeEventListener, dispatchEventПотомки первого уровняdispatchEvent инициирует событие на Fragment, и оно может всплыть к его родительскому DOM-элементу
Фокусfocus, focusLast, blurВложенные элементы, обход в глубинуfocus() принимает FocusOptions
НаблюдателиobserveUsing, unobserveUsingПотомки первого уровняIntersectionObserver или ResizeObserver; текстовые узлы не поддерживаются
Измерение и прокруткаgetClientRects, getRootNode, compareDocumentPosition, scrollIntoViewПотомки первого уровняgetRootNode работает от родителя; scrollIntoView принимает только булево значение

Большинство сюрпризов при работе с этим API связаны с правилом первого уровня. Короткий пример с вложенностью наглядно показывает, до чего дотягивается каждая группа методов:

<Fragment ref={groupRef}>
  <div className="row">          {/* first-level: gets listeners, observed, measured */}
    <button>Save</button>        {/* nested: not observed; focus() can reach it */}
  </div>
  <Card />                       {/* component: the element Card renders at its top level is first-level */}
  Some text                      {/* text node: observeUsing does not work on it */}
</Fragment>

Компоненты не образуют отдельного уровня. Значение имеет DOM-элемент, который компонент рендерит на верхнем уровне, поэтому <Card /> считается потомком первого уровня, хотя это React-компонент.

Как добавить один обработчик событий ко всем дочерним элементам?

Вызов addEventListener у FragmentInstance регистрирует обработчик сразу на всех DOM-потомках первого уровня. Регистрируйте его в эффекте и удаляйте в функции очистки, оба раза передавая одну и ту же ссылку на функцию.

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

function ClickGroup({ onClick, children }) {
  const groupRef = useRef(null);

  useEffect(() => {
    const group = groupRef.current;
    if (!group) return;
    group.addEventListener('click', onClick);
    return () => group.removeEventListener('click', onClick);
  }, [onClick]);

  return <Fragment ref={groupRef}>{children}</Fragment>;
}

Согласно справочнику, React сам добавляет или удаляет обработчик, когда дочерние элементы появляются во Fragment или покидают его. Передавайте стабильный onClick (например, обернутый в useCallback): если на каждом рендере создается новая функция, эффект будет каждый раз отписываться и подписываться заново.

Вложенные элементы по-прежнему вызывают обработчик благодаря всплытию событий. Клик по <button> внутри <div> первого уровня всплывает до этого <div>, на котором и висит обработчик. События, которые не всплывают, например focus или mouseenter, не дойдут до обработчика от более глубоко вложенных элементов.

Как отслеживать группу с помощью observeUsing?

observeUsing() подключает уже созданный вами IntersectionObserver или ResizeObserver ко всем DOM-потомкам первого уровня. unobserveUsing() отключает от них тот же экземпляр наблюдателя.

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

function VisibilityGroup({ onVisibilityChange, children }) {
  const groupRef = useRef(null);

  useEffect(() => {
    const group = groupRef.current;
    if (!group) return;
    const visible = new Set();
    const observer = new IntersectionObserver((entries) => {
      for (const entry of entries) {
        if (entry.isIntersecting) visible.add(entry.target);
        else visible.delete(entry.target);
      }
      onVisibilityChange(visible.size > 0);
    });
    group.observeUsing(observer);
    return () => {
      group.unobserveUsing(observer);
      observer.disconnect();
    };
  }, [onVisibilityChange]);

  return <Fragment ref={groupRef}>{children}</Fragment>;
}

Set хранит дочерние элементы, которые в данный момент находятся в зоне пересечения, поэтому группа считается видимой, пока на экране есть хотя бы одна карточка. Дочерними элементами могут быть компоненты, которые вообще не принимают ref. Как отмечено в справочнике по Fragment, текстовые узлы не отслеживаются, а если Fragment содержит только текст, React выводит предупреждение в режиме разработки.

Перемещение фокуса с помощью focus()

Вызов focus() у ref для Fragment переводит фокус на первый фокусируемый элемент группы. Это работает, даже если поле ввода находится внутри <label> или <fieldset>, отрендеренного дочерним компонентом, который не предоставляет ref, поскольку методы фокуса ищут вложенные элементы обходом в глубину.

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

function EditAddress() {
  const [editing, setEditing] = useState(false);
  const fieldsRef = useRef(null);

  useEffect(() => {
    if (editing) fieldsRef.current?.focus();
  }, [editing]);

  return (
    <section>
      <button onClick={() => setEditing(true)}>Edit address</button>
      {editing && (
        <Fragment ref={fieldsRef}>
          <AddressFields />
        </Fragment>
      )}
    </section>
  );
}

Запись сессии (session replay) формы или модального окна может показать, что после изменения состояния фокус никуда не попадает и следующее нажатие клавиши уходит в <body>. Вызов focus() у ref для Fragment решает эту проблему без обертки. Чтобы перевести фокус на последний фокусируемый элемент, используйте focusLast(). При необходимости передайте FocusOptions, например { preventScroll: true }. Подойдет и ref-коллбэк, но сделайте его стабильным, чтобы он не перезапускался на каждом рендере.

Когда обертка все еще нужна?

Используйте элемент-обертку, когда вам нужен сам DOM-узел. Ref для Fragment работает с группой дочерних элементов, но не создает блок. Оставьте обертку, если:

  • Нужно стилизовать группу. Фон, рамки, отступы и grid-области требуют настоящего элемента.
  • Нужен единый ограничивающий прямоугольник. getClientRects() возвращает плоский массив прямоугольников дочерних элементов, а не один общий прямоугольник.
  • Целевые элементы вложены глубже первого уровня. Наблюдатели и невсплывающие события до таких элементов не доходят.

Помните также, что scrollIntoView() принимает только alignToTop. Если нужна плавная прокрутка или выравнивание через block, используйте метод элемента на настоящем узле.

Заключение

Refs для Fragment позволяют подключать обработчики событий, наблюдатели и управление фокусом к группам соседних элементов, не ломая grid- и flex-раскладки. Прежде чем их использовать, учтите правило первого уровня: события, наблюдатели и измерения ограничиваются прямыми DOM-потомками, тогда как фокус ищет глубже. Чтобы попробовать, обновитесь до React 19.3 и замените одну div-обертку, существующую только ради ref, на <Fragment ref>. В том же релизе появилась поддержка Trusted Types, о которой рассказывается в посте о релизе React 19.3.

Часто задаваемые вопросы

Как типизировать ref для Fragment в TypeScript?

Используйте useRef с типом FragmentInstance, экспортируемым из react, и начальным значением null. В @types/react 19.3 интерфейс FragmentInstance по умолчанию пуст. @types/react-dom расширяет его через declare module 'react', добавляя focus, observeUsing, addEventListener и остальные методы. Если эти методы отсутствуют, установите @types/react-dom и убедитесь, что модуль react-dom входит в граф типов вашего проекта.

К какому дочернему элементу прокручивает scrollIntoView у ref для Fragment?

Если alignToTop равен true или не указан, scrollIntoView() выравнивает первый дочерний элемент Fragment по верхнему краю прокручиваемого предка. Если alignToTop равен false, по нижнему краю выравнивается последний дочерний элемент. Для пустого Fragment используется ближайший соседний элемент или родитель. Передача объекта параметров, например со значением behavior: 'smooth', приводит к ошибке вместо прокрутки.

Работают ли обработчики событий ref для Fragment внутри скрытой границы Activity?

Нет. React не применяет обработчики, добавленные через addEventListener у FragmentInstance, к дочерним элементам внутри скрытой границы Activity. Когда граница переключается из скрытого состояния в видимое, React применяет эти обработчики автоматически. Вашему коду не нужно регистрировать их заново после того, как содержимое станет видимым.

Что делает blur() у ref для Fragment, если фокус находится вне группы?

Ничего. blur() у FragmentInstance снимает фокус с document.activeElement, только если этот элемент находится внутри Fragment. Если активный элемент расположен в любом другом месте страницы, вызов ничего не делает. Поэтому blur() можно вызывать в обработчике закрытия или отмены, не проверяя предварительно, где находится фокус.

DevTools for the frontend

Gain Debugging Superpowers

Unleash the power of session replay to reproduce bugs, track slowdowns and uncover frustrations in your app. Get complete visibility into your frontend with OpenReplay — the most advanced open-source session replay tool for developers.

Star on GitHub12k

We use cookies to improve your experience. By using our site, you accept cookies.