createPortal

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

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

Довідник

createPortal(children, domNode, key?)

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

import { createPortal } from 'react-dom';

// ...

<div>
  <p>This child is placed in the parent div.</p>
  {createPortal(
    <p>This child is placed in the document body.</p>,
    document.body
  )}
</div>

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

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

Параметри

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

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

  • опція ключ: Унікальний рядок або число, яке буде використано як ключ порталу.

Повернення

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

Застереження

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

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

Відображення в іншу частину DOM

Портали дозволяють вашим компонентам рендерити деякі свої дочірні елементи в інше місце DOM. Це дозволяє частині вашого компонента "втекти" з будь-якого контейнера, в якому він може знаходитися. Наприклад, компонент може відображати модальне діалогове вікно або підказку, яка з'являється над рештою сторінки та поза нею.

Щоб створити портал, відрендеріть результат createPortal за допомогою деякого JSX</CodeStep> та <CodeStep data-step="2">DOM-вузол, куди він має потрапити</CodeStep> :</p> <pre><code data-meta="[[1, 8, "<p>Цей дочірній елемент розміщується в тілі документа. </p>"], [2, 9, "document.body"]]" class="language-js">import { createPortal } from 'react-dom'; функція MyComponent() { return ( <div style={{ border: '2px solid black' }}> <p>Цей дочірній елемент розміщується у батьківському div.</p> {createPortal( <p>Цей дочірній елемент розміщується в тілі документа.</p>, document.body )} </div> ); }

React створить вузли DOM для JSX, який ви передали</CodeStep> всередині <CodeStep data-step="2">DOM-вузла, який ви надали</CodeStep> .</p> <p>Без порталу другий <код><p> був би розміщений всередині батьківського <div>, але портал "телепортував" його в document.body:

import { createPortal } from 'react-dom';

export default function MyComponent() {
  return (
    <div style={{ border: '2px solid black' }}>
      <p>This child is placed in the parent div.</p>
      {createPortal(
        <p>This child is placed in the document body.</p>,
        document.body
      )}
    </div>
  );
}

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

<body>
  <div id="root">
    ...
      <div style="border: 2px solid black">
        <p>This child is placed inside the parent div.</p>
      </div>
    ...
  </div>
  <p>This child is placed in the 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>
    </>
  );
}
import { useState } from 'react';
import ModalContent from './ModalContent.js';

export default function NoPortalExample() {
  const [showModal, setShowModal] = useState(false);
  return (
    <>
      <button onClick={() => setShowModal(true)}>
        Show modal without a portal
      </button>
      {showModal && (
        <ModalContent onClose={() => setShowModal(false)} />
      )}
    </>
  );
}
import { useState } from 'react';
import { createPortal } from 'react-dom';
import ModalContent from './ModalContent.js';

export default function PortalExample() {
  const [showModal, setShowModal] = useState(false);
  return (
    <>
      <button onClick={() => setShowModal(true)}>
        Show modal using a portal
      </button>
      {showModal && createPortal(
        <ModalContent onClose={() => setShowModal(false)} />,
        document.body
      )}
    </>
  );
}
export default function ModalContent({ onClose }) {
  return (
    <div className="modal">
      <div>I'm a modal dialog</div>
      <button onClick={onClose}>Close</button>
    </div>
  );
}
.clipping-container {
  position: relative;
  border: 1px solid #aaa;
  margin-bottom: 12px;
  padding: 12px;
  width: 250px;
  height: 80px;
  overflow: hidden;
}

.modal {
  display: flex;
  justify-content: space-evenly;
  align-items: center;
  box-shadow: rgba(100, 100, 111, 0.3) 0px 7px 29px 0px;
  background-color: white;
  border: 2px solid rgb(240, 240, 240);
  border-radius: 12px;
  position:  absolute;
  width: 250px;
  top: 70px;
  left: calc(50% - 125px);
  bottom: 70px;
}

При використанні порталів важливо переконатися, що ваша програма доступна. Наприклад, вам може знадобитися керувати фокусом клавіатури так, щоб користувач міг переміщати фокус у портал і з порталу у природний спосіб.

Дотримуйтесь Практики створення модальних елементів керування WAI-ARIA під час створення модальних елементів керування. Якщо ви використовуєте пакунок спільноти, переконайтеся, що він доступний і відповідає цим настановам.


Відображення React-компонентів у розмітці сервера, що не є React

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

<!DOCTYPE html>
<html>
  <head><title>My app</title></head>
  <body>
    <h1>Welcome to my hybrid app</h1>
    <div class="parent">
      <div class="sidebar">
        This is server non-React markup
        <div id="sidebar-content"></div>
      </div>
      <div id="root"></div>
    </div>
  </body>
</html>
import { StrictMode } from 'react';
import { createRoot } from 'react-dom/client';
import App from './App.js';
import './styles.css';

const root = createRoot(document.getElementById('root'));
root.render(
  <StrictMode>
    <App />
  </StrictMode>
);
import { createPortal } from 'react-dom';

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

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

function MainContent() {
  return <p>This part is rendered by React</p>;
}

function SidebarContent() {
  return <p>This part is also rendered by React!</p>;
}
.parent {
  display: flex;
  flex-direction: row;
}

#root {
  margin-top: 12px;
}

.sidebar {
  padding:  12px;
  background-color: #eee;
  width: 200px;
  height: 200px;
  margin-right: 12px;
}

#sidebar-content {
  margin-top: 18px;
  display: block;
  background-color: white;
}

p {
  margin: 0;
}

Відображення React-компонентів у нереактівські DOM-вузли

Ви також можете використовувати портал для керування вмістом 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>Hello from React!</p>,
      popupContainer
    )}
  </div>
);

Ось повний приклад, з яким ви можете погратися:

{
  "dependencies": {
    "leaflet": "1.9.1",
    "react": "latest",
    "react-dom": "latest",
    "react-scripts": "latest",
    "remarkable": "2.0.1"
  },
  "scripts": {
    "start": "react-scripts start",
    "build": "react-scripts build",
    "test": "react-scripts test --env=jsdom",
    "eject": "react-scripts eject"
  }
}
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>Hello from React!</p>,
        popupContainer
      )}
    </div>
  );
}
import 'leaflet/dist/leaflet.css';
import * as L from 'leaflet';

export function createMapWidget(containerDomNode) {
  const map = L.map(containerDomNode);
  map.setView([0, 0], 0);
  L.tileLayer('https://tile.openstreetmap.org/{z}/{x}/{y}.png', {
    maxZoom: 19,
    attribution: '© OpenStreetMap'
  }).addTo(map);
  return map;
}

export function addPopupToMapWidget(map) {
  const popupDiv = document.createElement('div');
  L.popup()
    .setLatLng([0, 0])
    .setContent(popupDiv)
    .openOn(map);
  return popupDiv;
}
button { margin: 5px; }