Перейти к содержанию

Порталы

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

ReactDOM.createPortal(child, container)

Первый аргумент (child) — это любой React-компонент, который может быть отрендерен, такой как элемент, строка или фрагмент. Следующий аргумент (container) — это DOM-элемент.

Применение

Обычно, когда вы возвращаете элемент из рендер-метода компонента, он монтируется в 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, но вам нужно чтобы дочерний элемент визуально выходил за рамки своего контейнера. Например, диалоги, всплывающие карточки и всплывающие подсказки.

Примечание:

При работе с порталами, помните, что нужно уделить внимание управлению фокусом при помощи клавиатуры.

Для модальных диалогов, убедитесь, что любой пользователь будет способен взаимодействовать с ними, следуя практикам разработки модальных окон WAI-ARIA.

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

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

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

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

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

Родительский компонент в #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(state => ({
      clicks: state.clicks + 1
    }))
  }

  render() {
    return (
      <div onClick={this.handleClick}>
        <p>Количество кликов: {this.state.clicks}</p>
        <p>Откройте DevTools браузера, чтобы убедиться, что кнопка не является потомком блока div c обработчиком onClick.</p>
        <Modal>
          <Child />
        </Modal>
      </div>
    )
  }
}

function Child() {
  // Событие клика на этой кнопке будет всплывать вверх к родителю,
  // так как здесь не определён атрибут 'onClick'
  return (
    <div className="modal">
      <button>Кликните</button>
    </div>
  )
}

ReactDOM.render(<Parent />, appRoot)

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

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