Reactでの新しいポータル機能の使用
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が返すものと本質的に類似したオブジェクトを返します。
createPortal
はReactDOM
名前空間にあり、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
は、生成されたメッセージがレンダリングされる場所になります。
すべてのダイアログをアプリケーションの主要部分の上に視覚的に配置する必要があるため、.DialogHolder
div
ではz-index: 1
が宣言されています。 これにより、.PageHolder
のスタッキングコンテキストから独立した新しいスタッキングコンテキストが作成されます。
すべてのメッセージをダイアログの上に視覚的に配置する必要があるため、.MessageHolder
div
では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ポータルコンポーネントから親コンポーネントにバブルすることを示すために、.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
イベントハンドラーが呼び出されます(別のハンドラーが伝播を停止しなかった場合)。
上記のデモンストレーションの動作例を参照してください。
👉overflow: hidden
またはスタックコンテキストの問題が発生した場合は、Reactポータルを使用してください!