Skip to content

Latest commit

 

History

History
460 lines (363 loc) · 16.5 KB

File metadata and controls

460 lines (363 loc) · 16.5 KB
title createPortal

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

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

Справочник {/reference/}

createPortal(children, domNode, key?) {/createportal/}

Чтобы создать портал, вызовите 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-структуре).

Параметры {/parameters/}

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

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

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

Возвращаемое значение {/returns/}

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

Предостережения {/caveats/}

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

Использование {/usage/}

Рендеринг в другую часть DOM {/rendering-to-a-different-part-of-the-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-иерархии.


Рендеринг модального окна с помощью портала {/rendering-a-modal-dialog-with-a-portal/}

Вы можете использовать портал для отображения модального окна, которое всплывает поверх остальной части страницы — даже если компонент, открывающий это окно, находится внутри контейнера с 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)}>
        Показать модалку без портала
      </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)}>
        Показать модалку с использованием портала
      </button>
      {showModal && createPortal(
        <ModalContent onClose={() => setShowModal(false)} />,
        document.body
      )}
    </>
  );
}
export default function ModalContent({ onClose }) {
  return (
    <div className="modal">
      <div>Я — модальное окно</div>
      <button onClick={onClose}>Закрыть</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 {/rendering-react-components-into-non-react-server-markup/}

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

<!DOCTYPE html>
<html>
  <head><title>Моё приложение</title></head>
  <body>
    <h1>Добро пожаловать в гибридное приложение</h1>
    <div class="parent">
      <div class="sidebar">
          Это серверная (не-React) разметка
        <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>Эта часть отрендерена с помощью React</p>;
}

function SidebarContent() {
  return <p>И эта часть — тоже отрендерена 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-узлы, созданные вне React {/rendering-react-components-into-non-react-dom-nodes/}

Вы также можете использовать портал для управления содержимым 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>
);

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

{
  "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>Привет из 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; }