Перейти к содержанию

Fragment

<Fragment>, часто используемый через синтаксис <>...</>, позволяет группировать элементы без узла-обертки.

1
2
3
4
<>
    <OneChild />
    <AnotherChild />
</>

Описание

<Fragment>

Оберните элементы в <Fragment>, чтобы сгруппировать их вместе в ситуациях, когда вам нужен один элемент. Группировка элементов в Fragment не влияет на результирующий DOM; он такой же, как если бы элементы не были сгруппированы. Пустой JSX-тег <></> в большинстве случаев является сокращением для <Fragment></Fragment>.

Параметры

  • опционально key: Фрагменты, объявленные с явным синтаксисом <Fragment>, могут иметь ключи.

Предупреждения

  • Если вы хотите передать key фрагменту, вы не можете использовать синтаксис <>...</>. Вы должны явно импортировать Fragment из 'react' и передать <Fragment key={yourKey}>...</Fragment>.

  • React не сбрасывает состояние, когда вы переходите от рендеринга <><Child /></> к [<Child />] или обратно, или когда вы переходите от рендеринга <><Child /></> к <Child /> и обратно. Это работает только на одном уровне в глубину: например, переход от <><><Child /></></> к <Child /> сбрасывает состояние. Точную семантику можно посмотреть здесь.

FragmentInstance

Когда во фрагмент передают ref, React даёт объект FragmentInstance. У него есть методы для работы с DOM-потомками первого уровня, которые обёрнуты фрагментом.

  • addEventListener и removeEventListener управляют слушателями событий на всех DOM-потомках первого уровня.
  • dispatchEvent отправляет событие на фрагменте, и оно может всплыть к родительскому DOM-узлу.
  • focus, focusLast и blur управляют фокусом по всем вложенным потомкам в порядке обхода в глубину.
  • observeUsing и unobserveUsing подключают и отключают экземпляры IntersectionObserver или ResizeObserver.
  • getClientRects возвращает ограничивающие прямоугольники всех DOM-потомков первого уровня.
  • getRootNode возвращает корневой узел родителя фрагмента.
  • compareDocumentPosition сравнивает позицию фрагмента с другим узлом.
  • scrollIntoView прокручивает потомков фрагмента в область видимости.

addEventListener(type, listener, options?)

Добавляет слушатель события ко всем DOM-потомкам фрагмента первого уровня.

1
fragmentRef.current.addEventListener('click', handleClick);
Параметры
  • type: строка с типом события (например, 'click', 'focus').
  • listener: функция-обработчик события.
  • необязательно options: объект параметров или булево значение для захвата, как в DOM API addEventListener.
Возвращаемое значение

addEventListener ничего не возвращает (undefined).

removeEventListener(type, listener, options?)

Удаляет слушатель события со всех DOM-потомков фрагмента первого уровня.

1
fragmentRef.current.removeEventListener('click', handleClick);
Параметры
  • type: строка с типом события.
  • listener: функция-обработчик, которую нужно удалить.
  • необязательно options: объект параметров или булево значение, как в DOM API removeEventListener.
Возвращаемое значение

removeEventListener ничего не возвращает (undefined).

dispatchEvent(event)

Отправляет событие на фрагменте. Вызываются добавленные слушатели, и событие может всплыть к родительскому DOM-узлу фрагмента.

1
fragmentRef.current.dispatchEvent(new Event('custom', { bubbles: true }));
Параметры
  • event: объект Event, который нужно отправить. Если bubbles равен true, событие всплывает к родительскому DOM-узлу фрагмента.
Возвращаемое значение

true, если событие не отменено, и false, если вызван preventDefault().

focus(options?)

Переводит фокус на первый фокусируемый DOM-узел внутри фрагмента. В отличие от element.focus() на DOM-элементе, этот метод ищет всех вложенных потомков в глубину, пока не найдёт фокусируемый элемент, а не только сам элемент или его прямых потомков.

1
fragmentRef.current.focus();
Параметры
  • необязательно options: объект FocusOptions (например, { preventScroll: true }).
Возвращаемое значение

focus ничего не возвращает (undefined).

focusLast(options?)

Переводит фокус на последний фокусируемый DOM-узел внутри фрагмента. Ищет вложенных потомков в глубину, а затем идёт в обратном порядке.

