開発者ドキュメント

Reactでの新しいポータル機能の使用

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

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

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

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

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では、より多くの子タイプがレンダリング可能になりました。

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

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が返すものと本質的に類似したオブジェクトを返します。

ご了承ください createPortal の中に ReactDOM 名前空間ではなく React のような名前空間 createElement.

一部の注意深い読者は、 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>

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

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

すべてのメッセージをダイアログの上に視覚的に配置する必要があるため、 .MessageHolder div もっている z-index: 1 宣言した。 これにより、兄弟スタッキングコンテキストが作成されます。 .DialogHolderのスタッキングコンテキスト。 でも z-index 兄弟スタッキングコンテキストのいくつかは同じ値を持っていますが、これは、次の事実のために、私たちが望むようにレンダリングします .MessageHolder 後に来ます .DialogHolder DOMツリーで。

次の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;
}

例には Page にレンダリングされるコンポーネント .PageHolder.

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

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

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

これらの所有者への参照を解決できます divsレンダリングする前に Page コンポーネントを作成し、それらをプロパティとして 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参照があることを確認したので、ダイアログとメッセージを使用してページコンポーネントをレンダリングできます。

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ポータルコンポーネントから親コンポーネントにバブルすることを示すために、クリックハンドラーを追加しましょう。 .Page div.

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 イベントハンドラーが呼び出されます(別のハンドラーが伝播を停止しなかった場合)。

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

👉遭遇したときにReactポータルを使用する overflow: hidden またはコンテキストの問題を積み重ねます!

モバイルバージョンを終了