React v16は、portalsと呼ばれる新機能を導入しました。 ドキュメントには次のように記載されています。

ポータルは、親コンポーネントのDOM階層の外部に存在するDOMノードに子をレンダリングするためのファーストクラスの方法を提供します。

通常、機能コンポーネントまたはクラスコンポーネントは、React要素のツリー(通常はJSXから生成されます)をレンダリングします。 React要素は、親コンポーネントのDOMがどのように見えるかを定義します。

v16より前では、レンダリングできる子タイプはごくわずかでした。

  • nullまたはfalse(何もレンダリングしないことを意味します)。
  • JSX。
  • 要素を反応させます。
function Example(props) {
  return null;
}
function Example(props) {
  return false;
}
function Example(props) {
  return <p>Some JSX</p>;
}
function Example(props) {
  return React.createElement(
    'p',
    null,
    'Hand coded'
  );
}

v16では、より多くの子タイプがレンダリング可能になりました。

  • 番号(InfinityおよびNaNを含む)。
  • 文字列。
  • ポータルに反応します。
  • レンダリング可能な子の配列。

レンダリング可能な子の完全なリスト

function Example(props) {
  return 42;  // Becomes a text node.
}
function Example(props) {
  return 'The meaning of life.';  // Becomes a text node.
}
function Example(props) {
  return ReactDOM.createPortal(
    // Any valid React child type
    [
      'A string',
      <p>Some JSX</p>,
      'etc'
    ],
    props.someDomNode
  );
}

Reactポータルは、ReactDOM.createPortalを呼び出すことで作成されます。 最初の引数はレンダリング可能な子である必要があります。 2番目の引数は、レンダリング可能な子がレンダリングされるDOMノードへの参照である必要があります。 ReactDOM.createPortalは、React.createElementが返すものと本質的に類似したオブジェクトを返します。

createPortalReactDOM名前空間にあり、createElementのようなReact名前空間にはないことに注意してください。

一部の注意深い読者は、ReactDOM.createPortalの署名がReactDOM.renderと同じであり、覚えやすいことに気付いたかもしれません。 ただし、ReactDOM.renderとは異なり、ReactDOM.createPortalは、調整プロセス中に使用されるレンダリング可能な子を返します。

いつ使用するか

Reactポータルは、親コンポーネントでoverflow: hiddenが宣言されているか、スタッキングコンテキストに影響するプロパティがあり、そのコンテナーから視覚的に「ブレークアウト」する必要がある場合に非常に便利です。 いくつかの例には、ダイアログ、グローバルメッセージ通知、ホバーカード、およびツールチップが含まれます。

ポータルを介したイベントバブリング

Reactのドキュメントはこれを非常によく説明しています。

ポータルはDOMツリーのどこにあってもかまいませんが、他のすべての点で通常のReactの子のように動作します。 ポータルはDOMツリー内の位置に関係なくReactツリーに存在するため、コンテキストなどの機能は、子がポータルであるかどうかに関係なくまったく同じように機能します。

これには、イベントのバブリングが含まれます。 ポータル内から発生したイベントは、それらの要素がDOMツリーの祖先でなくても、含まれているReactツリーの祖先に伝播します。

これにより、ダイアログやホバーカードなどのイベントを、親コンポーネントと同じDOMツリーでレンダリングされたかのように簡単に聞くことができます。

次の例では、Reactポータルとそのイベントバブリング機能を利用します。

マークアップは以下から始まります。

<div class="PageHolder">
</div>
<div class="DialogHolder  is-empty">
  <div class="Backdrop"></div>
</div>
<div class="MessageHolder">
</div>

.PageHolder divは、アプリケーションの主要部分が存在する場所です。 .DialogHolder divは、生成されたダイアログがレンダリングされる場所になります。 .MessageHolder divは、生成されたメッセージがレンダリングされる場所になります。

すべてのダイアログをアプリケーションの主要部分の上に視覚的に配置する必要があるため、.DialogHolderdivではz-index: 1が宣言されています。 これにより、.PageHolderのスタッキングコンテキストから独立した新しいスタッキングコンテキストが作成されます。

すべてのメッセージをダイアログの上に視覚的に配置する必要があるため、.MessageHolderdivではz-index: 1が宣言されています。 これにより、.DialogHolderのスタッキングコンテキストへの兄弟スタッキングコンテキストが作成されます。 兄弟スタッキングコンテキストのz-indexは同じ値ですが、DOMツリーで.MessageHolder.DialogHolderの後に来るため、これでも希望どおりにレンダリングされます。

次のCSSは、目的のスタッキングコンテキストを確立するために必要なルールをまとめたものです。

.PageHolder {
  /* Just use stacking context of parent element. */
  /* A z-index: 1 would still work here. */
}

.DialogHolder {
  position: fixed;
  top: 0; left: 0;
  right: 0; bottom: 0;
  z-index: 1;
}

