Порталы (Portals) — это механизм в React, который позволяет рендерить дочерние элементы в DOM-узел, который находится вне иерархии DOM-компонента родителя.
✅ Основные особенности порталов:
❌ Частые ошибки:
Ключевое правило: Используйте порталы только тогда, когда нужно вывести элемент вне обычного потока DOM! 🎯
Представьте, что вы играете в прятки. Обычно дети прячутся в своей комнате, но порталы — это как дверь в другую комнату, где можно спрятаться, но при этом оставаться частью игры! 🚪
Порталы позволяют рендерить дочерние элементы в любом месте DOM-дерева:
// Создание портала
import { createPortal } from 'react-dom';
function Modal({ children, isOpen }) {
if (!isOpen) return null;
// Рендерим children в document.body, а не в родительский компонент
return createPortal(
<div className="modal">
{children}
</div>,
document.body
);
}Порталы решают проблему CSS-контекста и z-index:
// ❌ Проблема без порталов
function App() {
return (
<div style={{ position: 'relative', zIndex: 1 }}>
<div style={{ overflow: 'hidden' }}>
{/* Модальное окно обрезается контейнером! */}
<Modal>
<h1>Модальное окно</h1>
</Modal>
</div>
</div>
);
}
// ✅ Решение с порталами
function Modal({ children }) {
return createPortal(
<div className="modal-overlay">
<div className="modal-content">
{children}
</div>
</div>,
document.body
);
}Порталы не изменяют поведение событий в React:
function ParentComponent() {
const handleClick = () => {
console.log('Клик по родителю');
};
return (
<div onClick={handleClick}>
<h1>Родительский компонент</h1>
{/* Событие будет всплывать к ParentComponent,
даже если кнопка в портале! */}
{createPortal(
<button>Кнопка в портале</button>,
document.body
)}
</div>
);
}Порталы сохраняют доступ к контексту:
const ThemeContext = createContext('light');
function App() {
return (
<ThemeContext.Provider value="dark">
<div>
<Header />
{/* Портал получает доступ к теме через контекст */}
<PortalComponent />
</div>
</ThemeContext.Provider>
);
}
function PortalComponent() {
const theme = useContext(ThemeContext);
return createPortal(
<div className={`modal ${theme}`}>
Модальное окно в теме: {theme}
</div>,
document.body
);
}import { useState, useEffect } from 'react';
import { createPortal } from 'react-dom';
function Modal({ isOpen, onClose, children }) {
const [modalRoot, setModalRoot] = useState(null);
useEffect(() => {
// Создаем контейнер для модального окна
const modalContainer = document.createElement('div');
modalContainer.className = 'modal-root';
document.body.appendChild(modalContainer);
setModalRoot(modalContainer);
// Очистка
return () => {
document.body.removeChild(modalContainer);
};
}, []);
if (!isOpen || !modalRoot) return null;
return 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>,
modalRoot
);
}
// Использование
function App() {
const [isModalOpen, setIsModalOpen] = useState(false);
return (
<div>
<button onClick={() => setIsModalOpen(true)}>
Открыть модальное окно
</button>
<Modal
isOpen={isModalOpen}
onClose={() => setIsModalOpen(false)}
>
<h2>Содержимое модального окна</h2>
<p>Это модальное окно рендерится вне основного DOM-дерева!</p>
</Modal>
</div>
);
}import { useState, useRef } from 'react';
import { createPortal } from 'react-dom';
function Tooltip({ children, content }) {
const [isVisible, setIsVisible] = useState(false);
const targetRef = useRef(null);
const tooltipRef = useRef(null);
const showTooltip = () => setIsVisible(true);
const hideTooltip = () => setIsVisible(false);
if (!isVisible) return (
<span
ref={targetRef}
onMouseEnter={showTooltip}
onMouseLeave={hideTooltip}
>
{children}
</span>
);
const targetRect = targetRef.current?.getBoundingClientRect();
const position = targetRect ? {
top: targetRect.bottom + window.scrollY + 5,
left: targetRect.left + window.scrollX
} : { top: 0, left: 0 };
return createPortal(
<div
ref={tooltipRef}
className="tooltip"
style={{
position: 'absolute',
top: `${position.top}px`,
left: `${position.left}px`,
zIndex: 1000
}}
onMouseEnter={showTooltip}
onMouseLeave={hideTooltip}
>
{content}
</div>,
document.body
);
}
// Использование
function App() {
return (
<div>
<p>Наведите на <Tooltip content="Это тултип!">это слово</Tooltip> чтобы увидеть тултип.</p>
</div>
);
}// ✅ Идеальное использование порталов
function Modal({ isOpen, children }) {
if (!isOpen) return null;
return createPortal(
<div className="modal-backdrop">
<div className="modal-dialog">
{children}
</div>
</div>,
document.body
);
}// ✅ Выпадающие меню вне контейнера с overflow: hidden
function Dropdown({ options, isOpen }) {
if (!isOpen) return null;
return createPortal(
<div className="dropdown-menu">
{options.map(option => (
<div key={option.value} className="dropdown-item">
{option.label}
</div>
))}
</div>,
document.body
);
}// ✅ Уведомления, которые должны быть поверх всего
function Notification({ message, isVisible }) {
if (!isVisible) return null;
return createPortal(
<div className="notification toast">
{message}
</div>,
document.getElementById('notification-root') || document.body
);
}// ❌ Проблема с утечками памяти
function BadModal({ isOpen }) {
if (!isOpen) return null;
// Создаем новый элемент при каждом рендере!
const container = document.createElement('div');
document.body.appendChild(container);
return createPortal(<div>Модальное окно</div>, container);
}
// ✅ Правильная очистка
function GoodModal({ isOpen }) {
const [container, setContainer] = useState(null);
useEffect(() => {
const div = document.createElement('div');
document.body.appendChild(div);
setContainer(div);
return () => {
// Очистка при размонтировании
document.body.removeChild(div);
};
}, []);
if (!isOpen || !container) return null;
return createPortal(<div>Модальное окно</div>, container);
}// ❌ Недоступное модальное окно
function BadModal({ isOpen }) {
if (!isOpen) return null;
return createPortal(
<div>
<h2>Заголовок</h2>
<p>Содержимое</p>
</div>,
document.body
);
}
// ✅ Доступное модальное окно
function GoodModal({ isOpen, onClose }) {
const modalRef = useRef(null);
useEffect(() => {
if (isOpen && modalRef.current) {
// Фокус на модальное окно
modalRef.current.focus();
// Обработка Escape
const handleEscape = (e) => {
if (e.key === 'Escape') onClose();
};
document.addEventListener('keydown', handleEscape);
return () => document.removeEventListener('keydown', handleEscape);
}
}, [isOpen, onClose]);
if (!isOpen) return null;
return createPortal(
<div
role="dialog"
aria-modal="true"
ref={modalRef}
tabIndex="-1"
>
<h2>Заголовок</h2>
<p>Содержимое</p>
<button onClick={onClose}>Закрыть</button>
</div>,
document.body
);
}import { useState, useEffect } from 'react';
import { createPortal } from 'react-dom';
function usePortal(containerId) {
const [container, setContainer] = useState(null);
useEffect(() => {
// Пытаемся найти существующий контейнер
let element = document.getElementById(containerId);
// Если нет - создаем новый
if (!element) {
element = document.createElement('div');
element.id = containerId;
document.body.appendChild(element);
}
setContainer(element);
return () => {
// Удаляем только если создали сами
if (!document.getElementById(containerId)) {
document.body.removeChild(element);
}
};
}, [containerId]);
return container;
}
// Использование хука
function Modal({ isOpen, children, containerId = 'modal-root' }) {
const container = usePortal(containerId);
if (!isOpen || !container) return null;
return createPortal(children, container);
}// ✅ Система z-index для порталов
const Z_INDEX = {
tooltip: 1000,
dropdown: 1100,
modal: 1200,
notification: 1300
};
function Modal({ children }) {
return createPortal(
<div
className="modal-overlay"
style={{ zIndex: Z_INDEX.modal }}
>
<div className="modal-content">
{children}
</div>
</div>,
document.body
);
}Порталы — это как волшебная дверь, которая позволяет элементам появляться в другом месте DOM! 🚪✨
Когда использовать:
Когда не использовать:
Практические советы:
Порталы — мощный инструмент для решения специфических задач позиционирования в React! 💪
Хотите больше полезных статей о React? Подписывайтесь на EasyAdvice, добавляйте сайт в закладки и прокачивайтесь каждый день! 🚀