1
fragmentRef.current.focusLast();
Параметры
  • необязательно options: объект FocusOptions.
Возвращаемое значение

focusLast ничего не возвращает (undefined).

blur()

Снимает фокус с активного элемента, если он находится внутри фрагмента. Если document.activeElement не внутри фрагмента, blur ничего не делает.

1
fragmentRef.current.blur();
Возвращаемое значение

blur ничего не возвращает (undefined).

observeUsing(observer)

Начинает наблюдение за всеми DOM-потомками фрагмента первого уровня переданным наблюдателем.

1
2
const observer = new IntersectionObserver(callback, options);
fragmentRef.current.observeUsing(observer);
Параметры
Возвращаемое значение

observeUsing ничего не возвращает (undefined).

unobserveUsing(observer)

Прекращает наблюдение за DOM-потомками фрагмента указанным наблюдателем.

1
fragmentRef.current.unobserveUsing(observer);
Параметры
  • observer: тот же экземпляр IntersectionObserver или ResizeObserver, который раньше передали в observeUsing.
Возвращаемое значение

unobserveUsing ничего не возвращает (undefined).

getClientRects()

Возвращает плоский массив объектов DOMRect с ограничивающими прямоугольниками всех DOM-потомков первого уровня.

1
const rects = fragmentRef.current.getClientRects();
Возвращаемое значение

Array<DOMRect> с ограничивающими прямоугольниками всех потомков.

getRootNode(options?)

Возвращает корневой узел, в котором находится родительский DOM-узел фрагмента, как Node.getRootNode().

1
const root = fragmentRef.current.getRootNode();
Параметры
  • необязательно options: объект с булевым свойством composed, как в DOM API getRootNode.
Возвращаемое значение

Document, ShadowRoot или сам FragmentInstance, если родительского DOM-узла нет.

compareDocumentPosition(otherNode)

Сравнивает позицию фрагмента в документе с другим узлом и возвращает битовую маску, как Node.compareDocumentPosition().

1
const position = fragmentRef.current.compareDocumentPosition(otherElement);
Параметры
  • otherNode: DOM-узел, с которым нужно сравнить.
Возвращаемое значение

Битовая маска флагов позиции. Пустые фрагменты и фрагменты, чьи потомки отрендерены через портал, включают в результат Node.DOCUMENT_POSITION_IMPLEMENTATION_SPECIFIC.

scrollIntoView(alignToTop?)

Прокручивает потомков фрагмента в область видимости. Если alignToTop равен true или опущен, прокрутка выравнивает первого потомка по верху прокручиваемого предка. Если alignToTop равен false, прокрутка выравнивает последнего потомка по низу.

1
fragmentRef.current.scrollIntoView();
Параметры
  • необязательно alignToTop: булево значение. Если true (по умолчанию), первый потомок прокручивается к верху прокручиваемой области. Если false, последний потомок прокручивается к низу. В отличие от Element.scrollIntoView(), этот метод не принимает объект ScrollIntoViewOptions.
Возвращаемое значение

scrollIntoView ничего не возвращает (undefined).

Предупреждения
  • scrollIntoView не принимает объект параметров. Если его передать, будет ошибка. Используйте булево значение alignToTop.
  • Если у фрагмента нет потомков, scrollIntoView в качестве фолбэка прокручивает в область видимости ближайшего соседа или родителя.

Предупреждения FragmentInstance

  • Методы, которые работают с потомками (addEventListener, observeUsing, getClientRects), действуют на хост-потомков (DOM) первого уровня фрагмента. Они не нацелены напрямую на потомков, вложенных в другой DOM-элемент.
  • focus и focusLast ищут фокусируемые элементы среди вложенных потомков в глубину, в отличие от методов событий и наблюдателей, которые затрагивают только хост-потомков первого уровня.
  • observeUsing не работает с текстовыми узлами. В разработке React пишет предупреждение, если фрагмент содержит только текстовых потомков.
  • React не вешает слушатели, добавленные через addEventListener, на скрытые деревья <Activity>. Когда граница Activity переключается со скрытой на видимую, слушатели применяются автоматически.
  • Каждый DOM-потомок первого уровня фрагмента с ref получает свойство reactFragments — Set<FragmentInstance> со всеми экземплярами фрагмента, которым принадлежит элемент. Это позволяет кэшировать общий наблюдатель для нескольких фрагментов.