.MessageHolder {
  position: fixed;
  top: 0; left: 0;
  width: 100%;
  z-index: 1;
}

この例には、.PageHolderにレンダリングされるPageコンポーネントが含まれます。

class Page extends React.Component { /* ... */ }

ReactDOM.render(
  <Page/>,
  document.querySelector('.PageHolder')
)

Pageコンポーネントはダイアログとメッセージをそれぞれ.DialogHolder.MessageHolderにレンダリングするため、これらのホルダーdivへの参照が必要になります。レンダリング時間。 いくつかのオプションがあります。

Pageコンポーネントをレンダリングする前に、これらのホルダーdivへの参照を解決し、それらをプロパティとしてPageコンポーネントに渡すことができます。

let dialogHolder = document.querySelector('.DialogHolder');
let messageHolder = document.querySelector('.MessageHolder');

ReactDOM.render(
  <Page dialogHolder={dialogHolder} messageHolder={messageHolder}/>,
  document.querySelector('.PageHolder')
);

セレクターをプロパティとしてPageコンポーネントに渡し、componentWillMountで最初のレンダリングの参照を解決し、セレクターが変更された場合はcomponentWillReceivePropsで再解決できます。

class Page extends React.Component {

  constructor(props) {
    super(props);
    let { dialogHolder = '.DialogHolder',
          messageHolder = '.MessageHolder' } = props

    this.state = {
      dialogHolder,
      messageHolder,
    }
  }

  componentWillMount() {
    let state = this.state,
        dialogHolder = state.dialogHolder,
        messageHolder = state.messageHolder

    this._resolvePortalRoots(dialogHolder, messageHolder);
  }

  componentWillReceiveProps(nextProps) {
    let props = this.props,
        dialogHolder = nextProps.dialogHolder,
        messageHolder = nextProps.messageHolder

    if (props.dialogHolder !== dialogHolder ||
        props.messageHolder !== messageHolder
    ) {
      this._resolvePortalRoots(dialogHolder, messageHolder);
    }
  }

  _resolvePortalRoots(dialogHolder, messageHolder) {
    if (typeof dialogHolder === 'string') {
      dialogHolder = document.querySelector(dialogHolder)
    }
    if (typeof messageHolder === 'string') {
      messageHolder = document.querySelector(messageHolder)
    }
    this.setState({
      dialogHolder,
      messageHolder,
    })
  }

}

ポータルのDOM参照があることを確認したので、ダイアログとメッセージを使用してPageコンポーネントをレンダリングできます。

React要素と同様に、Reactポータルはコンポーネントのプロパティと状態に基づいてレンダリングされます。 この例では、2つのボタンがあります。 1つは、クリックしたときにダイアログホルダーに表示されるダイアログポータルを作成し、もう1つは、メッセージホルダーに表示されるメッセージポータルを作成します。 これらのポータルへの参照は、renderメソッドで使用されるコンポーネントの状態のままにします。

class Page extends React.Component {
  // ...

  constructor(props) {
    super(props);
    let { dialogHolder = '.DialogHolder',
          messageHolder = '.MessageHolder' } = props

    this.state = {
      dialogHolder,
      dialogs: [],
      messageHolder,
      messages: [],
    }
  }

  render() {
    let state = this.state,
        dialogs = state.dialogs,
        messages = state.messages

    return (
      <div className="Page">
        <button onClick={evt => this.addNewDialog()}>
          Add Dialog
        </button>
        <button onClick={evt => this.addNewMessage()}>
          Add Message
        </button>
        {dialogs}
        {messages}
      </div>
    )
  }

  addNewDialog() {
    let dialog = ReactDOM.createPortal((
        <div className="Dialog">
          ...
        </div>
      ),
      this.state.dialogHolder
    )
    this.setState({
      dialogs: this.state.dialogs.concat(dialog),
    })
  }

  addNewMessage() {
    let message = ReactDOM.createPortal((
        <div className="Message">
          ...
        </div>
      ),
      this.state.messageHolder
    )
    this.setState({
      messages: this.state.messages.concat(message),
    })
  }


  // ...
}

イベントがReactポータルコンポーネントから親コンポーネントにバブルすることを示すために、.Pagedivにクリックハンドラーを追加しましょう。

class Page extends React.Component {
  // ...

  render() {
    let state = this.state,
        dialogs = state.dialogs,
        messages = state.messages

    return (
      <div className="Page" onClick={evt => this.onPageClick(evt)}>
        ...
      </div>
    )
  }

  onPageClick(evt) {
    console.log(`${evt.target.className} was clicked!`);
  }

  // ...
}

ダイアログまたはメッセージがクリックされると、onPageClickイベントハンドラーが呼び出されます(別のハンドラーが伝播を停止しなかった場合)。

上記のデモンストレーションの動作例を参照してください。

👉overflow: hiddenまたはスタックコンテキストの問題が発生した場合は、Reactポータルを使用してください!