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> </> ); }
Рендеринг компонентов 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> ); }