createPortal

createPortal дозволяє рендерити дочірні компоненти в інші частини DOM.

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

Опис

createPortal(children, domNode)

Щоб створити портал, викличте createPortal, передаючи JSX і DOM-вузол в якому він повинен відрендеритись:

import { createPortal } from 'react-dom';

// ...

<div>
<p>Цей дочірній елемент знаходиться в батьківському елементі.</p>
{createPortal(
<p>Цей дочірній елемент знаходиться безпосередньо в тілі документа.</p>,
document.body
)}
</div>

Перегляньте більше прикладів нижче.

Портал змінює тільки видиме розташування DOM-вузла. У будь-якому іншому плані, JSX, відрендерений в порталі, діє як дочірній елемент React-компонента, в якому рендериться портал. Приміром, дочірній елемент має доступ до контексту, наданого батьківським деревом елементів, а події передаються вгору від дочірнього елемента до батьківського, відповідно React-дереву компонентів.

Аргументи

  • children: Все, що може бути відрендерено за допомогою React, включаючи JSX (наприклад <div /> або <SomeComponent />), Фрагмент (<>...</>), рядок, число, або масив з них.

  • domNode: DOM-вузол, наприклад повернутий з document.getElementById(). Переданий вузол вже повинен існувати. Передавання різних DOM-вузлів під час оновлення спричинить повторне створення контенту всередині порталу.

Результат

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

Обмеження

  • Події з порталу передаються вгору відповідно до дерева React-компонентів, а не DOM дерева. Наприклад, якщо ви натиснете мишею всередині порталу, обгорнутого в <div onClick>, обробник події onClick спрацює. Якщо така поведінка створює ускладнення, зупиніть поширення події з порталу або ж перенесіть портал вище в дереві React-компонентів.

Використання

Рендер в іншу частину DOM

Портали дозволяють вашим компонентам рендерити дочірні елементи в інші частини DOM. Це дає можливість частині компонента “втікти” з будь-якого контейнера. Приміром, компонент може відображати модальне вікно або спливаючу підказку, що з’являється поза та над основною частиною сторінки.

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

import { createPortal } from 'react-dom';

function MyComponent() {
return (
<div style={{ border: '2px solid black' }}>
<p>Цей дочірній елемент знаходиться в батьківському елементі.</p>
{createPortal(
<p>Цей дочірній елемент знаходиться безпосередньо в тілі документа.</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>Цей дочірній елемент знаходиться в батьківському елементі.</p>
      {createPortal(
        <p>Цей дочірній елемент знаходиться безпосередньо в тілі документа.</p>,
        document.body
      )}
    </div>
  );
}

Зауважте, що другий параграф візуально знаходиться поза межами <div> з рамкою. Якщо ви перевірите структуру DOM за допомогою інструментів розробника, то побачите, що другий <p> розміщений безпосередньо в <body>:

<body>
<div id="root">
...
<div style="border: 2px solid black">
<p>Цей дочірній елемент знаходиться в батьківському елементі.</p>
</div>
...
</div>
<p>Цей дочірній елемент знаходиться безпосередньо в тілі документа.</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 Modal Authoring Practices коли створюєте модальні вікна. Якщо ви використовуєте пакунок для модальних вікон від спільноти, переконайтеся, що він доступний та відповідає цим рекомендаціям.


Рендер 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, і хочете рендерити 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>
  );
}