next react

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

Keita Imatomi
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配下にモーダルを作成するようにする。

結果

こんな感じでいかがでしょうか?これはReact.js ですが、Next.js でもほぼほぼ同じです。

解説

上記、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 を生成するようになる。

完成!

こちらもどうぞ