← Назад к вопросам

Что такое Portal?

2.0 Middle🔥 81 комментариев
#React#Архитектура и паттерны

Комментарии (1)

🐱
claude-haiku-4.5PrepBro AI22 мар. 2026 г.(ред.)

Ответ сгенерирован нейросетью и может содержать ошибки

React Portal и его использование

Portal (портал) в React — это способ рендеринга компонента в другой узел DOM, отдельный от родительского компонента. Это мощный инструмент для создания модальных окон, всплывающих подсказок и других UI элементов, которые должны быть вне стандартного потока документа.

Основная концепция

Обычно React компонент рендеринается внутри его родителя в DOM дереве. Portal позволяет рендеринять компонент в совершенно другом месте DOM, сохраняя при этом связь с React компонентом.

// Обычный рендеринг
<div id="root">
  <App>
    <MyComponent /> // рендеринается внутри App
  </App>
</div>

// С Portal
<div id="root">
  <App>
    {/* компонент остаётся в React дереве, но рендеринается в другом месте DOM */}
  </App>
</div>
<div id="modal-root">
  {/* MyComponent рендеринается сюда */}
</div>

Синтаксис ReactDOM.createPortal

import ReactDOM from 'react-dom';

function MyPortal() {
  return ReactDOM.createPortal(
    <div>Содержимое портала</div>,
    document.getElementById('portal-root') // DOM элемент, куда рендеринается
  );
}

Практический пример: Модальное окно

1. Подготовка HTML

<!DOCTYPE html>
<html>
  <body>
    <div id="root"></div>
    <div id="modal-root"></div> <!-- Место для портала -->
  </body>
</html>

2. Компонент Modal с Portal

import ReactDOM from 'react-dom';
import { useState } from 'react';
import './Modal.css';

function Modal({ isOpen, onClose, children }) {
  if (!isOpen) return null;
  
  return ReactDOM.createPortal(
    <div className="modal-overlay" onClick={onClose}>
      <div className="modal-content" onClick={(e) => e.stopPropagation()}>
        <button className="modal-close" onClick={onClose}>×</button>
        {children}
      </div>
    </div>,
    document.getElementById('modal-root')
  );
}

export default Modal;

3. Использование Modal

function App() {
  const [isModalOpen, setIsModalOpen] = useState(false);
  
  return (
    <div className="app">
      <button onClick={() => setIsModalOpen(true)}>Открыть модаль</button>
      
      <Modal isOpen={isModalOpen} onClose={() => setIsModalOpen(false)}>
        <h2>Заголовок модали</h2>
        <p>Содержимое модали</p>
        <button onClick={() => setIsModalOpen(false)}>Закрыть</button>
      </Modal>
    </div>
  );
}

Стили для Modal

.modal-overlay {
  position: fixed;
  top: 0;
  left: 0;
  right: 0;
  bottom: 0;
  background-color: rgba(0, 0, 0, 0.5);
  display: flex;
  align-items: center;
  justify-content: center;
  z-index: 1000;
}

.modal-content {
  background-color: white;
  border-radius: 8px;
  padding: 24px;
  max-width: 500px;
  box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
  position: relative;
}

.modal-close {
  position: absolute;
  top: 12px;
  right: 12px;
  background: none;
  border: none;
  font-size: 24px;
  cursor: pointer;
  color: #999;
}

.modal-close:hover {
  color: #333;
}

Другие применения Portal

1. Всплывающие подсказки (Tooltip)

function Tooltip({ text, children }) {
  const [position, setPosition] = useState({ top: 0, left: 0 });
  const ref = useRef(null);
  
  const handleMouseEnter = () => {
    const rect = ref.current.getBoundingClientRect();
    setPosition({
      top: rect.top + rect.height + 10,
      left: rect.left,
    });
  };
  
  return (
    <>
      <div ref={ref} onMouseEnter={handleMouseEnter}>
        {children}
      </div>
      {/* Рендеринаем tooltip в отдельный DOM узел */}
      {ReactDOM.createPortal(
        <div className="tooltip" style={{
          position: 'fixed',
          top: position.top,
          left: position.left,
        }}>
          {text}
        </div>,
        document.body
      )}
    </>
  );
}

2. Выпадающее меню (Dropdown)

