Reactの練習で簡単なモーダルウィンドウを作成しました。
こういったモーダル関係の動きはChakra UIなどのコンポーネントライブラリを使うことが多いと思いますが、createPortalの使い方のサンプルとしてメモしておきます。
モーダル要素を</body>の直前にマウントする【createPotal】
モーダル要素など、親のCSSの影響を受けたくない要素は、</body>の直前に挿入することが多いです。
とはいえ、<body>はReactが操作できるDOMの範囲外にあります。そんな時に使えるのがReactDOM.createPortal というフックです。
以下は、モーダルコンポーネントを本来の場所ではなく、<body>タグ直下にマウントさせている例です。
createPotalは、上の図のように、指定したDOMに別の子要素をマウントできます。
構文は次の通りになります。
createPortal(<子要素>, <DOM要素>);
使用するには、ReactDomのcreatePortalをインポートしておきます。上記の構文は、<子要素>が<DOM要素>にマウントされます。
createPotalはコンポーネントにして、子要素をprops.childrenで受け取ると、汎用性が高く使い回しができます。
以下は、<body>の最後にprops.childrenで渡ってきた要素をマウントするコードです。
import { createPortal } from "react-dom";
// createPortal
const ModalPortal = ({ children }) => {
// マウントするDOMをbodyに指定する
const target = document.body;
return createPortal(children, target);
};
// modalElement
const ModalElement = () => {
return (
<>
<ModalPortal>
<div>
子要素としてprops.childrenに渡す
</div>
</ModalPortal>
</>
);
};
上記が基本的な使い方になります。
createPortalを使ってモーダルウィンドウを作成する(デモ)
createPortalを使ってモーダルウィンドウを作成しました。同一ページに複数のモーダルを設置できて、画像とテキストの表示に対応しています。
完成の動きはCodeSandboxで確認できます。
コード全体
App.tsx
import "./styles.css";
import { useState, useEffect } from "react";
import ModalBtn from "./components/ModalBtn";
import ModalElement from "./components/ModalElement";
// APP
export default function App() {
// モーダルが表示されている状態を管理
const [modalOpen, setModalOpen] = useState(false);
// モーダルの名前を管理
const [modalName, setModalName] = useState("");
// 読み込みの完了を管理
const [readComplete, setReadComplete] = useState(false);
// モーダルが表示されたらbody要素にクラスを追加
useEffect(() => {
if (modalOpen) {
document.body.classList.add("is_modal");
} else {
document.body.classList.remove("is_modal");
}
}, [modalOpen]);
// モーダルボタンをクリック(イベント)
const modalClick = (e: any) => {
const { target } = e;
setModalOpen((prev) => !prev);
setReadComplete(false);
// eがボタンではない場合、処理を中止
if (!(target instanceof HTMLButtonElement)) {
return;
}
// 表示するモーダルを紐付け
if (target.name) {
setModalName(target.name);
} else {
setModalName("");
}
};
return (
<div className="App">
<h1>モーダルウィンドウ</h1>
<ModalBtn modalBtnName="modal1" modalClick={modalClick}>
モーダル1(画像)
</ModalBtn>
<ModalElement
targetName="modal1"
modalOpen={modalOpen}
modalName={modalName}
modalClick={modalClick}
readComplete={readComplete}
>
<img
src="/images/sample1.jpg"
alt=""
onLoad={() => setReadComplete(true)}
/>
</ModalElement>
<ModalBtn modalBtnName="modal2" modalClick={modalClick}>
モーダル2(テキスト)
</ModalBtn>
<ModalElement
targetName="modal2"
modalOpen={modalOpen}
modalName={modalName}
modalClick={modalClick}
>
<p>テキストテキストテキストテキストテキストテキスト</p>
</ModalElement>
</div>
);
}
components/ModalPortal.tsx
createPortalフックでbodyにモーダル要素を追加します。
import { FC } from "react";
import { createPortal } from "react-dom";
type TypeModalPortal = {
children: React.ReactChild;
};
const ModalPortal: FC<TypeModalPortal> = ({ children }) => {
const target = document.body;
return createPortal(children, target);
};
export default ModalPortal;
components/Modal.tsx
モーダル要素のコンテンツ内容です。props.childrenでラップされた要素を表示します。画像は読み込みが完了するまで「…Loading」を表示させています。
import { FC } from "react";
// モーダル要素
type ModalType = {
modalClick: (event: any) => void;
readComplete?: boolean;
};
const Modal: FC<ModalType> = ({ children, modalClick, readComplete }) => {
console.log(readComplete);
return (
<>
<div className="modal_content">
{children}
{readComplete === undefined || readComplete ? (
""
) : (
<p className="loading">loading...</p>
)}
<div className="close_btn">
<span onClick={modalClick}></span>
</div>
</div>
<div className="overlay" onClick={modalClick}></div>
</>
);
};
export default Modal;
components/ModalElement.tsx
モーダル要素の外枠です。ModalPortalとModalコンポーネントを内包します。
import { FC } from "react";
import Modal from "./Modal";
import ModalPortal from "./ModalPortal";
type ModalElementType = {
targetName: string;
modalOpen: boolean;
modalName: string;
modalClick: (event: any) => void;
readComplete?: boolean;
};
// モーダル外枠
const ModalElement: FC<ModalElementType> = ({
children,
targetName,
modalOpen,
modalName,
modalClick,
readComplete
}) => {
return (
<>
{modalOpen && modalName === targetName ? (
<ModalPortal>
<Modal modalClick={modalClick} readComplete={readComplete}>
{children}
</Modal>
</ModalPortal>
) : (
""
)}
</>
);
};
export default ModalElement;
conponents/ModalBtn.tsx
モーダルを表示するトリガーとなるボタンです。
import { FC } from "react";
// モーダル要素
type ModalBtnType = {
modalBtnName: string;
modalClick: (event: any) => void;
};
const ModalBtn: FC<ModalBtnType> = ({ children, modalBtnName, modalClick }) => {
return (
<>
<button name={modalBtnName} onClick={modalClick}>
{children}
</button>
</>
);
};
export default ModalBtn;
モーダルの設定方法
ModalBtn(ボタン)とModalElement(モーダル)コンポーネントにいくつか設定が必要です。
<モーダル名>はボタンとモーダル、両方に同じユニークな名前を設定します。
モーダルに画像を設定する場合
モーダルに画像を設定する場合、useStateで管理しているreadCompleteとsetReadCompleteを設定します。これらは画像の読み込みの完了をbooleanで管理します。
読み込みが完了するまで、モーダルにはLoadingの文字を表示します。画像の読み込みの完了を管理する必要がなければ、記述は必要ありません。
App.tsx
<ModalBtn modalBtnName="<モーダル名>" modalClick={modalClick}>
ボタンに表示するテキスト
</ModalBtn>
<ModalElement
targetName="<モーダル名>"
modalOpen={modalOpen}
modalName={modalName}
modalClick={modalClick}
// 画像の場合、読み込み完了のフラグを管理するreadCompleteを設定する
readComplete={readComplete}
>
<img
src="/images/sample1.jpg"
alt=""
// 画像の読み込みが完了したらフラグをtrueにする
onLoad={() => setReadComplete(true)}
/>
</ModalElement>
モーダルにテキストを設定する場合
モーダルにテキストを設定する際、readCompleteとsetReadCompleteの値は必要ないので記述はなくてもOKです。
App.tsx
<ModalBtn modalBtnName="<モーダル名>" modalClick={modalClick}>
モーダル2(テキスト)
</ModalBtn>
<ModalElement
targetName="<モーダル名>"
modalOpen={modalOpen}
modalName={modalName}
modalClick={modalClick}
>
<p>テキストテキストテキストテキストテキストテキスト</p>
</ModalElement>
まとめ
今回、createPortalの使い方を調べました。モーダルをサンプルにしたので、<body>に子要素をマウントしましたが、別のコンポーネントにマウントしたい時にも使えます。
機会があったら使ってみようと思います!