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

30 июля 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!

Курс по теме

Пройти курс React и Redux Toolkit

40 000+ студентов · рейтинг 4.8 · гарантия возврата 30 дней

Создание портала: 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 управляет даннымиБезопасность в React: защита от XSS, CSRF и утечек данныхRender Props: гибкое управление рендерингом в ReactРефакторинг React-кода: техники и лучшие практикиReact Native приложение с нуля до публикации в сторахПрофилирование React: как найти и устранить узкие местаЧастичное применение: как создавать компоненты без лишнего кодаИменование компонентов в React: соглашения и лучшие практикиЛенивая загрузка: как ускорить React-приложение в разыHOC в React: мастерство композиции компонентовuseMemo: как спасти производительность от тяжелых вычисленийError Boundaries: создаем надежные React-приложенияКонтролируемые компоненты в React: полный контроль над формамиCompound Components в React: создаем гибкие компоненты с мощным APIДокументирование компонентов в React: Storybook, JSDoc и READMEКомпозиция компонентов в React: строим гибкие интерфейсыКомментирование кода в React: когда и как писать комментарииCode Splitting в React: как уменьшить бандл и ускорить загрузку приложенияАсинхронные компоненты в React: новый стандарт работы с даннымиДоступность (a11y) в React: ARIA, семантика и клавиатурная навигация
Zustand — управление состоянием в ReactZod - валидация с TypeScriptYup - валидация схемXState - конечные автоматыТемизация в ReactТестирование хуковTailwind CSS с ReactSWR - библиотека для запросовStyled Components — стилизация через JSStorybook - документация компонентовSnapshots тестированиеRTK Query - работа с APIRedux Toolkit - современный ReduxRecoil — библиотека управления состоянием от FacebookВиртуализация списков с react-window: как отображать тысячи элементов без лаговReact Toastify - уведомления в ReactReact Testing LibraryСоздание таблиц в React гайд по react-tableReact Spring - анимацииРабота с формами и селектами в ReactReact Query (TanStack Query) - работа с серверомПлагины в React что это и как их использоватьReact PDF - работа с PDF файламиОбзор популярных библиотек для ReactReact Icons - библиотека иконок для ReactReact Hook Form — валидация форм в ReactReact Dropzone — загрузка файловПодключение Bootstrap к React-приложениюReact Beautiful DnD - перетаскивание элементовАнимация при монтировании компонентов в ReactМокирование APIMobX — реактивное управление состоянием в ReactМикрофронтенды с React (micro-frontends)Загрузка и индикаторыАнимация списков в ReactJotai - атомарное состояниеБесконечная прокруткаFramer Motion - библиотека анимацийEmotion — библиотека CSS-in-JSДинамические стили в ReactE2E тестирование с CypressCSSTransition - переходыCSS-in-JS — плюсы и минусыКонтекст vs Redux — когда что использоватьИспользование Chart.js в ReactAxios с ReactТестирование асинхронных компонентовОбработка ошибок API
useState в React что это и как использоватьuseTransition - плавные переходы между состояниямиuseSyncExternalStore — работа с внешними сторамиuseRef в React — создание ссылок на DOM и значенияuseOptimistic — оптимистичные обновления UIuseLayoutEffect в React — эффект до отрисовкиuseInsertionEffect — внедрение стилей до мутаций DOMuseImperativeHandle в React — настройка ref дочернего компонентаuseId — генерация уникальных идентификаторовuseFormStatus - отслеживание статуса отправки формыuseDeferredValue — отложенное обновление состоянияuseDebugValue — отладка кастомных хуковuseCallback в React — мемоизация функцийuseReducer — альтернатива useState для сложной логикиuseMemo в React: как и когда оптимизировать тяжелые вычисленияХук use() в React 19useEffect в React что это и как использоватьuseContext — работа с контекстом в ReactuseCallback в React — мемоизация функций и оптимизация ре-рендеровuseActionState в React 19Оптимизация рендеринга в React: от теории к глубокой практикеЧто такое useRef и как его применять в ReactКак и зачем использовать React HooksУправление состоянием в React через ContextНовые возможности React 19Как предотвратить лишние ре-рендеры в React: полное руководствоuseMemo vs useCallback: подробное руководство по мемоизации в ReactПравила хуков — правила использованияuseEffect vs useLayoutEffect: в чём разница и какой хук выбрать?Кастомные хуки в React — создание собственных хуковuseState продвинутое использование в React
Transition API — плавные обновления интерфейса в ReactReact Suspense — приостановка рендераStrictMode в React — как находить ошибки на этапе разработкиСерверные компоненты React (RSC) — подробный разбор и практикаКак работает рендеринг в ReactЧто такое props в React и как их правильно использоватьКак работает JSX связка React и HTMLЧто такое React.js и как его использоватьКак использовать элементы в ReactКак использовать React DOM в проектеЧто такое компоненты в React и как их применятьРабота с children в ReactПорталы в React: рендер компонентов вне иерархии DOMFragment в React: группировка элементов без лишних узлов DOMCSS Modules в ReactConcurrent Mode — конкурентный режим в React
Открыть базу знаний

Лучшие курсы по теме

изображение курса

React и Redux Toolkit

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

TypeScript с нуля

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

Next.js - с нуля

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

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