0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

React.jsで再利用可能なモーダルのコンポーネントを作成する方法

Posted at

個人開発中、海外の記事を参考に以下のような特徴を持つモーダルコンポーネントを実装しました。

  • 機能の追加やカスタマイズ性の高い再利用可能なコンポーネント
  • escキー押下など様々なイベントで閉じることが可能
  • CSSのテンプレあり 
    ......など

かなり使いやすいものになっていると思うので、React.jsを用いてモーダルの実装がしたいと考えている方はぜひこの記事のコードを検討してみてください。

参考にした記事

この投稿は以下の記事を参考にしています。

それでは解説していきます。

モーダルコンポーネントを作成する

コード全文

汎用性の高いモーダルの例文コードは以下のようになります。
解説が不要な方はこちらをコピペしてカスタマイズしていただいて構いません。

import React, {useEffect} from 'react';
import './Modal.css'

interface Props {
    open: boolean;
    cancelFn?: () => void;
    primaryFn?: () => void;
    secondaryFn?: () => void;
    closeIcon?: string;
    content?: React.ReactNode;
    titleContent?: React.ReactNode;
    className?: string;
}

export const Modal: React.FC<Props> = (props) => {
const {open, cancelFn, primaryFn, secondaryFn, closeIcon, titleContent, content} = props;

    // simple useEffect to capture ESC key to close the modal 
    useEffect(() => {
        const handleKeyDown = (e: KeyboardEvent) => {
            if (e.key === 'Escape' && open) {
                if (cancelFn) {
                    cancelFn();
                }
            }
        };

        document.addEventListener('keydown', handleKeyDown);
        return () => document.removeEventListener('keydown', handleKeyDown);
    }, [open, cancelFn]);


    if (!open) return null;

    return (
        <div className="modalBackground">
            <div className="modalContainer">
                {titleContent && (<div className="title">
                        {titleContent}
                        <div className="titleCloseBtn">
                            <button onClick={cancelFn}>{closeIcon ?? 'X'}</button>
                        </div>
                    </div>
                )}

                <div className="body">
                    {content}
                </div>

                <div className="footer">
                    {secondaryFn && (
                        <button onClick={secondaryFn} id="cancelBtn">
                            Cancel
                        </button>
                    )}
                    {primaryFn && (
                        <button onClick={primaryFn}>Continue</button>
                    )}
                </div>
            </div>
        </div>

    );
};

各ブロックの解説

要点を解説していきます

import React, {useEffect} from 'react';
import './Modal.css'

最初のImportの部分は、コード内で使われるReact hookと後述するCSSです。
筆者は管理できる値が欲しかったので、この他にuseRefなどをimportして使いました。

interface Props {
    open: boolean;
    cancelFn?: () => void;
    primaryFn?: () => void;
    secondaryFn?: () => void;
    closeIcon?: string;
    content?: React.ReactNode;
    titleContent?: React.ReactNode;
    className?: string;
}

