ツクログネット

【TypeScript + React Hooks】使い易いモーダルウィンドウのコンポーネントを作成する

eye catch

使い易いモーダルウィンドウのコンポーネントを作成します。

useModalフックを作成する

まずは、Modalコンポーネントのロジック部分であるカスタムフックを作成します。名前はuseModalとします。

import React, { useState, useEffect } from "react";

const useModal = (initialShow: boolean): [boolean, () => void] => {
	const [show, setShow] = useState(initialShow);

	const closeModal = () => setShow(false);

	useEffect(() => {
		setShow(initialShow);
	}, [initialShow]);

	return [show, closeModal];
};

export default useModal;

useModalフックは、引数としてinitialShowを受け取り、showとcloseModalを返します。

initialShowは、モーダルウィンドウの初期状態を示す真偽値です。trueが指定されると初めから表示され、falseが指定されると初めは非表示となるようにします。

そしてinitialShowはshowステートの初期値に設定します。

また、useEffectフックを用いて、initialShowが変化したときにその都度

showは、モーダルウィンドウの現在の表示・非表示の状態を示すステートであり、この値の変化によってモーダルウィンドウの表示・非表示が切り替わるようにします。

closeModal関数はモーダルウィンドウを非表示にする関数であり、setShowによってshowをfalseに切り替えます。

Modalコンポーネントを作成する

次に、Modalコンポーネントを以下のように作成します。

import React from "react";
import Button from "./Button";
import useModal from "../hooks/useModal";

type ModalComponentProps = {
	className?: string;
	initialShow?: boolean;
	showCloseButton?: boolean;
	closableOverlay?: boolean;
};

const StyledModal = styled.div`
	background-color: #fff;
	border-radius: .4em;
	display: flex;
	justify-content: center;
	align-items: center;
	padding: 1em;
	position: relative;
	min-height: 14em;
	min-width: 20em;

	& .close-button {
		border-radius: .7em;
		display: block;
		height: 1.4em;
		padding: 0;
		position: absolute;
		top: -.5em;
		right: -.5em;
		width: 1.4em;
	}
`;

const Overlay = styled.div`
	background-color: rgba(0, 0, 0, .5);
	display: flex;
	align-items: center;
	justify-content: center;
	height: 100%;
	position: fixed;
	top: 0;
	left: 0;
	width: 100%;
	z-index: 2000;
`;

const ModalContent = styled.p`
	text-align: center;
`;

const Modal = ({ className, initialShow=true, showCloseButton=true, closableOverlay=true, children }: ModalComponentProps) => {
	const [show, closeModal] = useModal(initialShow);

	return show ? (
		<Overlay {...(closableOverlay && {onClick: closeModal})}>
			<StyledModal className={className} onClick={e => e.stopPropagation()}>
				<ModalContent>{children}</ModalContent>
				{showCloseButton && <Button
					className="close-button"
					onClick={closeModal}
				>×</Button>}
			</StyledModal>
		</Overlay>
	) : null;
};

export default Modal;

まずModalコンポーネントは、propsとして初期状態の表示、非表示を示すinitialShow、閉じるボタンの表示、非表示を表すshowCloseButton、そしてモーダルウィンドウのコンテンツとなるchildrenを受け取るようにしています。

次に、コンポーネント内で先ほど作成したuseModalフックを呼び出します。その際、propsで受け取ったinitialShowを引数として渡します。

Modalコンポーネントが返すReact要素は、show(=initialShow)がtrueであるときに表示されるようにします。そのため、React要素を返す際は三項演算子を用いてshowがtrueであればReact要素を返し、falseであればnullを返して何も表示されないようにしています。

showがtrueのときに返すReact要素は、モーダルウィンドウを構成する各コンポーネントOverlay、StyledModal、ModalContent、Buttonで構成されています。

Overlayコンポーネントはモーダルウィンドウのオーバーレイを表示するためのコンポーネントです。

StyledModalはモーダル部分を表すコンポーネントであり、ModalContentとButtonで構成されています。

ModalContentはモーダルのコンテンツ部分を表すコンポーネントであり、propsとして受け取ったchildrenを子要素に持たせています。

Buttonはボタンを表すコンポーネントであり、ここではモーダルウィンドウを閉じるボタンとして使用しています。 ※Buttonコンポーネントについてはを参照してください。

モーダルウィンドウを閉じるボタンは、デフォルトではモーダルウィンドウの右上にバツ印で表示されるようにしていますが、中には「右上には表示したくない!」という方や、「コンテンツ内に表示したい!」という方、そして「バツ印ではなく「close」のような独自のボタン名を表示したいという方もおられるのでは?と考え、右上の閉じるボタンについては論理積演算子&&を利用してshowCloseButtonにtrueが指定されたときのみ表示されるようにしています。

また、モーダルウィンドウは通常、閉じるボタンをクリックする以外にもオーバーレイをクリックしたときにも消える仕組みになっています。 しかし、年齢確認画面のような「はい」か「いいえ」のどちらかを選択しなければ消えないような画面を作る場合は、それでは困ります。 そこで、今回作成するModalコンポーネントでは、closableOverlayがtrueのときのみonClick属性にcloseModalが指定され、オーバーレイのクリックによるモーダルウィンドウのクローズが有効となるようにしています。

ですが、それだけではいけません。何故なら、closableOverlayにtrueが指定され、オーバーレイにイベントハンドラが追加された場合、オーバーレイの子要素であるコンテンツがクリックされたときにclickイベントが発生しますが、そのイベントはバブリングによって親要素のオーバーレイにまで伝わることによって、オーバーレイに登録されているイベントリスナーcloseModalが反応してしまうからです。 そうなると、オーバーレイをクリックしていないにも関わらず、モーダルウィンドウが消えてしまうのです。 これを防ぐために、StyledModalコンポーネントのclick属性にe => e.stopPropagation()を指定して、中身のコンテンツがクリックされたときにclickイベントが親であるオーバーレイに伝わらないようにしています。

Modalコンポーネントの使用例

以下は、作成したModalコンポーネントをAppコンポーネント内で使用した例です。

import React from "react";
import Modal from "./components/Modal";

const App = () => {
	return (
		<div>
			<Modal className="">
					<h1>Title</h1>
					<p>Text</p>
				</Modal>
		</div>
	);
};

Demo