Порталы в React — это особый механизм, который позволяет размещать HTML-код вне иерархии DOM родительского компонента. Это полезно, когда нужно вывести элементы, такие как модальные окна, всплывающие подсказки или диалоги, за пределы контейнера, в котором находится компонент.
Что такое React-порталы?
Портал — это метод React, предоставляемый пакетом react-dom. Основная идея портала заключается в том, что он позволяет рендерить элементы HTML за пределами родительского компонента, в другой области DOM дерева.
Без использования порталов возвращаемое содержимое HTML является непосредственным потомком родительского компонента:
Пример
Обычный рендеринг без порталов:
function myChild() {
return (
<div>
Привет
</div>
);
}
При использовании метода createPortal HTML-элемент оказывается вне родительского компонента и рендерится отдельно от него:
Пример
Рендеринг с использованием порталов:
import { createPortal } from 'react-dom';
function myChild() {
return createPortal(
<div>
Привет
</div>,
document.body
);
}
Синтаксис
import { createPortal } from 'react-dom';
createPortal(children, domNode)
Где:
- Первый аргумент (
children) — любое рендеримое содержимое React, например, элементы, строки или фрагменты. - Второй аргумент (
domNode) — DOM-узел, куда нужно вставить портал.
Создание модального окна с использованием порталов
Порталы особенно полезны для компонентов, которые должны находиться вне основного потока документа, например, модальных окон или всплывающих подсказок.
Посмотрим на простой пример модального окна, который рендерится за пределами родительского компонента:
Пример
import { createRoot } from 'react-dom/client';
import { useState } from 'react';
import { createPortal } from 'react-dom';
function Modal({ isOpen, onClose, children }) {
if (!isOpen) return null;
return createPortal(
<div style={{
position: 'fixed',
top: 0,
left: 0,
right: 0,
bottom: 0,
backgroundColor: 'rgba(0, 0, 0, 0.5)', // полупрозрачная подложка
display: 'flex',
alignItems: 'center',
justifyContent: 'center'
}}>
<div style={{
background: 'white',
padding: '20px',
borderRadius: '8px'
}}>
{children}
<button onClick={onClose}>Закрыть</button>
</div>
</div>,
document.body
);
}
function MyApp() {
const [isOpen, setIsOpen] = useState(false);
return (
<div>
<h1>Мой сайт</h1>
<button onClick={() => setIsOpen(true)}>
Открыть модальное окно
</button>
<Modal isOpen={isOpen} onClose={() => setIsOpen(false)}>
<h2>Содержание модального окна</h2>
<p>Этот контент рендерится вне компонента App!</p>
</Modal>
</div>
);
}
createRoot(document.getElementById('root')).render(
<MyApp />
);
Объяснение примера
Сначала импортируются необходимые функции:
createPortalиз пакетаreact-dom— для создания портала.useStateиз пакетаreact— для управления состоянием открытости модального окна.
Компонент Modal использует метод createPortal, чтобы отрендерить своё содержимое непосредственно в элемент document.body.
Причины использования порталов
Порталы полезны в случаях, когда нужно нарушить границы родительского контейнера:
- Модальные окна и диалоги
- Всплывающие подсказки
- Контекстные меню
- Уведомления
Особенно полезны порталы, когда контейнер родителя имеет такие характеристики, как:
overflow: hidden- Конфликты с
z-index - Сложные требования позиционирования
Распространение событий в порталах
Даже если портал рендерится в другой части DOM, события из него продолжают подниматься вверх по дереву React-компонентов так, будто портал отсутствует. Например, если вы нажмёте на кнопку внутри портала, событие сначала запустится в самой кнопке, а затем поднимется к родителю.
Пример
Нажатие на кнопку порождает событие, которое поднимается вверх по цепочке, вызывая обработчики событий:
import { createRoot } from 'react-dom/client';
import { useState } from 'react';
import { createPortal } from 'react-dom';
function PortalButton({ onClick, children }) {
return createPortal(
<button
onClick={onClick}
style={{
position: 'fixed',
bottom: '20px',
right: '20px',
padding: '10px',
background: 'blue',
color: 'white'
}}>
{children}
</button>,
document.body
);
}
function App() {
const [count1, setCount1] = useState(0);
const [count2, setCount2] = useState(0);
return (
<div
style={{
padding: '20px',
border: '2px solid black',
margin: '20px'
}}
onClick={() => {
setCount1(c => c + 1);
}}>
<h2>Нажатий по div: {count1}</h2>
<h2>Нажатий по кнопке: {count2}</h2>
<p>Плавающая кнопка рендерится вне этого контейнера с помощью портала, но её щелчки поднимаются вверх к родительскому контейнеру div!</p>
<p>Попробуйте также нажать по самому контейнеру, чтобы увеличить счётчик</p>
<PortalButton
onClick={(e) => {
// Первым сработает этот обработчик
setCount2(c => c + 1);
}}>
Плавающая кнопка
</PortalButton>
</div>
);
}
createRoot(document.getElementById('root')).render(
<App />
);
Объяснение примера:
- Компонент
PortalButtonрендерится как фиксированная кнопка в правом нижнем углу экрана, используя портал. - Хотя кнопка физически расположена вне родительского элемента
<div>, при её нажатии срабатывают два обработчика событий:- Сначала выполняется обработчик самой кнопки (увеличивает счётчик нажатий на кнопку).
- Затем срабатывает обработчик родительского элемента
<div>(увеличивает счётчик нажатий на div).
- Это демонстрирует, что события распространяются через дерево компонентов React, игнорируя физическую позицию в DOM.