Использование

Возвращение нескольких элементов

Используйте Fragment или эквивалентный синтаксис <>...</> для группировки нескольких элементов вместе. С его помощью вы можете поместить несколько элементов в любое место, где может находиться один элемент. Например, компонент может вернуть только один элемент, но с помощью фрагмента вы можете сгруппировать несколько элементов вместе и затем вернуть их как группу:

1
2
3
4
5
6
7
8
function Post() {
    return (
        <>
            <PostTitle />
            <PostBody />
        </>
    );
}

Фрагменты полезны тем, что группировка элементов с помощью фрагмента не влияет на макет или стили, в отличие от того, если бы вы обернули элементы в другой контейнер, например, DOM-элемент. Если вы посмотрите этот пример с помощью инструментов браузера, вы увидите, что все узлы DOM <h1> и <article> выглядят как родные братья и сестры без оберток вокруг них:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
export default function Blog() {
    return (
        <>
            <Post
                title="An update"
                body="It's been a while since I posted..."
            />
            <Post
                title="My new blog"
                body="I am starting a new blog!"
            />
        </>
    );
}

function Post({ title, body }) {
    return (
        <>
            <PostTitle title={title} />
            <PostBody body={body} />
        </>
    );
}

function PostTitle({ title }) {
    return <h1>{title}</h1>;
}

function PostBody({ body }) {
    return (
        <article>
            <p>{body}</p>
        </article>
    );
}

Как написать фрагмент без специального синтаксиса?

Приведенный выше пример эквивалентен импорту Fragment из React:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
import { Fragment } from 'react';

function Post() {
    return (
        <Fragment>
            <PostTitle />
            <PostBody />
        </Fragment>
    );
}

Обычно это не нужно, если только вам не нужно передать ключ вашему фрагменту.

Присвоение переменной нескольких элементов

Как и любой другой элемент, вы можете присваивать элементы Fragment переменным, передавать их в качестве пропсов и так далее:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
function CloseDialog() {
    const buttons = (
        <>
            <OKButton />
            <CancelButton />
        </>
    );
    return (
        <AlertDialog buttons={buttons}>
            Are you sure you want to leave this page?
        </AlertDialog>
    );
}

Группировка элементов с помощью текста

Вы можете использовать Fragment для группировки текста вместе с компонентами:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
function DateRangePicker({ start, end }) {
    return (
        <>
            From
            <DatePicker date={start} />
            to
            <DatePicker date={end} />
        </>
    );
}

Рендеринг списка фрагментов

Вот ситуация, когда вам нужно написать Fragment явно вместо использования синтаксиса <></>. Когда вы рендерите несколько элементов в цикле, вам нужно назначить key каждому элементу. Если элементы в цикле являются фрагментами, то для указания атрибута key необходимо использовать обычный синтаксис JSX-элементов:

1
2
3
4
5
6
7
8
function Blog() {
    return posts.map((post) => (
        <Fragment key={post.id}>
            <PostTitle title={post.title} />
            <PostBody body={post.body} />
        </Fragment>
    ));
}

Вы можете просмотреть DOM, чтобы убедиться, что вокруг дочерних элементов фрагмента нет элементов-оберток:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
import { Fragment } from 'react';

const posts = [
    {
        id: 1,
        title: 'An update',
        body: "It's been a while since I posted...",
    },
    {
        id: 2,
        title: 'My new blog',
        body: 'I am starting a new blog!',
    },
];

export default function Blog() {
    return posts.map((post) => (
        <Fragment key={post.id}>
            <PostTitle title={post.title} />
            <PostBody body={post.body} />
        </Fragment>
    ));
}

function PostTitle({ title }) {
    return <h1>{title}</h1>;
}

function PostBody({ body }) {
    return (
        <article>
            <p>{body}</p>
        </article>
    );
}

Слушатели событий без элемента-обёртки

Рефы фрагмента позволяют добавить слушатели событий к группе элементов, не добавляя DOM-узел-обёртку. Чтобы подключить и очистить слушатели, используйте ref-колбэк:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
import { Fragment, useState, useRef, useEffect } from 'react';

