3.11 Порталы

Доступны с 16 версии.

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


Код
    
  ReactDOM.createPortal(child, container)
  

Первым аргументом (child) является любой отображаемый потомок React, такой как элемент, строка или фрагмент. Второй аргумент (container) является элементом DOM.


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

Как правило, когда вы возвращаете элемент из метода отрисовки компонента, он монтируется в DOM как дочерний элемент ближайшего родительского узла:


Код
    
  render() {
    // React монтирует новый div и отрисовывает в него потомок
    return (
      <div>
        {this.props.children}
      </div>
    );
  }
  

Однако иногда полезно вставлять дочерний элемент в другое место в DOM:


Код
    
  render() {
    // React не создаёт новый div. Он отрисовывает потомок в `domNode`.
    // `domNode` - это всегда валидный DOM-узел, независимо от его места в DOM.
    return ReactDOM.createPortal(
      this.props.children,
      domNode,
    );
  }
  

Типичный вариант использования порталов - это когда родительский компонент имеет overflow: hidden или z-index стиль, но вам нужно, чтобы дочерний компонент визуально «выходил» из своего контейнера. Например, диалоги, всплывающие подсказки.


Замечание!

Важно помнить, что при работе с порталами вам необходимо следить за тем, что вы следуете рекомендациям по общедоступности.

Попробовать в CodePen


3.11.2 Всплытие событий через порталы

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

Это же касается и всплытия события. Событие, созданное внутри портала, будет распространяться к предкам в объемлющем дереве React, даже если они не являются предками в дереве DOM. Представим следующую структуру HTML:


Код
    
  <html>
    <body>
      <div id="app-root"></div>
      <div id="modal-root"></div>
    </body>
  </html>
  

Компонент Parent в #app-root мог бы поймать неперехваченное всплывающее событие из соседнего узла #modal-root.


Код
    
  // Эти два контейнера являются соседями в DOM
  const appRoot = document.getElementById('app-root');
  const modalRoot = document.getElementById('modal-root');
  
  class Modal extends React.Component {
    constructor(props) {
      super(props);
      this.el = document.createElement('div');
    }
  
    componentDidMount() {
      /*
        Элемент портала вставлен в дерево DOM после того, как потомки Modal
      были монтированы, что означает, что потомки будут монтированы в отдельный
      узел DOM.
        Если дочерний компонент требует присоединения к дереву DOM сразу после
      его монтирования, например, для измерения узла DOM или использования
      «autoFocus» в потомке, добавьте состояние в Modal и отрисуйте дочерние
      элементы, после того, как Modal будет вставлен в DOM дерево.
      */
      modalRoot.appendChild(this.el);
    }
  
    componentWillUnmount() {
      modalRoot.removeChild(this.el);
    }
  
    render() {
      return ReactDOM.createPortal(
        this.props.children,
        this.el,
      );
    }
  }
  
  class Parent extends React.Component {
    constructor(props) {
      super(props);
      this.state = {clicks: 0};
      this.handleClick = this.handleClick.bind(this);
    }
  
    handleClick() {
      // Он сработает, когда кнопка в Child будет нажата,
      // обновляя состояние Parent, даже если кнопка
      // не является его прямым потомком в DOM.
      this.setState(prevState => ({
        clicks: prevState.clicks + 1
      }));
    }
  
    render() {
      return (
        <div onClick={this.handleClick}>
          <p>Число кликов: {this.state.clicks}</p>
          <p>
            Откройте DevTools браузера,
            чтобы увидеть, что кнопка button
            не является потомком div
            с обработчиком onClick.
          </p>
          <Modal>
            <Child />
          </Modal>
        </div>
      );
    }
  }
  
  function Child() {
    // Событие клика на этой кнопке будет всплывать к Parent,
    // так как нет заданного 'onClick' атрибута
    return (
      <div className="modal">
        <button>Click</button>
      </div>
    );
  }
  
  ReactDOM.render(<Parent />, appRoot);
  

Попробовать в CodePen

Захват события, всплывающего из портала в родительском компоненте, позволяет создавать более гибкие абстракции, которые по своей сути не зависят от порталов. Например, если вы отрисовываете компонент <Modal />, родитель может захватывать свои события независимо от того, реализован ли он с помощью порталов.