React.js ( Next.js )で Modal コンポーネント を作る

imatomix
2020年6月9日 18:01

余談

最近、仕事で React.js と Next.js を使い始めました。 Vue.js -> Nuxt.js -> React.js -> Next.js ときて、これでひと通り(?)コンプです。
双方扱ってみて感じたのは、ささっと作ることを楽しむなら Vue.js / Nuxt.js 、考り方を楽しんで作るなら React.js / Next.js で、車に例えると、Vue.js はオートマ車、 React.js はマニュアル車のような感じでした。それぞれに面白いです。

ただ、Typescript を採用するなら今のところ、React.js / Next.js です。

目的

  • モーダルコンポーネントを作る。(ことにより、React.js / Next.js を理解したい。)
  • 要件
  • モーダルパネルの中身は自由にできる。
  • パネル上のボタン、またはパネル外のエリアをクリックで閉じることができる。
  • 任意のDOM配下にモーダルを作成するようにする。

結果

こんな感じ?というのができた。
<iframe src="https://codesandbox.io/embed/empty-sun-42mr3?fontsize=14&hidenavigation=1&theme=dark" style="width:100%; height:500px; border:0; border-radius: 4px; overflow:hidden;" title="empty-sun-42mr3" allow="accelerometer; ambient-light-sensor; camera; encrypted-media; geolocation; gyroscope; hid; microphone; midi; payment; usb; vr; xr-spatial-tracking" sandbox="allow-autoplay allow-forms allow-modals allow-popups allow-presentation allow-same-origin allow-scripts"></iframe>

解説

codesandbox上のソースコードを元に解説。

概要

親のApp コンポーネントが持つ
  • モーダルのステート (isOpenModal)
  • モーダルの開閉処理 (toggleModal)
のうち、toggleModalpropsとして、Modalコンポーネントとその children (Panelコンポーネント) に渡していく。

Modal コンポーネント

親のAppコンポーネントから受け取った toggleModal の処理を
  • パネル外をクリックすると実行する。
  • 子コンポーネントにも渡す。

props

type Props = { close: (e: any) => void; // toggleModal を受け取る children: React.ReactNode; // 子コンポーネントを受け取る };

cloneElement

あとは、普通に
const Modal: React.FC<Props> = props => { return ( <div onClick={props.close}> { // パネルの外をクリックでクローズ } <div>{children}</div> </div> );
としたいところだけど、これだと childrenprops を渡せない。。。propsが渡せないと、Panel コンポーネントからモーダルを閉じることができない。。。しょうがないので、App から直接 Panel に props を渡せばいいかと思ったけど、ちょっとググったら children の代わりに cloneElement() というのがあるらしいので使ってみた。
const Modal: React.FC<Props> = props => { return ( <div onClick={props.close}> <div> {React.cloneElement(props.children as any, { close: props.close })} </div> </div> ); };
これで、children にModalコンポーネントが持つ props.close (=toggleMpdal) を渡すことができた。

Panel コンポーネント

受け取った toggleModalの処理を必要な箇所で呼び出す。

props

Modal コンポーネントから モーダルを閉じる処理を受け取るので、
type Props = { close?: (e: any) => void; };

モーダルを閉じる

キャンセルボタンはonClickから直接props.close
<button type="button" onClick={props.close}>Cancel</button>
実行ボタンは、内部処理の中で
<button type="submit" onClick={submit}>OK</button>
const submit = e => { e.preventDefault(); {// いろいろな処理 } if (props.close) { props.close(e); } };
これで、モーダルを閉じることができる。

Appコンポーネント

useState() でモーダルの開閉状態を扱うステートを用意する。
const [isOpenModal, setIsOpenModal] = useState(false);
ステートを切り替える関数 (toggleModal) を用意する。
const toggleModal = e => { if (e.target === e.currentTarget) { setIsOpenModal(!isOpenModal); } };
button と Modal コンポーネントに toggleModal を渡す。Modal コンポーネントは IsOpenModal ステートに応じてON/OFFにする。
return ( <div className="App"> <button type="button" onClick={toggleModal}> Open! </button> {isOpenModal && ( <Modal close={toggleModal}> <Panel /> </Modal> )} </div> );

Portal コンポーネント

これまででモーダルはできているけど、Modalコンポーネントを配置した位置にDOMが生成されるのが、なんとなく気持ち悪い。できれば、他からの影響の少ない上層の任意の場所に生成させたい。 そんな時は Portal を使うらしい。
Portal.tsx
import ReactDOM from "react-dom"; const Portal = ({ children }) => { const element = document.querySelector("#root"); return element ? ReactDOM.createPortal(children, element) : null; }; export default Portal;
ちなみにdocument.querySelector はクライアント側の処理になるので、Next.js でやる場合は
if (process.browser) {}
で囲って、サーバー側では無視するようにしないとエラーになる。
あとはこの Portal を Modal 内に仕込む。
Modal.tsx
import React from "react"; import Portal from "./Portal"; import styles from "./Modal.module.scss"; type Props = { close: (e: any) => void; children: React.ReactNode; }; const Modal: React.FC<Props> = props => { return ( <Portal> <div className={styles.modal} onClick={props.close}> <div> {React.cloneElement(props.children as any, { close: props.close })} </div> </div> </Portal> ); }; export default Modal;
これで所定のDOM(ここでは#root、Next.js の場合は #__next)配下に Modal を生成するようになる。
完成!