作るもの
以下の機能をもったモーダルの土台的な部分です。
- スクロールロック
- モーダルオープン時、クローズ時に関数を渡すことで処理を追加できる
- モーダル表示中は薄暗い透明の背景を表示
- 透明部分をクリックしてもモーダルを閉じるようにする
使用技術とか
- TypeScript
- React
- TailwindCSS
- body-scroll-lock
コード全体
import React, { useEffect, useRef, useState } from "react"
import {
disableBodyScroll,
enableBodyScroll,
clearAllBodyScrollLocks,
} from "body-scroll-lock";
type WrapperFn = <T extends readonly unknown[]>(fn?: (...args: T) => void, ...args: T) => void;
type ModalProps = {
children: React.ReactNode;
};
type Return = {
Modal: React.FC<ModalProps>;
openModal: WrapperFn;
closeModal: WrapperFn;
};
export const useModal = (): Return => {
const [isVisible, setIsVisible] = useState<boolean>(false);
const modalRef = useRef<HTMLDivElement>(null);
useEffect(() => {
if (modalRef.current === null) return;
if (isVisible) {
disableBodyScroll(modalRef.current);
} else {
enableBodyScroll(modalRef.current);
}
return () => clearAllBodyScrollLocks();
}, [isVisible, modalRef]);
const openModal: WrapperFn = (onOpen, ...args) => {
if (onOpen) onOpen(...args);
setIsVisible(true);
};
const closeModal: WrapperFn = (onClose, ...args) => {
if (onClose) onClose(...args);
setIsVisible(false);
};
const Modal: React.FC<ModalProps> = ({ children }) => {
const handleOverlayClick = (e: React.MouseEvent<HTMLDivElement>) => {
if (e.target === e.currentTarget) {
closeModal();
}
};
return (
<div ref={modalRef}>
{isVisible && (
<div className="fixed top-0 left-0 w-screen h-screen z-50 bg-slate-800/50">
<div
className="flex w-full h-full items-center justify-center"
onClick={(e) => handleOverlayClick(e)}
>
{children}
</div>
</div>
)}
</div>
);
};
return { Modal, openModal, closeModal };
};細かく
type WrapperFn = <T extends readonly unknown[]>(
fn?: (...args: T) => void,
...args: T
) => void;モーダル開閉時のラップ関数の型定義です。ややこしそうなことしてますが、要は第一引数に渡した関数の引数を可変長引数で受け取るようにしてします。
useEffect(() => {
if (modalRef.current === null) return;
if (isVisible) {
disableBodyScroll(modalRef.current);
} else {
enableBodyScroll(modalRef.current);
}
return () => clearAllBodyScrollLocks();
}, [isVisible, modalRef]);モーダルを開いている時にはモーダル以外のスクロールをロックしています。個人的にモーダル開いている時に元の画面が動いてほしくないのでこの形にしています・
const openModal: WrapperFn = (onOpen, ...args) => {
if (onOpen) onOpen(...args);
setIsVisible(true);
};
const closeModal: WrapperFn = (onClose, ...args) => {
if (onClose) onClose(...args);
setIsVisible(false);
};先程定義したWrapperFnになるように組んでいます。あとは関数が渡されれば実行し開閉フラグを変更しているだけです。
const Modal: React.FC<ModalProps> = ({ children }) => {
const handleOverlayClick = (e: React.MouseEvent<HTMLDivElement>) => {
if (e.target === e.currentTarget) {
closeModal();
}
};
return (
<div ref={modalRef}>
{isVisible && (
<div className="fixed top-0 left-0 w-screen h-screen z-50 bg-slate-800/50">
<div
className="flex w-full h-full items-center justify-center"
onClick={(e) => handleOverlayClick(e)}
>
{children}
</div>
</div>
)}
</div>
);
};ここではモーダルの共通スタイルを定義します。ここも個人的にモーダル本体以外をクリックすると閉じてくれるのが好きなのでそのようにしています。
利点
WrapperFnを使うことで下のようなケースに対応できます。
type Props = {
func: (id: number) => void;
}
const HogeHoge: React.FC<Props> = ({ func }) => {
// ...
}
const FugaFuga: React.FC = () => {
const { openModal, closeModal, Modal } = useModal();
const updateInfo = (id: number) => {
// do something...
}
return (
<Modal>
<HogeHoge openFunc{(id) => openModal(updateInfo, id)} />
</Modal>
);
}ここでupdateInfoだけを渡すと型エラーを吐いてくれます。え?() => voidにして(id) => openModal(() => updateInfo(id))にすればいいじゃないかって?
ださいじゃないですか!!
見比べてください。WrapperFn定義した方スタイリッシュですよね?カッコイイですよね?
まとめ
はい。自分の何か作る時のモチベなんてこんなものです。やっぱりコーディングする以上、画面覗かれても大丈夫なようにカッコよくしたいですからね。
話は変わりますがカッコよさを求める上で自分はアロー関数大好きです。見た目がイケメンです。