Комментарии (1)
🐱
claude-haiku-4.5PrepBro AI22 мар. 2026 г.(ред.)
Ответ сгенерирован нейросетью и может содержать ошибки
React Portal и его использование
Portal (портал) в React — это способ рендеринга компонента в другой узел DOM, отдельный от родительского компонента. Это мощный инструмент для создания модальных окон, всплывающих подсказок и других UI элементов, которые должны быть вне стандартного потока документа.
Основная концепция
Обычно React компонент рендеринается внутри его родителя в DOM дереве. Portal позволяет рендеринять компонент в совершенно другом месте DOM, сохраняя при этом связь с React компонентом.
// Обычный рендеринг
<div id="root">
<App>
<MyComponent /> // рендеринается внутри App
</App>
</div>
// С Portal
<div id="root">
<App>
{/* компонент остаётся в React дереве, но рендеринается в другом месте DOM */}
</App>
</div>
<div id="modal-root">
{/* MyComponent рендеринается сюда */}
</div>
Синтаксис ReactDOM.createPortal
import ReactDOM from 'react-dom';
function MyPortal() {
return ReactDOM.createPortal(
<div>Содержимое портала</div>,
document.getElementById('portal-root') // DOM элемент, куда рендеринается
);
}
Практический пример: Модальное окно
1. Подготовка HTML
<!DOCTYPE html>
<html>
<body>
<div id="root"></div>
<div id="modal-root"></div> <!-- Место для портала -->
</body>
</html>
2. Компонент Modal с Portal
import ReactDOM from 'react-dom';
import { useState } from 'react';
import './Modal.css';
function Modal({ isOpen, onClose, children }) {
if (!isOpen) return null;
return ReactDOM.createPortal(
<div className="modal-overlay" onClick={onClose}>
<div className="modal-content" onClick={(e) => e.stopPropagation()}>
<button className="modal-close" onClick={onClose}>×</button>
{children}
</div>
</div>,
document.getElementById('modal-root')
);
}
export default Modal;
3. Использование Modal
function App() {
const [isModalOpen, setIsModalOpen] = useState(false);
return (
<div className="app">
<button onClick={() => setIsModalOpen(true)}>Открыть модаль</button>
<Modal isOpen={isModalOpen} onClose={() => setIsModalOpen(false)}>
<h2>Заголовок модали</h2>
<p>Содержимое модали</p>
<button onClick={() => setIsModalOpen(false)}>Закрыть</button>
</Modal>
</div>
);
}
Стили для Modal
.modal-overlay {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background-color: rgba(0, 0, 0, 0.5);
display: flex;
align-items: center;
justify-content: center;
z-index: 1000;
}
.modal-content {
background-color: white;
border-radius: 8px;
padding: 24px;
max-width: 500px;
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
position: relative;
}
.modal-close {
position: absolute;
top: 12px;
right: 12px;
background: none;
border: none;
font-size: 24px;
cursor: pointer;
color: #999;
}
.modal-close:hover {
color: #333;
}
Другие применения Portal
1. Всплывающие подсказки (Tooltip)
function Tooltip({ text, children }) {
const [position, setPosition] = useState({ top: 0, left: 0 });
const ref = useRef(null);
const handleMouseEnter = () => {
const rect = ref.current.getBoundingClientRect();
setPosition({
top: rect.top + rect.height + 10,
left: rect.left,
});
};
return (
<>
<div ref={ref} onMouseEnter={handleMouseEnter}>
{children}
</div>
{/* Рендеринаем tooltip в отдельный DOM узел */}
{ReactDOM.createPortal(
<div className="tooltip" style={{
position: 'fixed',
top: position.top,
left: position.left,
}}>
{text}
</div>,
document.body
)}
</>
);
}
2. Выпадающее меню (Dropdown)
function Dropdown({ trigger, items }) {
const [isOpen, setIsOpen] = useState(false);
const [position, setPosition] = useState({});
const triggerRef = useRef(null);
const handleToggle = () => {
if (!isOpen && triggerRef.current) {
const rect = triggerRef.current.getBoundingClientRect();
setPosition({
top: rect.bottom + 5,
left: rect.left,
});
}
setIsOpen(!isOpen);
};
return (
<>
<button ref={triggerRef} onClick={handleToggle}>
{trigger}
</button>
{isOpen && ReactDOM.createPortal(
<ul className="dropdown" style={{
position: 'fixed',
...position,
zIndex: 999,
}}>
{items.map((item) => (
<li key={item.id}>
<a href={item.href}>{item.label}</a>
</li>
))}
</ul>,
document.body
)}
</>
);
}
3. Уведомления (Toast)
function Toast({ message, type }) {
useEffect(() => {
const timer = setTimeout(() => {
// удалить toast
}, 3000);
return () => clearTimeout(timer);
}, []);
return ReactDOM.createPortal(
<div className={`toast toast-${type}`}>
{message}
</div>,
document.getElementById('toast-root')
);
}
Почему Portal полезен
1. Избегаем проблем с overflow и z-index
// Без Portal: модаль может быть обрезана parent overflow
<div style={{ overflow: 'hidden' }}>
<Modal /> {/* может быть скрыта! */}
</div>
// С Portal: модаль рендеринается вне этого контекста
<div style={{ overflow: 'hidden' }}>
{/* компонент остаётся в React дереве */}
</div>
<div id="modal-root"> {/* модаль рендеринается сюда */}
<Modal />
</div>
2. Правильная иерархия для доступности
// Modal рендеринается в отдельный контейнер, что улучшает z-index
// и делает её доступной для screen readers
function Modal() {
return ReactDOM.createPortal(
<div role="dialog" aria-modal="true">
{/* содержимое */}
</div>,
document.getElementById('modal-root')
);
}
3. Управление фокусом
function Modal({ isOpen, onClose }) {
useEffect(() => {
if (isOpen) {
// Переместить фокус в модаль
document.getElementById('modal-content').focus();
}
}, [isOpen]);
if (!isOpen) return null;
return ReactDOM.createPortal(
<div id="modal-content" tabIndex="-1">
{/* Modal content */}
</div>,
document.getElementById('modal-root')
);
}
Portal с контекстом
Portal сохраняет доступ к контексту родительского компонента:
const ThemeContext = React.createContext('light');
function App() {
return (
<ThemeContext.Provider value="dark">
<div>
<MyPortalComponent /> {/* может получить доступ к контексту */}
</div>
</ThemeContext.Provider>
);
}
function MyPortalComponent() {
const theme = useContext(ThemeContext); // 'dark'
return ReactDOM.createPortal(
<div className={`portal-${theme}`}>
Это портал с темой
</div>,
document.getElementById('portal-root')
);
}
Обработка событий в Portal
События в Portal всё ещё всплывают в React дерево, не в DOM дерево:
function App() {
const handleClick = (e) => {
console.log('App получил клик'); // Выполнится!
};
return (
<div onClick={handleClick}>
<button onClick={() => ReactDOM.createPortal(
<button onClick={(e) => {
e.stopPropagation();
console.log('Portal button clicked');
}}>Кнопка в портале</button>,
document.body
)}>Open Portal</button>
</div>
);
}
Когда использовать Portal
- Модальные окна — самое распространённое применение
- Всплывающие подсказки (tooltips)
- Выпадающие меню (dropdowns)
- Уведомления (toast, snackbars)
- Меню контекста (context menu)
- Компоненты, требующие полного экрана (fullscreen видео)
Практический пример с React 19 синтаксисом
import { createPortal } from 'react-dom';
import { useState } from 'react';
export function Modal() {
const [isOpen, setIsOpen] = useState(false);
return (
<>
<button onClick={() => setIsOpen(true)}>Открыть</button>
{isOpen && createPortal(
<dialog open>
<h2>Модальное окно</h2>
<button onClick={() => setIsOpen(false)}>Закрыть</button>
</dialog>,
document.getElementById('modal-root')
)}
</>
);
}
Portal — это мощный инструмент для создания качественных UI компонентов, которые не зависят от структуры родительского контейнера. Это особенно полезно для модальных окон, где нужно избежать проблем с overflow и z-index.