export const Modal: React.FC<Props> = (props) => {
    const {open, cancelFn, primaryFn, secondaryFn, closeIcon, titleContent, content} = props;

// 省略

ここでpropsの設定を行います。
親から渡されるデータを設定する部分であり、モーダルコンポーネントが呼び出される場面によって開発者が変化させる部分になります。

たとえばtitleContentをpropsとすることによって、呼び出し先のコンポーネント別にわかりやすくタイトルをつけられるといった具合です。

例文コードが全て使われるわけではなく、開発したいアプリに合わせて必要な機能を実現できるように足したり消したりとカスタマイズしてください。
筆者の場合、open, cancelFn, titleContent, primaryFnを残し、残りを削除しました。

例文において、prymaryFnはモーダルのContinueボタンに使われる機能を担当しています。

// simple useEffect to capture ESC key to close the modal 
useEffect(() => {
    const handleKeyDown = (e: KeyboardEvent) => {
        if (e.key === 'Escape' && open) {
            if (cancelFn) {
                cancelFn();
            }
        }
    };

    document.addEventListener('keydown', handleKeyDown);
    return () => document.removeEventListener('keydown', handleKeyDown);
}, [open, cancelFn]);

if (!open) return null;

キーボードのescキーが押されたタイミングで発火する、モーダルを閉じる処理です。
KeyboardEvent型はブラウザが提供してくれるイベントオブジェクトになります。

cancelFnはpropsなので別途実装する必要があります。

その下のif文はみたままですが、openがtrueじゃない時にモーダルを表示させないためのものです。

 return (
        <div className="modalBackground">
            <div className="modalContainer">
                {titleContent && (<div className="title">
                        {titleContent}
                        <div className="titleCloseBtn">
                            <button onClick={cancelFn}>{closeIcon ?? 'X'}</button>
                        </div>
                    </div>
                )}

                <div className="body">
                    {content}
                </div>

                <div className="footer">
                    {secondaryFn && (
                        <button onClick={secondaryFn} id="cancelBtn">
                            Cancel
                        </button>
                    )}
                    {primaryFn && (
                        <button onClick={primaryFn}>Continue</button>
                    )}
                </div>
            </div>
        </div>

こちらがモーダルのUIを決定するJSXになります。
titleContentやcloseIcon,contentなどpropsで渡されていたものが条件式に使われており、呼び出し先によるカスタム性の幅が広がっています。

&&は短絡評価と呼ばれるもので、左側が真の場合のみ右側を評価します。
titleContentを例にとると、タイトルが存在する場合のみtitleContentを表示するということになります。具体例を見ると簡単ですね。

筆者はキャンセルボタンはそのままキャンセルで良かったので、SecondaryFnをcancelFnにしました。

CSSのテンプレ

以下、上記モーダルで読み込んでいるテンプレのCSSになります。
同じディレクトリ内にModal.cssファイルを作成してコピペしてください。
もちろん自由にカスタムしていただいて結構です。

.modalBackground {
    width: 100vw;
    height: 100vh;
    background-color: rgb(33, 33, 33, 0.9);
    inset: 0;
    z-index: 9999;         
    position: fixed;
    display: flex;
    justify-content: center;
    align-items: center;
}

.modalContainer {
    display: flex;
    flex-direction: column;
    border-radius: 20px;
    background-color: white;
    box-shadow: rgba(0, 0, 0, 0.35) 0px 5px 15px;

}

.modalContainer .title {
    display: flex;
    flex-direction: row;
    text-align: center;
    align-items: center;
    justify-content: space-between;
    padding: 8px;
    border-top-right-radius: 20px;
    border-top-left-radius: 20px;
    background-color: #FFE936;
}

.titleCloseBtn {
    display: flex;
    justify-content: flex-end;
}

.titleCloseBtn button {
    font-size: 1rem;
}

.titleCloseBtn button {
    background-color: transparent;
    border: none;
    font-size: 25px;
    cursor: pointer;
}

.modalContainer .body {
    flex: 1;
    padding: 16px;
    display: flex;
    flex-direction: column;
    justify-content: center;
    align-items: center;
    font-size: 1rem;
    text-align: center;
}

.modalContainer .footer {
    display: flex;
    justify-content: center;
    align-items: center;
}

.modalContainer .footer button {
    width: 150px;
    height: 45px;
    margin: 10px;
    border: none;
    background-color: cornflowerblue;
    color: white;
    border-radius: 8px;
    font-size: 20px;
    cursor: pointer;
}

#cancelBtn {
    background-color: crimson;
}

親コンポーネントから呼び出す

通常親コンポーネントから子コンポーネントを呼び出す方法と変わりませんが、一応呼び出し方法を書いておきます。
必要のない方は読み飛ばしてください。

// Parent.tsx
export default function Parent({ hoge }: Props) {


    const [isModalOpen, setIsModalOpen] = useState(false)

    const openModalButton = () => setIsModalOpen(true)

    const closeModal = () => setIsModalOpen(false)

return (
    // 省略
        <button
            onClick={() => openModalButton()}
        >
                モーダル表示ボタン
        </button>

    // 省略

    {isModalOpen && 
    <Modal 
        open={isModalOpen}
        cancelFn={closeModal}
        titleContent={"hogehoge"}
        // 用途に合わせて変更してください
    />}
)

    // 省略

筆者コードを抜粋して多少書き加えただけのものですので、上記を参考に書き換えてください。
不安な方はpropsを用いて親から子へデータを渡す方法を調べてもらえるとスッと入ってくると思います。

最後に

いかがだったでしょうか?
つよつよエンジニアを目指して奮闘するよわよわの筆者ですが、ネット上にある汎用性の高いコードを見ることで学ぶことはとても多いなと感じているこの頃です。いずれは僕も、1から便利な再利用性の高いコードを書いて公開できたらいいな。

0
0
3

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?