【React】React PortalとSemantic UIで5分でモーダルをつくる

React Portalの学習のために超簡単なモーダルを作成しました。
モーダルのスタイルにはSemantic UI Reactを使用しました。

https://react.semantic-ui.com/

#React Portalとは
React Portalは、コンポーネントをレンダリングする際にレンダリング先のDOMを事前に選んで、親コンポーネントの外側にレンダリングできるようにするための機能です。

Reactのコンポーネントの通常の構成だと、`index.html`のbody要素内のdiv(id='root')から何個もネストしたところにモーダル(Modal)を表示させたいコンポーネント(StreamDelete)が存在しています。
しかし、モーダルのように子要素が親要素から飛び出してみえる必要がある場合、モーダルがネストしたdivの内部にあるとstyleの設定が非常にめんどくさくなります。
しかも、styleをうまく設定できたつもりでも、祖先の要素の表示になにかしら影響を与えてしまう可能性が高いです。
<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/569054/0139d0f9-208d-79e2-cb72-a191c8184f6c.png" width="700">

一方、React Portalを使うと、指定したコンポーネント(Modal)を他の要素(div(id='modal'))の子要素としてレンダリングすることができるようになります。
Portalを利用することでModalはdiv(id='modal')のDOMツリーに組み込まれますが、Reactツリーは以前のまま変わりません。
そのため、Modalで発火したイベントはReactツリーの祖先(Modal->StreamDelete->...)に伝搬していきます。
Contextのような機能についても子要素がPortalであろうと問題なく使うことができます。
<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/569054/e3e4514c-39fd-51d8-0ff1-79a7c7fdccad.png" width="720">

https://qiita.com/suzuki0430/items/6e5dec67eaf6f5faf56f

#Modalの作成
簡単にModalを作成してみます。
まずはModalの中身を以下のようにします。

```jsx:Modal.js
import React from 'react';
import ReactDOM from 'react-dom';

const Modal = props => {
  return ReactDOM.createPortal(
    <div className="ui dimmer modals visible active">
      <div className="ui standard modal visible active">
        React Portalのお勉強
      </div>
    </div>,
    document.querySelector('#modal')
  );
};

export default Modal;
```

Modalの中身は`ReactDOM.createPortal(child, container)`のような構成になっています。
第1引数(child)にはReactの子要素としてレンダー可能なもの(要素、文字列、フラグメント)、第2引数(container)にはDOM要素を指定します。

Modal内で`id='modal'`のdiv要素を指定したので、`index.html`のbody要素に`<div id="modal"></div>`を追加します。

```html:index.html
  <body>
    <div id="root"></div>
    <div id="modal"></div>
```
Modal自体はReactツリーにおける親要素(StreamDelete)に配置します。

```jsx
import React from 'react';
import Modal from '../Modal';

const StreamDelete = () => {
  return (
    <div>
      StreamDelete
      <Modal />
    </div>
  );
};

export default StreamDelete;
```
該当のRouteに飛んでみるとModalが以下のように飛び出して表示されます。
![スクリーンショット 2021-05-18 7.49.25.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/569054/83e21551-f5be-55ca-a782-cc18334f3343.png)

##Modalの見栄え改善
以下のように、ModalにヘッダーやDelete/Cancelボタンを加えてみます。
![スクリーンショット 2021-05-19 7.51.17.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/569054/fda0ff32-b506-69f8-3158-788e80f3e845.png)

```jsx:Modal.js
const Modal = props => {
  return ReactDOM.createPortal(
    <div 
      onClick={props.onDismiss}
      className="ui dimmer modals visible active"
    >
      <div onClick={(e) => e.stopPropagation()} className="ui standard modal visible active">
        <div className="header">{props.title}</div>
        <div className="content">{props.content}</div>
        <div className="actions">{props.actions}</div>
      </div>
    </div>,
    document.querySelector('#modal')
  );
};
```
Semantic UIを使うと、classNameにheaderやcontentを指定するだけでModalのレイアウトを形成してくれます。
また、Modalの再利用を考え、表示内容や発火イベントはModalに依存しないようにpropsから読込んでいます。

`props.onDismiss`では`() => history.push('/')`を読み込んでおり、画面をクリックしたときに前にいた画面を履歴に追加し、ブラウザの戻るボタンで戻れるようにしています。
ただ、Modalの表示部分をクリックしたときに画面が遷移しないよう、内部のdivで`(e) => e.stopPropagation()`としています。

親要素のStreamDeleteでは、以下のようにModalに値やイベントを渡しています。

```jsx
      <Modal 
        title='Delete Stream' 
        content={
          !props.stream ? 
          'Are you sure you want to delete this stream?' 
          : `Are you sure you want to delete this stream with title: ${props.stream.title}?`
        }
        actions={<Actions id={id} deleteStream={props.deleteStream}/>}
        onDismiss={() => history.push('/')}
      />
```

#参考資料

https://ja.reactjs.org/docs/portals.html

https://qiita.com/official-events/b9ad63394fa2635dfca9