Хотите заказать веб-сайт? Связаться с нами

Порталы в React — телепорт для компонентов в любую точку DOM

Цель урока: понять механизм порталов как способа рендеринга React-компонентов за пределами родительского DOM-дерева. Научиться использовать порталы для решения задач, выходящих за рамки обычной иерархии: модальные окна, всплывающие подсказки, нотификации и любые другие сценарии, где нужно разорвать DOM-связь, но сохранить React-связь.

Порталы в React — телепорт для компонентов в любую точку DOM

Введение: зачем разрывать DOM-иерархию

В обычном React-приложении компоненты образуют строгое дерево: родитель → дочерний элемент. React монтирует дочерние компоненты внутрь DOM-элементов родителей. Это называется иерархическим рендерингом.

Проблема этого подхода

  1. Родительские CSS-стили (overflow: hidden, z-index, transform) могут «запирать» дочерние элементы
  2. Сложно создавать элементы, которые должны перекрывать родительские границы
  3. Некоторые компоненты логически принадлежат одному месту в React-дереве, но визуально должны отображаться в другом

Главная мысль: портал — это «телепорт». Ты оставляешь компонент в React-дереве (сохраняя контекст и события), но отправляешь его DOM-узел в любое другое место на странице.

Когда нужны порталы (не только «поверх всего»)

  1. Элементы, которые должны вырываться из CSS-ограничений родителя (overflow: hidden, z-index и т.д.)
  2. Компоненты, которые логически принадлежат одному месту в React-дереве, но физически должны быть в другом
  3. Интеграция с сторонними DOM-библиотеками (карты, графики)
  4. Визуальные компоненты, которые должны перекрывать родительские границы (модалки, тултипы, дропдауны, нотификации)

Суть проблемы: когда иерархия мешает

Пример проблемы с CSS:

function App() {
  return (
    <div style={{ overflow: 'hidden', position: 'relative' }}>
      {/* Родитель с overflow: hidden */}
      <div style={{ position: 'absolute', top: '100px', left: '100px' }}>
        {/* Дочерний элемент обрезается родителем */}
        <Tooltip text="Подсказка" />
      </div>
    </div>
  );
}

Решение через портал:

function Tooltip({ text }) {
  return ReactDOM.createPortal(
    <div style={{ position: 'fixed', top: '100px', left: '100px' }}>
      {text}
    </div>,
    document.body // Рендерим в body, минуя родителя
  );
}

ReactDOM.createPortal — синтаксис и принцип работы

Синтаксис:

ReactDOM.createPortal(children, domNode)

Важно: портал не меняет React-дерево:

  1. children остаются частью React-дерева
  2. Контекст продолжает работать
  3. События всплывают через React-дерево (не DOM-дерево!)
  4. Все хуки и жизненный цикл работают как обычно

Базовый пример:

import ReactDOM from 'react-dom';

function PortalExample() {
  return ReactDOM.createPortal(
    <div>
      <h3>Я в портале!</h3>
      <p>Я рендерюсь в другом DOM-узле, но остаюсь в React-дереве</p>
    </div>,
    document.getElementById('portal-root') // Любой DOM-узел
  );
}

Реальный сценарий: рендеринг вне родителя

Сценарий: компонент должен рендериться в конкретном месте DOM, независимо от того, где он используется в React-дереве.

function App() {
  return (
    <div>
      <Header />
      <Sidebar>
        {/* Этот компонент логически внутри Sidebar, но физически в другом месте */}
        <NotificationBadge count={5} />
      </Sidebar>
    </div>
  );
}

function NotificationBadge({ count }) {
  // Рендерим в специальный контейнер в хедере, хотя используем в сайдбаре
  return ReactDOM.createPortal(
    <span className="badge">{count}</span>,
    document.getElementById('header-badge-root')
  );
}
Из ноутбука появляются летающие светящиеся экраны

Оживи свой сайт. Освой JavaScript!

Статичная верстка — это только скелет. Наш онлайн-курс "JavaScript с нуля до профи" даст твоим страницам мышцы и нервы. Научись создавать слайдеры, формы, интерактивные карты и получать данные с сервера.

От теории — к реальным скриптам в твоём портфолио.

Подробнее о курсе

Портал для глобальных UI-элементов

Порталы идеально подходят для глобальных элементов, которые должны быть доступны из любого компонента.

Глобальный контейнер:

<!-- index.html -->
<body>
  <div id="root"></div>
  <div id="portal-root"></div> <!-- Общий контейнер для порталов -->
</body>

Использование:

function GlobalUI() {
  return ReactDOM.createPortal(
    <div>
      <Modal />
      <Tooltip />
      <Notifications />
    </div>,
    document.getElementById('portal-root')
  );
}

