| title | createPortal |
|---|
createPortal дозволяє рендерити дочірні компоненти в інші частини DOM.
<div>
<SomeComponent />
{createPortal(children, domNode, key?)}
</div>Щоб створити портал, викличте createPortal, передаючи JSX і DOM-вузол в якому він повинен відрендеритись:
import { createPortal } from 'react-dom';
// ...
<div>
<p>Цей дочірній елемент знаходиться в батьківському div.</p>
{createPortal(
<p>Цей дочірній елемент знаходиться безпосередньо в тілі документа.</p>,
document.body
)}
</div>Перегляньте більше прикладів нижче.
Портал лише змінює фізичне розташування DOM-вузла. У всіх інших випадках, JSX, який ви рендерите в портал, поводиться як дочірній елемент React-компонента, який його рендерить. Для прикладу, цей дочірній елемент має доступ до контексту, наданого батьківським деревом елементів, а події передаються вгору від дочірніх елементів до батьківських, відповідно до React-дереву компонентів.
-
children: Все, що може бути відрендерено за допомогою React, включаючи JSX (наприклад<div />або<SomeComponent />), Фрагмент (<>...</>), рядок, число, або масив з них. -
domNode: DOM-вузол, наприклад повернутий зdocument.getElementById(). Переданий вузол вже повинен існувати. Передавання різних DOM-вузлів під час оновлення спричинить повторне створення контенту всередині порталу. -
опційний
key: Унікальна стрічкова або числова змінна, що використовується як ключ порталу.
createPortal повертає React-вузол, який може бути включеним в JSX або ж повернутим з React-компонента. Якщо React зіткнеться з таким у виводі рендеру, він помістить надані children всередину переданого domNode.
- Події з порталів поширюються згідно з деревом React-компонентів, а не DOM. Наприклад, якщо ви клікнете в межах порталу, обгорнутого в
<div onClick>, то цей обробник подіїonClickспрацює. Якщо така поведінка створює проблеми, зупиніть поширення події з порталу або ж перенесіть портал вище в дереві React-компонентів.
Портали дозволяють вашим компонентам рендерити деякі їхні дочірні елементи в інші частини DOM. Це дозволяє частині вашого компонента "втекти" з будь-яких контейнерів, в яких вона перебуває. Приміром, компонент може відображати модальне вікно або спливаючу підказку, що з'являється поза та над основною частиною сторінки.
Щоб створити портал, відрендеріть результат createPortal з JSX і DOM-вузлом, куди потрібно помістити JSX:
import { createPortal } from 'react-dom';
function MyComponent() {
return (
<div style={{ border: '2px solid black' }}>
<p>Цей дочірній елемент знаходиться в батьківському div.</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>Цей дочірній елемент знаходиться в батьківському div.</p>
{createPortal(
<p>Цей дочірній елемент знаходиться безпосередньо в тілі документа.</p>,
document.body
)}
</div>
);
}Зверніть увагу, як другий параграф візуально знаходиться поза межами <div> з рамкою. Якщо ви перевірите структуру DOM за допомогою інструментів розробника, то побачите, що другий <p> розміщений безпосередньо в <body>:
<body>
<div id="root">
...
<div style="border: 2px solid black">
<p>Цей дочірній елемент знаходиться в батьківському div.</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>
</>
);
}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, і хочете рендерити 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; }