Что такое Portals в React и зачем они нужны?
Portals в React — это способ рендерить дочерние элементы в DOM-узел, который находится за пределами DOM-иерархии родительского компонента. Это официальная возможность React, представленная начиная с версии 16.
Функция ReactDOM.createPortal(child, container) позволяет рендерить любой JSX (или компонент) в указанное место в DOM, даже если оно находится вне текущей иерархии компонентов.
Синтаксис
ReactDOM.createPortal(
<MyComponent />,
document.getElementById('portal-root')
);
-
child — React-элемент, который нужно отрендерить.
-
container — DOM-элемент (например, div), куда этот элемент будет вставлен в HTML-дереве.
Пример: Рендер модального окна через портал
HTML:
<body>
<div id="root"></div>
<div id="modal-root"></div>
</body>
Компонент:
import ReactDOM from 'react-dom';
function Modal({ children }) {
return ReactDOM.createPortal(
<div className="modal-overlay">
{children}
</div>,
document.getElementById('modal-root')
);
}
Использование:
function App() {
const \[isOpen, setIsOpen\] = React.useState(false);
return (
<div>
<button onClick={() => setIsOpen(true)}>Открыть модалку</button>
{isOpen && (
<Modal>
<div className="modal-content">
<h1>Это модальное окно</h1>
<button onClick={() => setIsOpen(false)}>Закрыть</button>
</div>
</Modal>
)}
</div>
);
}
Зачем нужны Portals
В React компоненты обычно "вложены" друг в друга логически и DOM-иерархически. Но в некоторых случаях нужно визуально и структурно "вырваться" из этой иерархии. Примеры:
-
Модальные окна
-
Тултипы
-
Дропдауны
-
Контекстные меню
-
Нотификации
-
Фиксированные элементы, не зависящие от родителя по стилям
Если такие компоненты рендерить в пределах обычной иерархии (например, внутри overflow: hidden), они могут быть обрезаны, перекрыты, неправильно позиционированы. Portals позволяют избежать этих проблем.
Визуальная структура
Предположим, у вас есть следующая структура:
<div id="root">
<App>
<Modal>
// содержимое
</Modal>
</App>
</div>
<div id="modal-root"></div>
Компонент Modal визуально может находиться в modal-root, но React по-прежнему понимает, что он является дочерним элементом App и других компонентов выше в дереве. Это значит, что он наследует контекст, хуки, props.
Контекст и события
-
Порталы не разрывают дерево контекста React.
Это значит, что внутри портала можно использовать useContext, useTheme, useStore, и всё будет работать корректно. -
Порталы не теряют связь с React-деревом. Например, всплытие событий работает как будто элемент по-прежнему находится внутри родительского DOM.
Пример:
function App() {
const handleClick = () => {
console.log('Click bubbled to App');
};
return (
<div onClick={handleClick}>
<Modal>
<button>Нажми меня</button>
</Modal>
</div>
);
}
Когда вы кликаете кнопку внутри Modal, событие onClick всё равно поднимется до родителя App, даже если Modal отрисован в другом DOM-элементе.
Типичный паттерн использования порталов
1. Создайте отдельный контейнер в HTML:
<div id="portal-root"></div>
2. Используйте createPortal в компоненте:
const Portal = ({ children }) => {
const el = document.getElementById('portal-root');
return ReactDOM.createPortal(children, el);
};
3. Добавьте в компонент:
<Portal>
<div>Я вне обычной иерархии DOM</div>
</Portal>
Особенности работы и ограничения
-
Нужно удостовериться, что DOM-элемент (portal-root) существует в момент монтирования.
-
В SSR (Next.js, Gatsby) или при изоляции DOM (например, при тестировании) может потребоваться проверка typeof window !== 'undefined' и доступность DOM.
Типичный баг: рендер до монтирования DOM
const el = document.getElementById('modal-root'); // может быть null
return ReactDOM.createPortal(<div>Контент</div>, el!); // ошибка при SSR
Решение — использовать useEffect и useState для динамического рендера портала только после загрузки DOM:
function SafePortal({ children }) {
const \[mounted, setMounted\] = useState(false);
useEffect(() => {
setMounted(true);
}, \[\]);
if (!mounted) return null;
return ReactDOM.createPortal(
children,
document.getElementById('portal-root')
);
}
Использование с библиотеками
-
В UI-библиотеках (Material UI, Ant Design, Headless UI) многие модалки, поповеры, тултипы уже используют Portals.
-
В библиотеке React Portal можно использовать createPortal напрямую или через их обёртки.
Альтернатива: CSS-only модалки?
Можно делать модалки и тултипы без порталов — всё зависит от структуры CSS. Но Portals дают больше гибкости, особенно при работе с ограничениями overflow, z-index, и при необходимости монтировать элементы в строго определённое место DOM.
Portals — это важный инструмент, который решает одну из ключевых проблем фронтенд-разработки: отделение визуального отображения от логической иерархии компонентов, сохраняя при этом связь с React-контекстом и деревом событий.