Порталы в React: рендер компонентов вне иерархии DOM

16 марта 2026
Автор

Олег Марков

Порт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

Дополнительные материалы

Стрелочка влевоРабота с children в ReactFragment в React: группировка элементов без лишних узлов DOMСтрелочка вправо

Постройте личный план изучения React до уровня Middle — бесплатно!

React — часть карты развития Frontend

  • step100+ шагов развития
  • lessons30 бесплатных лекций
  • lessons300 бонусных рублей на счет

Бесплатные лекции

Все гайды по 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: новый стандарт работы с данными
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

Антон Ларичев
AI-тренажеры
Практика в студии
Гарантия
Бонусы
иконка звёздочки рейтинга4.8
3 999 ₽ 6 990 ₽
Подробнее
изображение курса

TypeScript с нуля

Антон Ларичев
AI-тренажеры
Практика в студии
Гарантия
Бонусы
иконка звёздочки рейтинга4.8
3 999 ₽ 6 990 ₽
Подробнее
изображение курса

Next.js - с нуля

Антон Ларичев
AI-тренажеры
Практика в студии
Гарантия
Бонусы
иконка звёздочки рейтинга4.7
3 999 ₽ 6 990 ₽
Подробнее

Отправить комментарий