Что такое React Portals и когда их стоит использовать?
React Portal рендерит дочернее дерево в произвольный DOM-узел вне родительского контейнера, при этом сохраняя React-иерархию. Используется для модалок, тултипов и нотификаций, чтобы обойти ограничения CSS-стекирования (overflow: hidden, stacking context).
Что такое React Portals
Portal — механизм рендеринга дочернего дерева React в DOM-узел, находящийся вне родительского контейнера компонента. API: ReactDOM.createPortal(children, domNode). Несмотря на физическое расположение в другом DOM-узле, компонент остаётся полноправным дочерним в дереве React: события всплывают по React-дереву, а не по DOM, контекст работает как обычно.
import { useEffect, useRef } from "react";
import { createPortal } from "react-dom";
function Modal({ isOpen, onClose, children }) {
const elRef = useRef(null);
if (!elRef.current) {
elRef.current = document.createElement("div");
}
useEffect(() => {
const modalRoot = document.getElementById("modal-root");
modalRoot.appendChild(elRef.current);
return () => modalRoot.removeChild(elRef.current);
}, []);
if (!isOpen) return null;
return createPortal(
<div className="overlay" onClick={onClose}>
<div className="modal" onClick={(e) => e.stopPropagation()}>
{children}
</div>
</div>,
elRef.current
);
}
В HTML нужен целевой контейнер рядом с #root:
// index.html
<div id="root"></div>
<div id="modal-root"></div>
Когда порталы нужны
- Модальные окна и диалоги — компонент модалки может быть вложен в элемент с
overflow: hiddenилиposition: relative, что обрежет её визуально. Портал рендерит модалку вbody, обходя ограничения CSS-стекирования. - Тултипы и выпадающие списки —
z-indexработает в рамках stacking context. Если родитель создаёт свой stacking context (transform,filter,opacity < 1), тултип окажется под другими элементами. Портал выводит его за пределы этого контекста. - Toast-уведомления — глобальные уведомления должны отображаться поверх всего интерфейса, независимо от того, из какого компонента они инициированы.
- Drag-and-drop слой — перетаскиваемый элемент должен двигаться поверх всего без ограничений.
Всплытие событий через порталы
Событие клика на элементе портала всплывает по React-дереву до логического родителя, а не по DOM-дереву. Это может удивить: обработчик клика на компоненте-родителе сработает, даже если кликнуть внутри портала.
function Parent() {
// Этот onClick сработает при клике внутри Modal,
// хотя в DOM они в разных узлах
return (
<div onClick={() => console.log("parent clicked")}>
<Modal isOpen />
</div>
);
}
Порталы в Next.js и SSR
При серверном рендеринге document недоступен. Безопасный способ — монтировать портал только на клиенте через useEffect или проверять typeof document !== "undefined". В Next.js App Router компонент с порталом должен быть помечен директивой "use client".
Подводные камни
- Отсутствие DOM-узла при SSR — вызов
document.getElementByIdна сервере бросает ошибку; обязательно монтировать черезuseEffectили динамический импорт сssr: false. - Утечка DOM-узлов — если вручную создавать
divи добавлять вbody, необходимо удалять его в cleanupuseEffect, иначе каждый ремаунт оставляет «мусор» в DOM. - Неожиданное всплытие событий — onClick на родителе ловит события из портала; нужно явно вызывать
e.stopPropagation()там, где это нежелательно. - Фокус и доступность — при открытии модалки необходимо переводить фокус внутрь и ловить Tab/Shift+Tab (focus trap); без этого скринридеры и клавиатурная навигация сломаны.
- z-index не панацея — сам по себе
z-indexне решает проблему stacking context; портал решает её переносом узла выше по DOM. - Контекст работает, но не всегда очевидно — значение контекста берётся из React-дерева, поэтому провайдер, обёрнутый вокруг портала, будет виден внутри него, даже если DOM-узел портала находится за его пределами.
- Тестирование с Testing Library —
getByRoleи другие запросы ищут по всему документу, включая порталы, поэтому тесты работают корректно; но нужно убедиться, что DOM-контейнер портала существует в тестовой среде (jsdom).
Common mistakes
- Класть portal-target внутрь компонента с
overflow: hiddenи удивляться обрезке. - Полагаться на DOM-всплытие, забыв, что React-делегирование идёт по логическому дереву.
- Забыть про фокус-трап и обработку
Escapeв модалке.
What the interviewer is testing
- Знает ли разницу между всплытием в DOM и в React.
- Может ли назвать сценарии (modal, tooltip, toast).
- Помнит ли про accessibility-требования к модалкам.