function ClickableFragment({ children, onClick }) {
    const fragmentRef = useRef(null);
    useEffect(() => {
        const fragmentInstance = fragmentRef.current;
        if (fragmentInstance === null) {
            return;
        }
        fragmentInstance.addEventListener('click', onClick);
        return () => {
            fragmentInstance.removeEventListener(
                'click',
                onClick
            );
        };
    }, [onClick])
    return (
        <Fragment ref={fragmentRef}>
            {children}
        </Fragment>
    );
}

export default function App() {
    const [clicks, setClicks] = useState(0);

    return (
        <>
            <p>Total clicks: {clicks}</p>
            <ClickableFragment onClick={() => {
                setClicks(c => c + 1);
            }}>
                <button>Button A</button>
                <button>Button B</button>
                <button>Button C</button>
            </ClickableFragment>
        </>
    );
}
1
2
3
4
5
6
7
{
  "dependencies": {
    "react": "19.3.0-canary-f1f7ed2a-20260904",
    "react-dom": "19.3.0-canary-f1f7ed2a-20260904",
    "react-scripts": "latest"
  }
}

Вызов addEventListener вешает слушатель на каждого DOM-потомка фрагмента первого уровня. Когда потомков динамически добавляют или удаляют, FragmentInstance сам добавляет или снимает слушатель.

Каких потомков затрагивает реф фрагмента?

FragmentInstance нацелен на хост-потомков (DOM) первого уровня фрагмента. Рассмотрим такое дерево:

1
2
3
4
5
6
7
8
9
<Fragment ref={ref}>
  <div id="A" />
  <Wrapper>
    <div id="B">
      <div id="C" />
    </div>
  </Wrapper>
  <div id="D" />
</Fragment>

Wrapper — это компонент React, поэтому FragmentInstance смотрит сквозь него в поисках DOM-узлов. Целевые потомки — A, B и D. C не входит в них, потому что вложен в DOM-элемент B.

Методы вроде addEventListener, observeUsing и getClientRects работают с этими DOM-потомками первого уровня. focus и focusLast устроены иначе: они ищут всех вложенных потомков в глубину, чтобы найти фокусируемые элементы.

Фокус на группе элементов

Рефы фрагмента дают методы focus, focusLast и blur, которые работают со всеми DOM-узлами внутри фрагмента:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
import { Fragment, useRef } from 'react';

function FormFields({ children }) {
    const fragmentRef = useRef(null);

    return (
        <>
            <div className="buttons">
                <button onClick={() => {
                    fragmentRef.current.focus();
                }}>
                    Focus first
                </button>
                <button onClick={() => {
                    fragmentRef.current.focusLast();
                }}>
                    Focus last
                </button>
                <button onClick={() => {
                    fragmentRef.current.blur();
                }}>
                    Blur
                </button>
            </div>
            <Fragment ref={fragmentRef}>
                {children}
            </Fragment>
        </>
    );
}

// Even though the inputs are deeply nested,
// focus() searches depth-first to find them.
export default function App() {
    return (
        <FormFields>
            <fieldset>
                <legend>Shipping</legend>
                <label>
                    Street: <input name="street" />
                </label>
                <label>
                    City: <input name="city" />
                </label>
            </fieldset>
        </FormFields>
    );
}
1
2
3
4
5
6
7
8
9
.buttons {
  display: flex;
  gap: 8px;
  margin-bottom: 10px;
}

label {
  display: inline-block;
}
1
2
3
4
5
6
7
{
  "dependencies": {
    "react": "19.3.0-canary-f1f7ed2a-20260904",
    "react-dom": "19.3.0-canary-f1f7ed2a-20260904",
    "react-scripts": "latest"
  }
}

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

Прокрутка группы элементов в область видимости

scrollIntoView прокручивает потомков фрагмента в область видимости без элемента-обёртки. Передайте true (или опустите аргумент), чтобы прокрутить первого потомка к верху. Передайте false, чтобы прокрутить последнего потомка к низу:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
import { Fragment, useRef } from 'react';

function ScrollableSection({ children }) {
    const fragmentRef = useRef(null);

    return (
        <>
            <div className="buttons">
                <button onClick={() => {
                    fragmentRef.current.scrollIntoView();
                }}>
                    Scroll to top
                </button>
                <button onClick={() => {
                    fragmentRef.current.scrollIntoView(false);
                }}>
                    Scroll to bottom
                </button>
            </div>
            <div className="container">
                <Fragment ref={fragmentRef}>
                    {children}
                </Fragment>
            </div>
        </>
    );
}

const items = [];
for (let i = 1; i <= 25; i++) {
    items.push('Item ' + i);
}

export default function App() {
    return (
        <ScrollableSection>
            <h3>Section Start</h3>
            {items.map((item) => (
                <p key={item}>{item}</p>
            ))}
            <h3>Section End</h3>
        </ScrollableSection>
    );
}
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
.buttons {
  display: flex;
  gap: 8px;
  margin-bottom: 10px;
}

.container {
  height: 200px;
  overflow-y: auto;
  border: 2px solid #c4c4c4;
  border-radius: 4px;
  padding: 10px;
}

h3 {
  margin: 4px 0;
  /* Padding to handle offset of global sticky nav when scrolling for example */
  padding-top: 4em;
  color: #1a73e8;
}

p {
  margin: 4px 0;
}
1
2
3
4
5
6
7
{
  "dependencies": {
    "react": "19.3.0-canary-f1f7ed2a-20260904",
    "react-dom": "19.3.0-canary-f1f7ed2a-20260904",
    "react-scripts": "latest"
  }
}

Наблюдение за видимостью без элемента-обёртки

observeUsing подключает IntersectionObserver ко всем DOM-потомкам фрагмента первого уровня. Так можно следить за видимостью, не заставляя дочерние компоненты отдавать ref и не добавляя элемент-обёртку:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
import {
    Fragment,
    useRef,
    useLayoutEffect,
    useState,
} from 'react';
import Card from './Card';

