React 汎用モーダル - React hooks + react-modal で比較的お手軽に

## はじめに

React hooks を使ったローカル state でモーダルをコンポーネントにしてみました。react-modal がそもそもお手軽なのですが、コンポーネント化しておくと、まあ、それなりには汎用に使えるかと。

親コンテナ → 子コンポーネント（とオーバレイするモーダルコンポーネント）という使い方を想定しています。

## 材料（一人分）
```sh
npm install -S react-modal @material-ui/icons
```

もちろん React アプリであるのが前提です。モーダルのアイコンに @material-ui/icons 使っていますが、気に食わなければいくらでも置き換えて頂ければと。

実装例では Typescript で書いてはいますが、ほぼ依存してないので、Typescript 記法でなくてもかまいません。

## モーダルの実装
```Modal.tsx
//
//  Components/Modal - モーダル画面
//
import  *       as React                from 'react';
import  Modal                           from 'react-modal';
import  IconButton                      from '@material-ui/core/IconButton';
import  InfoCircle                      from '@material-ui/icons/InfoRounded';
import  CheckCircle                     from '@material-ui/icons/CheckCircle';
import  CancelRounded                   from '@material-ui/icons/CancelRounded';

// クリックイベントの定義
type            ClickEventHandlerType   =  ( ( ( e : React.MouseEvent ) => void ) | undefined )

// export Props - このコンポーネントのインターフェイスの定義
export interface        Props {

        app?            : string                        // モーダル表示すべきエレメントの ID。デフォルトは 'root'
        title?          : string                        // モーダル画面のタイトル
        text?           : string                        // モーダル画面の詳細テキスト
        onCancel?       : ClickEventHandlerType         // キャンセル時にハンドルされる関数
        onConfirm?      : ClickEventHandlerType         // 完了時にハンドルされる関数
}

//  export default - アラート表示用モーダル画面のコンポーネント
export default          React.memo( function ModalWindow( props : Props ) {

    // Props を展開する
    const               {
        app                     = 'root',
        title                   = '確認してください',
        text                    = undefined,
        onConfirm               = undefined,
        onCancel                = undefined
    }                   = props;

    // 指定されたエレメントを得る
    const           element         = document.getElementById( app );
    const           root            = element ? element : undefined;

    // モーダルが開いた時の処理
    const           afterOpen       = () => {
    };

    // 完了ボタンの定義
    const           confirmButton   = ( handler :  ClickEventHandlerType ) => {

        return (
            handler ?
                <IconButton onClick = { handler }>
                    <CheckCircle color = 'secondary' fontSize = 'large' />
                </IconButton>
            : null
        );
    };

    // キャンセルボタンの定義
    const           cancelButton    = ( handler : ClickEventHandlerType ) => {

        return(
            handler ?
                <IconButton onClick = { handler }>
                    <CancelRounded color = 'primary' fontSize = 'large' />
                </IconButton>
            : null
        );
    };

    // モーダル画面のスタイル
    const           styles          = {
        overlay         : {
            position            : 'fixed',
            top                 : 0,
            right               : 0,
            bottom              : 0,
            left                : 0,
            backgroundColor    : 'rgba( 0, 0, 0, .4 )',
            zIndex              : 10000,
        },
        content         : {
            position            : 'absolute',
            display             : 'block',
            textAlign           : 'center',
            backgroundColor     : 'whitesmoke',
            top                 : '50%',
            left                : '50%',
            right               : 'auto',
            bottom              : 'auto',
            width               : '50%',
            border              : '2px solid gray',
            transform           : 'translate( -50%, -50% )',
            outline             : 'transparent',
        }
    };
    
    // モーダル画面をレンダリングする
    return (

        <Modal
            appElement          = { root }
            isOpen              = { !! text }
            onRequestClose      = { onCancel }
            onAfterOpen         = { afterOpen }
            style               = { styles }
        >

            { /* モーダル画面の詳細を表示する */ }
            <h4> <InfoCircle color = 'secondary' fontSize = 'large' /> </h4>
            <h4> { title } </h4>
            <div> { text } </div>

            <br />

            { /* ボタンを表示する */ }
            { confirmButton( onConfirm ) }
            { cancelButton( onCancel ) }

        </Modal>
    )
} );
```

## 使い方
### 親コンテナ側

このモーダルは、親コンテナのローカル state で開閉を管理しています。

親コンテナで、子コンポーネント＋モーダルに渡る表示文字列をローカル state として管理します。この text はモーダルのテキストになります。テキストがあればモーダルが開きます。ローカル state の初期値は undefined なのでモーダルは閉じています

```js
const           [ text, openModal ]         = useState( undefined );
```

親コンテナでモーダルが閉じられた時のハンドラ onConfirm, onCancel を定義します。

### 子コンポーネント側

以下のように、モーダル画面を子コンポーネント内に定義し、モーダルとともにコンテンツをレンダリングしてください。

```js
import Modal from 'Modal';

...

return(
  <div>
    { /* 子コンポーネントのレンダリング内容 */ }
    <Content>
    ...
    </Content>

    { /* モーダル画面 */ }
    <Modal text = { text } app = { app } title = { title } onCancel = { onCancel } onConfirm = { onConfirm } />
  </div>
);
```

title   : モーダルに出すタイトルテキスト - 省略時は「確認してください」
text    : モーダルに出すテキスト - undefined なら、モーダルを閉じる
app     : モーダルを出すエレメントの ID - 省略時は 'root' (全画面)

### 親コンテナ側に戻って

モーダルを出す時は以下のようにします。

```js
openModal( 'モーダルに出すテキスト' );
```

モーダルを閉じる時、ダイアログ等なら、閉じるボタンのコールバック側 onClose, onConfirm などで行うのが通常かと思いますが、以下のようにローカル state の text を undefined にすると、モーダルが閉じますので、親コンポーネントで任意のタイミングで行ってください。


```js
const           onCancel        = () => {
    openModal( undefined );
};
```

モーダルコンポーネント内にスタイルも定義できますので、アラートや確認ダイアログだけでなく、「処理中表示」「詳細画面」など、いろいろ改造できるかと思います。

##注意

モーダルが開いても、その場でモーダルの処理は終了し、その他の処理は動作していることに留意してください。 同期が必要なら呼び出しを Promise なりを使って表示すべきです。