Олег Марков
Порталы в React: рендер компонентов вне иерархии DOM
Автор
Портals — рендер вне иерархии DOM
Portal — это механизм React, позволяющий рендерить дочерний компонент в другой узел DOM, находящийся вне иерархии родительского компонента. При этом компонент остаётся частью React-дерева и получает события и контекст от своих родителей.
import { createPortal } from 'react-dom';
function Modal({ children }: { children: React.ReactNode }) {
return createPortal(
<div className="modal">{children}</div>,
document.body // Рендерим прямо в body, а не внутри родителя
);
}
Проблема: CSS overflow и z-index
Порталы решают классическую проблему: компонент внутри контейнера с overflow: hidden или z-index не может выйти за его пределы.
// ❌ Проблема: модальное окно обрезается overflow: hidden
// HTML-структура:
// <div style="overflow: hidden; position: relative;"> ← родитель
// <div class="modal">...</div> ← модалка обрезана!
// </div>
// ✅ Решение с Portal: модалка рендерится в body
// <div style="overflow: hidden;">...</div> ← родитель в DOM
// <div class="modal">...</div> ← модалка в body!
Создание портала: createPortal
import { createPortal } from 'react-dom';
// Сигнатура:
// createPortal(children, domNode, key?)
// children — JSX для рендера
// domNode — DOM-узел назначения
// key — опциональный уникальный ключ
function Tooltip({ text, children }: TooltipProps) {
const [visible, setVisible] = useState(false);
const ref = useRef<HTMLDivElement>(null);
return (
<div
ref={ref}
onMouseEnter={() => setVisible(true)}
onMouseLeave={() => setVisible(false)}
>
{children}
{visible && createPortal(
<div className="tooltip">{text}</div>,
document.body
)}
</div>
);
}
Модальное окно через Portal
Наиболее распространённый случай применения порталов — модальные окна:
// components/Modal.tsx
import { createPortal } from 'react-dom';
import { useEffect, useRef } from 'react';
interface ModalProps {
isOpen: boolean;
onClose: () => void;
title: string;
children: React.ReactNode;
}
function Modal({ isOpen, onClose, title, children }: ModalProps) {
const modalRef = useRef<HTMLDivElement>(null);
// Закрытие по Escape
useEffect(() => {
const handleKeyDown = (e: KeyboardEvent) => {
if (e.key === 'Escape') onClose();
};
if (isOpen) {
document.addEventListener('keydown', handleKeyDown);
document.body.style.overflow = 'hidden'; // Блокируем скролл
}
return () => {
document.removeEventListener('keydown', handleKeyDown);
document.body.style.overflow = '';
};
}, [isOpen, onClose]);
if (!isOpen) return null;
return createPortal(
<div
className="modal-overlay"
onClick={onClose}
role="dialog"
aria-modal="true"
aria-labelledby="modal-title"
>
<div
className="modal-content"
ref={modalRef}
onClick={e => e.stopPropagation()} // Не закрывать при клике внутри
>
<div className="modal-header">
<h2 id="modal-title">{title}</h2>
<button onClick={onClose} aria-label="Закрыть">✕</button>
</div>
<div className="modal-body">{children}</div>
</div>
</div>,
document.body
);
}
export default Modal;
// Использование
function App() {
const [isModalOpen, setIsModalOpen] = useState(false);
return (
<div style={{ overflow: 'hidden', height: '200px' }}>
<button onClick={() => setIsModalOpen(true)}>Открыть модальное окно</button>
{/* Modal рендерится в document.body, не внутри overflow: hidden */}
<Modal
isOpen={isModalOpen}
onClose={() => setIsModalOpen(false)}
title="Подтверждение"
>
<p>Вы уверены?</p>
<button onClick={() => setIsModalOpen(false)}>Да</button>
</Modal>
</div>
);
}
Всплытие событий через Portal
Несмотря на то что портал рендерится вне DOM-иерархии, события всплывают по React-дереву, а не по DOM-дереву.
function Parent() {
const handleClick = () => {
console.log('Клик перехвачен родителем!');
// Сработает, даже если портал рендерится в document.body
};
return (
<div onClick={handleClick}>
<p>Родительский элемент</p>
{createPortal(
<button>Кнопка в портале</button>, // Рендерится в body
document.body
)}
</div>
);
}
Это означает, что:
- Контекст React доступен внутри портала
- Обработчики событий родителей перехватывают события из портала
- Состояние и пропсы работают как обычно
Кастомный хук usePortal
// hooks/usePortal.ts
import { useEffect, useRef } from 'react';
function usePortal(id: string = 'portal-root') {
const portalRef = useRef<HTMLDivElement | null>(null);
useEffect(() => {
// Ищем существующий контейнер или создаём новый
let portal = document.getElementById(id) as HTMLDivElement;
if (!portal) {
portal = document.createElement('div');
portal.id = id;
document.body.appendChild(portal);
}
portalRef.current = portal;
return () => {
// Удаляем контейнер если он пустой
if (portal.childNodes.length === 0) {
portal.remove();
}
};
}, [id]);
return portalRef.current;
}
// Использование хука
function Notification({ message }: { message: string }) {
const portal = usePortal('notifications');
if (!portal) return null;
return createPortal(
<div className="notification">{message}</div>,
portal
);
}
Server-Side Rendering (SSR)
При SSR document недоступен. Порталы нужно рендерить только на клиенте:
// components/ClientPortal.tsx
'use client'; // Next.js App Router
import { createPortal } from 'react-dom';
import { useState, useEffect } from 'react';
interface ClientPortalProps {
children: React.ReactNode;
selector?: string;
}
function ClientPortal({ children, selector = 'body' }: ClientPortalProps) {
const [mounted, setMounted] = useState(false);
useEffect(() => {
setMounted(true);
}, []);
if (!mounted) return null;
const target = document.querySelector(selector);
if (!target) return null;
return createPortal(children, target);
}
// Использование в Next.js
function MyModal({ isOpen, onClose }: ModalProps) {
if (!isOpen) return null;
return (
<ClientPortal selector="body">
<div className="modal-overlay" onClick={onClose}>
<div className="modal-content">
<p>Контент модального окна</p>
</div>
</div>
</ClientPortal>
);
}
Использование в React 19
В React 19 появилась возможность рендерить порталы в <head> для метаданных:
// React 19 — нативные метаданные через Portal-подобный механизм
function BlogPost({ post }: { post: Post }) {
return (
<article>
{/* React 19 автоматически перемещает эти теги в <head> */}
<title>{post.title}</title>
<meta name="description" content={post.excerpt} />
<link rel="canonical" href={post.url} />
<h1>{post.title}</h1>
<p>{post.content}</p>
</article>
);
}
Типичные случаи применения
| Случай | Причина использования Portal |
|---|---|
| Модальные окна | Выход за пределы overflow: hidden |
| Тултипы и поповеры | Корректный z-index поверх любого контента |
| Уведомления (toasts) | Всегда в углу экрана вне зависимости от родителя |
| Выпадающие меню | Избежать обрезания по границам контейнера |
| Глобальный лоадер | Перекрытие всего интерфейса |
Краткое резюме
| Концепция | Суть |
|---|---|
| Portal | Рендер в другой DOM-узел, оставаясь в React-дереве |
createPortal | API для создания портала |
| Всплытие событий | Идёт по React-дереву, не по DOM |
| Контекст | Полностью доступен внутри портала |
| SSR | Нужна защита от document is not defined |
Дополнительные материалы
Постройте личный план изучения React до уровня Middle — бесплатно!
React — часть карты развития Frontend
100+ шагов развития
30 бесплатных лекций
300 бонусных рублей на счет
Бесплатные лекции
Все гайды по React
Uncontrolled Components: когда DOM управляет даннымиRender Props: гибкое управление рендерингом в ReactПрофилирование React: как найти и устранить узкие местаЧастичное применение: как создавать компоненты без лишнего кодаЛенивая загрузка: как ускорить React-приложение в разыHOC в React: мастерство композиции компонентовuseMemo: как спасти производительность от тяжелых вычисленийError Boundaries: создаем надежные React-приложенияКонтролируемые компоненты в React: полный контроль над формамиCompound Components в React: создаем гибкие компоненты с мощным APIКомпозиция компонентов в React: строим гибкие интерфейсыCode Splitting в React: как уменьшить бандл и ускорить загрузку приложенияАсинхронные компоненты в React: новый стандарт работы с данными
Zustand — управление состоянием в ReactRecoil — библиотека управления состоянием от FacebookВиртуализация списков с react-window: как отображать тысячи элементов без лаговСоздание таблиц в React гайд по react-tableРабота с формами и селектами в ReactПлагины в React что это и как их использоватьОбзор популярных библиотек для ReactПодключение Bootstrap к React-приложениюMobX — реактивное управление состоянием в ReactИспользование Chart.js в React
Что такое Vite и как запускать проекты React через негоКак использовать Next.js вместе с ReactКак подключить Tailwind к ReactРабота с Redux в React-приложенииИспользование API в React-приложенияхИспользование Expo для разработки на ReactИспользование React вместе с GoИнтеграция Express и ReactНастройка маршрутизации в React с React Router
useState в React что это и как использоватьuseTransition - плавные переходы между состояниямиuseSyncExternalStore — работа с внешними сторамиuseRef в React — создание ссылок на DOM и значенияuseLayoutEffect в React — эффект до отрисовкиuseInsertionEffect — внедрение стилей до мутаций DOMuseImperativeHandle в React — настройка ref дочернего компонентаuseId — генерация уникальных идентификаторовuseDeferredValue — отложенное обновление состоянияuseDebugValue — отладка кастомных хуковuseCallback в React — мемоизация функцийuseReducer — альтернатива useState для сложной логикиuseMemo в React: как и когда оптимизировать тяжелые вычисленияuseEffect в React что это и как использоватьuseContext — работа с контекстом в ReactОптимизация рендеринга в React: от теории к глубокой практикеЧто такое useRef и как его применять в ReactКак и зачем использовать React HooksУправление состоянием в React через ContextКак предотвратить лишние ре-рендеры в React: полное руководствоuseMemo vs useCallback: подробное руководство по мемоизации в ReactПравила хуков — правила использованияuseEffect vs useLayoutEffect: в чём разница и какой хук выбрать?Кастомные хуки в React — создание собственных хуковuseState продвинутое использование в React
StrictMode в React — как находить ошибки на этапе разработкиСерверные компоненты React (RSC) — подробный разбор и практикаКак работает рендеринг в ReactЧто такое props в React и как их правильно использоватьКак работает JSX связка React и HTMLЧто такое React.js и как его использоватьКак использовать элементы в ReactКак использовать React DOM в проектеЧто такое компоненты в React и как их применятьРабота с children в ReactПорталы в React: рендер компонентов вне иерархии DOMFragment в React: группировка элементов без лишних узлов DOM
Лучшие курсы по теме

React и Redux Toolkit
Антон ЛаричевГарантия
Бонусы

TypeScript с нуля
Антон ЛаричевГарантия
Бонусы

Next.js - с нуля
Антон ЛаричевГарантия
Бонусы