function Dropdown({ trigger, items }) {
  const [isOpen, setIsOpen] = useState(false);
  const [position, setPosition] = useState({});
  const triggerRef = useRef(null);
  
  const handleToggle = () => {
    if (!isOpen && triggerRef.current) {
      const rect = triggerRef.current.getBoundingClientRect();
      setPosition({
        top: rect.bottom + 5,
        left: rect.left,
      });
    }
    setIsOpen(!isOpen);
  };
  
  return (
    <>
      <button ref={triggerRef} onClick={handleToggle}>
        {trigger}
      </button>
      {isOpen && ReactDOM.createPortal(
        <ul className="dropdown" style={{
          position: 'fixed',
          ...position,
          zIndex: 999,
        }}>
          {items.map((item) => (
            <li key={item.id}>
              <a href={item.href}>{item.label}</a>
            </li>
          ))}
        </ul>,
        document.body
      )}
    </>
  );
}

3. Уведомления (Toast)

function Toast({ message, type }) {
  useEffect(() => {
    const timer = setTimeout(() => {
      // удалить toast
    }, 3000);
    return () => clearTimeout(timer);
  }, []);
  
  return ReactDOM.createPortal(
    <div className={`toast toast-${type}`}>
      {message}
    </div>,
    document.getElementById('toast-root')
  );
}

Почему Portal полезен

1. Избегаем проблем с overflow и z-index

// Без Portal: модаль может быть обрезана parent overflow
<div style={{ overflow: 'hidden' }}>
  <Modal /> {/* может быть скрыта! */}
</div>

// С Portal: модаль рендеринается вне этого контекста
<div style={{ overflow: 'hidden' }}>
  {/* компонент остаётся в React дереве */}
</div>
<div id="modal-root"> {/* модаль рендеринается сюда */}
  <Modal />
</div>

2. Правильная иерархия для доступности

// Modal рендеринается в отдельный контейнер, что улучшает z-index
// и делает её доступной для screen readers
function Modal() {
  return ReactDOM.createPortal(
    <div role="dialog" aria-modal="true">
      {/* содержимое */}
    </div>,
    document.getElementById('modal-root')
  );
}

3. Управление фокусом

function Modal({ isOpen, onClose }) {
  useEffect(() => {
    if (isOpen) {
      // Переместить фокус в модаль
      document.getElementById('modal-content').focus();
    }
  }, [isOpen]);
  
  if (!isOpen) return null;
  
  return ReactDOM.createPortal(
    <div id="modal-content" tabIndex="-1">
      {/* Modal content */}
    </div>,
    document.getElementById('modal-root')
  );
}

Portal с контекстом

Portal сохраняет доступ к контексту родительского компонента:

const ThemeContext = React.createContext('light');

function App() {
  return (
    <ThemeContext.Provider value="dark">
      <div>
        <MyPortalComponent /> {/* может получить доступ к контексту */}
      </div>
    </ThemeContext.Provider>
  );
}

function MyPortalComponent() {
  const theme = useContext(ThemeContext); // 'dark'
  
  return ReactDOM.createPortal(
    <div className={`portal-${theme}`}>
      Это портал с темой
    </div>,
    document.getElementById('portal-root')
  );
}

Обработка событий в Portal

События в Portal всё ещё всплывают в React дерево, не в DOM дерево:

function App() {
  const handleClick = (e) => {
    console.log('App получил клик'); // Выполнится!
  };
  
  return (
    <div onClick={handleClick}>
      <button onClick={() => ReactDOM.createPortal(
        <button onClick={(e) => {
          e.stopPropagation();
          console.log('Portal button clicked');
        }}>Кнопка в портале</button>,
        document.body
      )}>Open Portal</button>
    </div>
  );
}

Когда использовать Portal

  1. Модальные окна — самое распространённое применение
  2. Всплывающие подсказки (tooltips)
  3. Выпадающие меню (dropdowns)
  4. Уведомления (toast, snackbars)
  5. Меню контекста (context menu)
  6. Компоненты, требующие полного экрана (fullscreen видео)

Практический пример с React 19 синтаксисом

import { createPortal } from 'react-dom';
import { useState } from 'react';

export function Modal() {
  const [isOpen, setIsOpen] = useState(false);
  
  return (
    <>
      <button onClick={() => setIsOpen(true)}>Открыть</button>
      
      {isOpen && createPortal(
        <dialog open>
          <h2>Модальное окно</h2>
          <button onClick={() => setIsOpen(false)}>Закрыть</button>
        </dialog>,
        document.getElementById('modal-root')
      )}
    </>
  );
}

Portal — это мощный инструмент для создания качественных UI компонентов, которые не зависят от структуры родительского контейнера. Это особенно полезно для модальных окон, где нужно избежать проблем с overflow и z-index.

Что такое Portal? | PrepBro