function VisibleGroup({ onVisibilityChange, children }) {
    const fragmentRef = useRef(null);

    useLayoutEffect(() => {
        const visibleElements = new Set();
        const observer = new IntersectionObserver(
            (entries) => {
                entries.forEach(e => {
                    if (e.isIntersecting) {
                        visibleElements.add(e.target);
                    } else {
                        visibleElements.delete(e.target);
                    }
                });
                onVisibilityChange(visibleElements.size > 0);
            }
        );
        const fragmentInstance = fragmentRef.current;
        fragmentInstance.observeUsing(observer);
        return () => {
            fragmentInstance.unobserveUsing(observer);
        };
    }, [onVisibilityChange]);

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

export default function App() {
    const [isVisible, setIsVisible] = useState(true);

    return (
        <div className={isVisible ? 'page visible' : 'page'}>
            <div className="filler">Scroll down</div>
            <VisibleGroup onVisibilityChange={setIsVisible}>
                <Card title="First section" />
                <Card title="Second section" />
            </VisibleGroup>
            <div className="filler">Scroll up</div>
        </div>
    );
}
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
.page {
  transition: background 0.3s;
}

.page.visible {
  background: #d4edda;
}

.filler {
  height: 500px;
  display: flex;
  align-items: center;
  justify-content: center;
  color: #aaa;
  font-size: 14px;
}

.card {
  padding: 16px;
  background: white;
  border: 1px solid #ddd;
  border-radius: 8px;
  margin: 8px 16px;
  box-shadow: 0 1px 3px rgba(0,0,0,0.08);
  font-weight: 600;
  font-size: 14px;
}
1
2
3
export default function Card({ title }) {
    return <div className="card">{title}</div>;
}
1
2
3
4
5
6
7
{
  "dependencies": {
    "react": "19.3.0-canary-f1f7ed2a-20260904",
    "react-dom": "19.3.0-canary-f1f7ed2a-20260904",
    "react-scripts": "latest"
  }
}

Кэширование общего IntersectionObserver

Частая оптимизация для страниц со множеством наблюдателей — один IntersectionObserver на конфигурацию и маршрутизация его записей в нужные колбэки по тому, какой элемент пересёкся. Рефы фрагмента поддерживают тот же приём через свойство reactFragments.

У каждого DOM-потомка первого уровня фрагмента с ref есть свойство reactFragments: Set объектов FragmentInstance, которые содержат этот элемент. Когда срабатывает общий наблюдатель, по этому свойству можно найти, какому FragmentInstance принадлежит пересекающийся элемент, и вызвать нужные колбэки.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
import { useState, useCallback } from 'react';
import ObservedGroup from './ObservedGroup';
import Card from './Card';

export default function App() {
    const [bgColor, setBgColor] = useState(null);

    const onGreen = useCallback((entry) => {
        if (entry.isIntersecting) {
            setBgColor('#d4edda');
        }
    }, []);

    const onBlue = useCallback((entry) => {
        if (entry.isIntersecting) {
            setBgColor('#cce5ff');
        }
    }, []);

    return (
        <div className="page" style={{
            background: bgColor || 'white',
        }}>
            <div className="filler">Scroll down</div>
            <ObservedGroup onIntersection={onGreen}>
                <Card title="Green section" className="green" />
            </ObservedGroup>
            <div className="filler" />
            <ObservedGroup onIntersection={onBlue}>
                <Card title="Blue section" className="blue" />
            </ObservedGroup>
            <div className="filler">Scroll up</div>
        </div>
    );
}
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
import {
    Fragment,
    useRef,
    useLayoutEffect,
} from 'react';

const callbackMap = new WeakMap();
const observerCache = new Map();

function getOptionsKey(options) {
    const root = options?.root ?? null;
    const rootMargin = options?.rootMargin ?? '0px';
    const threshold = options?.threshold ?? 0;
    return `${rootMargin}|${threshold}`;
}

function getSharedObserver(
    fragmentInstance,
    onIntersection,
    options,
) {
    // Register this callback for the
    // fragment instance.
    const existing =
        callbackMap.get(fragmentInstance);
    callbackMap.set(
        fragmentInstance,
        existing
            ? [...existing, onIntersection]
            : [onIntersection],
    );

    const key = getOptionsKey(options);
    if (observerCache.has(key)) {
        return observerCache.get(key);
    }

    const observer = new IntersectionObserver(
        (entries) => {
            for (const entry of entries) {
                // Look up which FragmentInstances own
                // this element.
                const fragmentInstances =
                    entry.target.reactFragments;
                if (fragmentInstances) {
                    for (const inst of fragmentInstances) {
                        const callbacks =
                            callbackMap.get(inst) || [];
                        callbacks.forEach(cb => cb(entry));
                    }
                }
            }
        },
        options,
    );

    observerCache.set(key, observer);
    return observer;
}

export default function ObservedGroup({
    onIntersection,
    options,
    children,
}) {
    const fragmentRef = useRef(null);

    useLayoutEffect(() => {
        const fragmentInstance = fragmentRef.current;
        const observer = getSharedObserver(
            fragmentInstance,
            onIntersection,
            options,
        );
        fragmentInstance.observeUsing(observer);
        return () => {
            fragmentInstance.unobserveUsing(observer);
            callbackMap.delete(fragmentInstance);
        };
    }, [onIntersection, options]);

    return (
        <Fragment ref={fragmentRef}>
            {children}
        </Fragment>
    );
}
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
.page {
  transition: background 0.3s;
}

.filler {
  height: 500px;
  display: flex;
  align-items: center;
  justify-content: center;
  color: #aaa;
  font-size: 14px;
}

.card {
  padding: 16px;
  background: white;
  border: 1px solid #ddd;
  border-radius: 8px;
  margin: 0 16px;
  box-shadow: 0 1px 3px rgba(0,0,0,0.08);
  font-weight: 600;
  font-size: 14px;
}

.card.green {
  border-left: 3px solid #28a745;
}

.card.blue {
  border-left: 3px solid #007bff;
}
1
2
3
export default function Card({ title, className }) {
    return <div className={'card' + (className ? ' ' + className : '')}>{title}</div>;
}
1
2
3
4
5
6
7
{
  "dependencies": {
    "react": "19.3.0-canary-f1f7ed2a-20260904",
    "react-dom": "19.3.0-canary-f1f7ed2a-20260904",
    "react-scripts": "latest"
  }
}

Несколько компонентов ObservedGroup с одинаковыми параметрами переиспользуют один IntersectionObserver. Когда любая из секций прокручивается в область видимости, общий наблюдатель срабатывает и через reactFragments направляет запись в нужный колбэк.

Источник — https://react.dev/reference/react/Fragment

Комментарии