【React】Portal+Context+Hooks でモーダルダイアログを簡単に扱う



# Portal とは

`ReactDOM.createPortal()`は、コンポーネント階層の外側(=#root と同じ階層)の DOM 要素にコンポーネントを描画するメソッドです。
これによってコンポーネント階層内での z-index を気にすることなくモーダルダイアログを表示することが可能になります。
開閉の制御がバケツリレーになることを避けるためと、記述を簡潔にするために Context と Hooks を利用してできるだけ簡単に扱えるモーダルダイアログを実装してみます。
（わかりやすい見た目にするために SemanticUI を使用しているので実際に実行する場合は `<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/semantic-ui/2.4.1/semantic.min.css"/>` を追加してください）

# 実装
## ルートノード
モーダル用のルートノードを追加します。

```index.html
<!-- 省略 -->
<div id="modal"></div>
<!-- 省略 -->
```
## Context
`useState()`で使える形で Context を定義します。

```isModalShown.js
import React from 'react';

export default React.createContext({
  isModalShown: false, // モーダル開閉ステータス
  toggleModalShown: () => {}, // ステータス更新関数
});

```
## モーダルダイアログ本体
Hooks を使わない場合と比べるとだいぶすっきり書けます。
タイトルやテキストは props で渡すべきですが今回は省略します。

```Modal.jsx
import React, { useContext } from 'react';
import ReactDOM from 'react-dom';
import isModalShown from '../contexts/isModalShown';

const Modal = () => {
  // 背景とCloseボタンのクリックで開閉ステータスをfalseにする
  const renderModal = modalHookState => {
    const [isModalShown, changeShownModal] = modalHookState;
    return (
      <div
        onClick={() => changeShownModal(false)}
        className="ui dimmer modals visible active"
      >
        <div
          onClick={e => e.stopPropagation()}
          className="ui standard modal visible active"
        >
          <div className="ui header">Modal Dialog</div>
          <div className="content">This is modal dialog with Context</div>
          <div className="actions">
            <button
              className="ui button primary"
              onClick={() => changeShownModal(false)}
            >
              Close
            </button>
          </div>
        </div>
      </div>
    );
  };

  // Contextの値を取得して開閉制御
  const modalHookState = useContext(isModalShown);
  return ReactDOM.createPortal(
    modalHookState[0] && renderModal(modalHookState),
    document.querySelector('#modal'),
  );
};

export default Modal;


```

## モーダルを呼ぶ側のコンポーネント
モーダル開閉のステータスを更新する関数をonClickにバインドします。

```CallModal.jsx
import React, { useContext } from 'react';
import isModalShown from '../contexts/isModalShown';
import Modal from './Modal';

const CallModal = () => {
  // Open Modalボタンクリックで開閉ステータスをtrueにする
  const renderCallModal = changeShownModal => {
    return (
      <div>
        <button onClick={() => changeShownModal(true)}>Open Modal</button>
      </div>
    );
  };

  return (
    <div>
      {renderCallModal(useContext(isModalShown)[1])}
      <Modal />
    </div>
  );
};

export default CallModal;


```
## Provider
モーダル開閉のステータスと、変更するための関数を Provider で渡します。

```App.jsx
import React from 'react';
import isModalShown from '../contexts/isModalShown';
import CallModal from './CallModal';

const App = () => {
  // modalState...[isModalShown, toggleModalShown]
  const modalState = React.useState(false);
  return (
    <isModalShown.Provider value={modalState}>
      <CallModal />
    </isModalShown.Provider>
  );
};

export default App;


```

## 結果
![画面収録 2019-04-01 20.37.38.mov.gif](https://qiita-image-store.s3.amazonaws.com/0/258226/a84bddc2-488c-3539-1298-24c9bcc4e2cb.gif)
バケツリレーを避けることで、Modalまでのコンポーネントが深くネストした時の再利用性や独立性を高めることができました。
表示/非表示を切り替えるときは必ずContextの値を変更すればいいので、扱いやすくもあると思います。

## 参考URL
https://reactjs.org/docs/context.html
https://reactjs.org/docs/hooks-reference.html