Портали

Портали дозволяють рендерити дочірні елементи в 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
          з обробником onClick.
        </p>
        <Modal>
          <Child />
        </Modal>
      </div>
    );
  }
}

function Child() {
  // Подія натискання на цю кнопку буде спливати вгору до батьківського елемента,
  // тому що не визначено атрибут "onClick"
  return (
    <div className="modal">
      <button>Натисніть</button>
    </div>
  );
}

ReactDOM.render(<Parent />, appRoot);

Спробувати на CodePen

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