function App() {
  return (
    <ThemeProvider>
      <MainContent />
      <GlobalUI /> {/* Всё рендерится в portal-root */}
    </ThemeProvider>
  );
}

Портал и события (важное отличие)

Важно: события в портале всплывают через React-дерево, а не через DOM-дерево!

function Parent() {
  const handleClick = () => console.log('Клик на родителе');

  return (
    <div onClick={handleClick}>
      {/* Кнопка в Portal */}
      {ReactDOM.createPortal(
        <button onClick={() => console.log('Клик на кнопке')}>
          Кликни меня
        </button>,
        document.body
      )}
    </div>
  );
}
// Порядок: "Клик на кнопке" → "Клик на родителе"
// React-дерево: Parent → button (несмотря на DOM-позицию)

Портал с контекстом

Context работает через портал!

const ThemeContext = createContext('light');

function ThemedButton() {
  const theme = useContext(ThemeContext);
  
  // Рендерим в body, но контекст сохраняется
  return ReactDOM.createPortal(
    <button style={{ background: theme === 'light' ? '#fff' : '#333' }}>
      Кнопка с темой
    </button>,
    document.body
  );
}

function App() {
  const [theme, setTheme] = useState('light');

  return (
    <ThemeContext.Provider value={theme}>
      <ThemedButton />
    </ThemeContext.Provider>
  );
}
// Кнопка получает тему через контекст, хотя физически в body

Портал и SSR (Server Side Rendering)

При SSR нужно проверять существование DOM-узла:

function Portal({ children }) {
  const [mounted, setMounted] = useState(false);

  useEffect(() => {
    setMounted(true);
  }, []);

  if (!mounted) {
    // На сервере не рендерим portal
    return null;
  }

  const portalRoot = document.getElementById('portal-root');
  
  if (!portalRoot) {
    // Fallback, если контейнер отсутствует
    return <div className="fallback">{children}</div>;
  }

  return ReactDOM.createPortal(children, portalRoot);
}

Множественные порталы (разные контейнеры)

// Контейнеры в HTML
<div id="modal-root"></div>
<div id="tooltip-root"></div>
<div id="notification-root"></div>

function App() {
  return (
    <>
      {/* Модалки в одном контейнере */}
      {ReactDOM.createPortal(<Modal />, document.getElementById('modal-root'))}
      
      {/* Тултипы в другом */}
      {ReactDOM.createPortal(<Tooltip />, document.getElementById('tooltip-root'))}
      
      {/* Нотификации в третьем */}
      {ReactDOM.createPortal(<Notifications />, document.getElementById('notification-root'))}
    </>
  );
}

Частые ошибки новичков

Ошибка Почему Исправление
Не создают DOM-контейнер Ошибка «Target container is not a DOM element» Добавь <div id="portal-root"> в HTML
Пытаются использовать Portal на сервере Ошибка, т.к. DOM отсутствует Проверяй typeof window !== 'undefined'
Не учитывают всплытие событий События ведут себя неожиданно Помни: всплытие по React-дереву, не DOM
Используют Portal для всего Излишнее усложнение Используй только когда нужно разорвать DOM
Забывают про CSS-контекст Стили не применяются Portal сохраняет React-контекст, но не DOM-контекст

Итог (что мы узнали)

Что узнали Как это работает
Порталы Рендеринг в другую часть DOM
ReactDOM.createPortal Создание портала
Разрыв DOM-иерархии Компонент вне родительского DOM-узла
Сохранение React-дерева Контекст, события, хуки работают
Глобальные элементы Модалки, тултипы, нотификации
Множественные порталы Разные контейнеры для разных задач

Задание для закрепления (на 1–2 часа)

Создай два портал-компонента:

GlobalModal — модальное окно, которое рендерится в #modal-root:

  1. Открывается через хук useModal
  2. Поддерживает закрытие по Escape и клику на оверлей
  3. Блокирует скролл страницы

ContextualTooltip — тултип, который:

  1. Рендерится в #tooltip-root (отдельный контейнер)
  2. Появляется при наведении на элемент
  3. Использует контекст темы (светлая/тёмная) для стилизации

Дополнительно: создай контейнер #portal-root и компонент PortalProvider, который автоматически создаёт все необходимые DOM-контейнеры при монтировании.

Проверка: Убедись, что оба компонента используют разные DOM-контейнеры и работают независимо от родительских CSS-стилей.

Что дальше?

В следующем уроке разберём Error Boundaries — механизм отлова ошибок в React-компонентах. Ты узнаешь, как не дать ошибке «уронить» всё приложение, показывать красивые fallback-интерфейсы и логировать ошибки для отладки. Это спасёт твои приложения от краша на глазах у пользователей.

Теги: