createPortal

createPortal позволяет отрендерить дочерние элементы в другую часть DOM-дерева.

<div>
<SomeComponent />
{createPortal(children, domNode, key?)}
</div>

Справочник

createPortal(children, domNode, key?)

Чтобы создать портал, вызовите createPortal, передав JSX и DOM-узел, в который нужно отрендерить содержимое:

import { createPortal } from 'react-dom';

// ...

<div>
<p>Этот элемент будет отрендерен внутри родительского div.</p>
{createPortal(
<p>А этот — прямо в document.body.</p>,
document.body
)}
</div>

См. больше примеров ниже.

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

Параметры

  • children: Всё, что может быть отрендерено в React — JSX (например, <div /> или <SomeComponent />), Фрагмент (<>...</>), строка, число или массив этих элементов.

  • domNode: DOM-элемент (например, возвращённый document.getElementById()). Он должен существовать к моменту рендера. Если при обновлении передать другой DOM-узел — содержимое портала будет пересоздано.

  • необязательный key: Уникальная строка или число, используемые как key для портала.

Возвращаемое значение

createPortal возвращает React-узел, который можно включить в JSX или вернуть из компонента. При рендере React поместит переданный children внутрь указанного domNode.

Предостережения

  • События от порталов всплывают по дереву React-компонентов, а не по DOM. Например, если вы кликнете внутри портала, и он обёрнут в <div onClick>, этот обработчик сработает. Если это вызывает проблемы, остановите всплытие события внутри портала или поднимите портал выше в дереве компонентов.

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

Рендеринг в другую часть DOM

Порталы позволяют компонентам рендерить часть своих потомков в другое место DOM-дерева. Это даёт возможность “вырваться” из любых ограничивающих контейнеров. Например, компонент может отобразить модальное окно или всплывающую подсказку, расположенные поверх остального содержимого страницы.

Чтобы создать портал, отрендерите результат createPortal, передав JSX и DOM-узел, куда его вставить:

import { createPortal } from 'react-dom';

function MyComponent() {
return (
<div style={{ border: '2px solid black' }}>
<p>Этот элемент внутри родительского div.</p>
{createPortal(
<p>А этот — в document.body.</p>,
document.body
)}
</div>
);
}

React вставит DOM-узлы для переданного JSX внутрь указанного DOM-узла.

Без портала второй <p> оказался бы внутри родительского <div>, но портал “телепортировал” его в document.body:

import { createPortal } from 'react-dom';

export default function MyComponent() {
  return (
    <div style={{ border: '2px solid black' }}>
      <p>Этот элемент внутри родительского div.</p>
      {createPortal(
        <p>А этот — в document.body.</p>,
        document.body
      )}
    </div>
  );
}

Обратите внимание: второй абзац визуально размещён вне родительского <div> с рамкой. Если открыть DOM в инструментах разработчика, вы увидите, что второй <p> действительно размещён прямо в <body>:

<body>
<div id="root">
...
<div style="border: 2px solid black">
<p>Этот элемент внутри родительского div.</p>
</div>
...
</div>
<p>А этот — в document.body.</p>
</body>

Портал изменяет только физическое размещение DOM-узла. Во всех остальных отношениях JSX, отрендеренный через портал, остаётся потомком React-компонента, который его рендерит. Например, он может получать контекст от родительского дерева, а события продолжают всплывать вверх по React-иерархии.


Рендеринг модального окна с помощью портала

Вы можете использовать портал для отображения модального окна, которое всплывает поверх остальной части страницы — даже если компонент, открывающий это окно, находится внутри контейнера с overflow: hidden или другими стилями, мешающими отображению.

В этом примере оба контейнера имеют стили, которые могут “обрезать” модальное окно, но тот, что отрендерен через портал, работает корректно — потому что в DOM он не находится внутри родительских JSX-элементов.

import NoPortalExample from './NoPortalExample';
import PortalExample from './PortalExample';

export default function App() {
  return (
    <>
      <div className="clipping-container">
        <NoPortalExample />
      </div>
      <div className="clipping-container">
        <PortalExample />
      </div>
    </>
  );
}

Pitfall

Важно убедиться, что при использовании порталов ваше приложение остаётся доступным. Например, вам может потребоваться управлять фокусом клавиатуры, чтобы пользователь мог перемещать фокус внутрь и наружу из портала естественным способом.

Следуйте рекомендациям WAI-ARIA по созданию модальных окон. Если вы используете готовую библиотеку, убедитесь, что она поддерживает доступность и следует этим рекомендациям.


Рендеринг компонентов React в разметку, сгенерированную вне React

Порталы полезны, когда корень React занимает только часть страницы, собранной статически или на сервере без использования React. Например, если ваша страница построена с помощью серверного фреймворка (например, Rails), вы можете добавить интерактивные элементы в статические области — например, в боковую панель. В отличие от нескольких отдельных корней React, порталы позволяют работать с приложением как с единой React-структурой с общим состоянием — даже если части рендерятся в разные участки DOM.

import { createPortal } from 'react-dom';

const sidebarContentEl = document.getElementById('sidebar-content');

export default function App() {
  return (
    <>
      <MainContent />
      {createPortal(
        <SidebarContent />,
        sidebarContentEl
      )}
    </>
  );
}

function MainContent() {
  return <p>Эта часть отрендерена с помощью React</p>;
}

function SidebarContent() {
  return <p>И эта часть — тоже отрендерена React!</p>;
}


Рендеринг компонентов React в DOM-узлы, созданные вне React

Вы также можете использовать портал для управления содержимым DOM-элемента, который создаётся и управляется сторонним кодом вне React. Например, если вы интегрируете сторонний виджет карты и хотите отрендерить React-контент внутри всплывающего окна. Сначала объявите состояние popupContainer, в котором вы будете хранить DOM-узел для рендера:

const [popupContainer, setPopupContainer] = useState(null);

Когда вы создаёте виджет, сохраните возвращённый элемент DOM, чтобы потом использовать его как контейнер:

useEffect(() => {
if (mapRef.current === null) {
const map = createMapWidget(containerRef.current);
mapRef.current = map;
const popupDiv = addPopupToMapWidget(map);
setPopupContainer(popupDiv);
}
}, []);

Теперь вы можете использовать createPortal, чтобы отрендерить React-контент в popupContainer, как только он будет доступен:

return (
<div style={{ width: 250, height: 250 }} ref={containerRef}>
{popupContainer !== null && createPortal(
<p>Привет из React!</p>,
popupContainer
)}
</div>
);

Ниже приведён полный пример, с которым можно поэкспериментировать:

import { useRef, useEffect, useState } from 'react';
import { createPortal } from 'react-dom';
import { createMapWidget, addPopupToMapWidget } from './map-widget.js';

export default function Map() {
  const containerRef = useRef(null);
  const mapRef = useRef(null);
  const [popupContainer, setPopupContainer] = useState(null);

  useEffect(() => {
    if (mapRef.current === null) {
      const map = createMapWidget(containerRef.current);
      mapRef.current = map;
      const popupDiv = addPopupToMapWidget(map);
      setPopupContainer(popupDiv);
    }
  }, []);

  return (
    <div style={{ width: 250, height: 250 }} ref={containerRef}>
      {popupContainer !== null && createPortal(
        <p>Привет из React!</p>,
        popupContainer
      )}
    </div>
  );
}