Что такое React Portals и для чего они используются?react-48

Определение

React Portals (порталы) — это специальный механизм в React, который позволяет рендерить дочерние элементы в DOM-узел, находящийся вне иерархии родительского компонента, сохраняя при этом все свойства React (контекст, пропсы, события).

Основная идея:

"Рендери здесь, но отображай там"


Когда использовать порталы?

  1. Модальные окна (Modal dialogs)
  2. Всплывающие подсказки (Tooltips)
  3. Тостер-уведомления (Toast notifications)
  4. Загрузочные индикаторы (Loaders)
  5. Любые UI-элементы, которые должны "всплывать" над основным интерфейсом

Базовый синтаксис

ReactDOM.createPortal(child, container)

Где:

  • child — React-элемент (JSX)
  • container — DOM-элемент, куда будет вставлен элемент

Пошаговая реализация

1. Создаём целевой DOM-элемент

Обычно добавляется прямо перед закрывающим тегом </body>:

// В public/index.html
<div id="modal-root"></div>

2. Создаём компонент с порталом

import ReactDOM from 'react-dom';

function Modal({ children }) {
  // Создаём ссылку на DOM-элемент
  const modalRoot = document.getElementById('modal-root');

  // Если элемента нет (для SSR), возвращаем null
  if (!modalRoot) return null;

  return ReactDOM.createPortal(
    <div className="modal">
      {children}
    </div>,
    modalRoot
  );
}

3. Используем компонент

function App() {
  const [isOpen, setIsOpen] = useState(false);

  return (
    <div>
      <button onClick={() => setIsOpen(true)}>
        Открыть модалку
      </button>

      {isOpen && (
        <Modal>
          <h2>Заголовок модалки</h2>
          <p>Содержимое модального окна</p>
          <button onClick={() => setIsOpen(false)}>
            Закрыть
          </button>
        </Modal>
      )}
    </div>
  );
}

Ключевые особенности

1. Сохранение контекста

Порталы сохраняют React-контекст, даже если рендерятся вне родительского дерева.

2. Обработка событий

События из портала всплывают по React-дереву, а не по DOM-дереву. Это значит, что событие клика внутри портала будет поймано родителем в React-компоненте.

3. Жизненный цикл

Компоненты в портале полностью управляются React, включая эффекты и очистку.


Практические примеры

1. Модальное окно с затемнением

function Modal({ children, onClose }) {
  const modalRoot = document.getElementById('modal-root');

  return ReactDOM.createPortal(
    <>
      <div className="modal-overlay" onClick={onClose} />
      <div className="modal-content">
        {children}
      </div>
    </>,
    modalRoot
  );
}

2. Кастомный dropdown

function DropdownMenu({ anchorEl, children }) {
  const menuRoot = document.getElementById('dropdown-root');
  const rect = anchorEl.getBoundingClientRect();

  return ReactDOM.createPortal(
    <div
      className="dropdown-menu"
      style={{
        position: 'absolute',
        top: rect.bottom,
        left: rect.left
      }}
    >
      {children}
    </div>,
    menuRoot
  );
}

Лучшие практики

  1. SSR-совместимость: Всегда проверяйте существование DOM-элемента
  2. Доступность: Добавляйте ARIA-атрибуты для модальных окон
  3. Управление фокусом: Реализуйте ловушку фокуса для модалок
  4. Анимации: Используйте CSS-анимации для плавного появления

Ограничения

  1. Не влияет на DOM-иерархию: Только меняет точку вставки
  2. Сложность позиционирования: Для сложных позиций нужны дополнительные расчеты
  3. Z-index войны: Требует аккуратного управления z-index

Альтернативы

  1. CSS-решения: position: fixed для простых случаев
  2. Библиотеки: react-modal, @reach/dialog
  3. Новые API: Popover API (экспериментальный)

Резюмируем

  1. Порталы позволяют рендерить контент вне основного DOM-дерева
  2. Сохраняют все преимущества React (контекст, события)
  3. Идеальны для модалок, тултипов и всплывающих окон
  4. Требуют создания целевого DOM-элемента
  5. Основные шаги использования:
    • Создать DOM-элемент (обычно в public/index.html)
    • Импортировать ReactDOM
    • Использовать createPortal(child, container)

Пример полной реализации модального окна:

import { useEffect } from 'react';
import ReactDOM from 'react-dom';

function Modal({ children, onClose }) {
  const modalRoot = document.getElementById('modal-root');

  useEffect(() => {
    const handleEscape = (e) => {
      if (e.key === 'Escape') onClose();
    };

    document.body.style.overflow = 'hidden';
    document.addEventListener('keydown', handleEscape);

    return () => {
      document.body.style.overflow = '';
      document.removeEventListener('keydown', handleEscape);
    };
  }, [onClose]);

  if (!modalRoot) return null;

  return ReactDOM.createPortal(
    <div
      className="modal"
      role="dialog"
      aria-modal="true"
    >
      <div className="modal-content">
        {children}
      </div>
    </div>,
    modalRoot
  );
}