Reactでモーダルを作る

最近Reactを使っているのでReactのモーダルメモ。
モーダルを開いている時はbodyのスクロールができないようになります。

モーダルを起動するボタン(ModalBtn)と表示するモーダル(Modal)が分かれているので、AppでUseStateをセットしてそれぞれボタンとモーダルに渡しています。

AppとModalBtnの値の引き渡し

子→親へ値を返す

子から親へ値を返すにはReactはVueのように$emit()のような便利なものはないので、親コンポーネントで実行する関数を作り、子のクリックイベントで発火させます。

export default function App() {
  const [isModalOpen, setIsModalOpen] = useState(false);//モーダルの表示状態
  const [modalSelect, setModalSelect] = useState("");//モーダルの中身の振り分け
  const [modalScroll, setModalScroll] = useState(0);//モーダルを開いた時のスクロール値を取っておく

  //モーダルを開く
  const modalOpen = (taregt: string) => {
    setIsModalOpen(true);//モーダル状態をtrueに
    setModalSelect(taregt);//モーダルの中身で何が選ばれたか
    setModalScroll(window.scrollY);
    let body = document.getElementsByTagName("body");
    body[0].style.top = -window.scrollY + "px";
    body[0].classList.add("is-fixed");
  };

  //中略

  return (

  //中略
          //関数modalOpenを子コンポーネントに渡す
          <ModalBtn title="モーダル1" onClick={() => modalOpen("modal-1")} />

  //中略
  );
}
import React from "react";

type Props = {
  title: string;
  onClick: any;
};
export const ModalBtn: React.VFC<Props> = (props) => {
  //クリックイベントで受け取ったonClickを発火させる
  return <button onClick={props.onClick}>{props.title}</button>;
};
export default ModalBtn;

そのまま入れると無限ループになる

ちなみにボタンを下記のようにするとToo many re-renders. React limits the number of renders to prevent an infinite loop.と警告が出ます。私も数時間ハマっていました。
関数modalOpenの中でuseStateを使用しており、useStateはrender時に呼ばれるので無限ループに陥ってしまいます。そのため関数の中に入れて、クリックイベントが呼ばれた時だけ実行させます。

<div className="modal-button-wrap">
          //↓Too many re-renders. React limits the number of renders to prevent an infinite loop.
          <ModalBtn title="モーダル1" onClick={modalOpen("modal-1")} />

          //関数に入れて子コンポーネントに渡す
          <ModalBtn title="モーダル1" onClick={() => modalOpen("modal-1")} />
</div>

AppとModalの値の引き渡し

ModalBtnのクリックイベントでuseStateを変えたら、その情報をModalに渡します。

export default function App() {
  const [isModalOpen, setIsModalOpen] = useState(false);//モーダルの表示状態
  const [modalSelect, setModalSelect] = useState("");//モーダルの中身の振り分け
  const [modalScroll, setModalScroll] = useState(0);//モーダルを開いた時のスクロール値を取っておく

  //モーダルを開く
  const modalOpen = (taregt: string) => {
    setIsModalOpen(true);
    setModalSelect(taregt);
    setModalScroll(window.scrollY);
    let body = document.getElementsByTagName("body");
    body[0].style.top = -window.scrollY + "px";
    body[0].classList.add("is-fixed");
  };
  //モーダルを閉じる
  const modalClose = () => {
    setIsModalOpen(false);//モーダル表示状態をfalseに
    let body = document.getElementsByTagName("body");
    body[0].style.top = "0px";
    body[0].classList.remove("is-fixed");
    window.scrollTo(0, modalScroll);
    setModalScroll(0);
  };

  return (

  //中略

        <Modal
          modalStatus={isModalOpen}
          modalContent={modalSelect}
          onClick={modalClose}
        />

  //中略

  );
}

ここでは孫コンポーネントのModalContentへさらに値を渡したりしています。

import React from "react";

import { IconContext } from "react-icons";//バツのアイコンを出すのに読ませている
import { MdClose } from "react-icons/md";//バツのアイコンを出すのに読ませている
import ModalContent from "../modules/ModalContent";

type Props = {
  modalStatus: boolean;
  modalContent: string;
  onClick: any;
};

export const Modal: React.VFC<Props> = (props) => {
  //モダールを閉じる
  const modalClose = () => {
    //親コンポーネント(App)のmodalCloseを発火させる。
    props.onClick();
  };

  return (
    //props.modalStatusの値によってモーダルを出し分け
    <div className={props.modalStatus ? "modal is-active" : "modal"}>
      <div className="modal-inner">
        //クリックでmodalCloseを発火させる
        <div className="modal-close" onClick={modalClose}>
          <IconContext.Provider value={{ size: "20px" }}>
            <MdClose />
          </IconContext.Provider>
        </div>
        <div className="modal-content">
          //ModalContentに受け取った props.modalContent をそのまま渡す。
          <ModalContent modalId={props.modalContent} />
        </div>
      </div>
    </div>
  );
};
export default Modal;

モーダルの中身の振り分け

別で用意したGetModalContents.tsにモーダル情報の入っている配列を配置し、受け取ったmodalIdをもとに配列をfilterで抜き出します。

import React from "react";

import { GetModalContents } from "../../hooks/GetModalContents";

type Props = {
  modalId: string;
};

//モーダルの中身
export const ModalContent: React.VFC<Props> = (props) => {
  //親のModalから受け取ったmodalIdをもとに中身を選ぶ。GetModalContentsにidを渡す。
  const modalContent = GetModalContents(props.modalId);
  // ↓こんな感じで値が返ってくる
  //modalContent = [{
  //  id: "modal-1",
  //  title: "モーダル1です",
  //  content: "モダール1のコンテンツ"
  // }]
  return (
    <>
      {0 < modalContent.length ? (
        <>
          <p className="modal-title">{modalContent[0].title}</p>
          <div className="modal-text">{modalContent[0].content}</div>
        </>
      ) : (
        <></>
      )}
    </>
  );
};
export default ModalContent;
const modalContents = [
  {
    id: "modal-1",
    title: "モーダル1です",
    content: "モダール1のコンテンツ"
  },
  {
    id: "modal-2",
    title: "モーダル2です",
    content: "モダール2のコンテンツ"
  },
  {
    id: "modal-3",
    title: "モーダル3です",
    content: "モダール3のコンテンツ"
  }
];
export const GetModalContents = (id: string) => {
  //渡されたidをもとにモーダルの中身を選んで返す
  const modalContent = modalContents.filter((x) => x.id === id);
  return modalContent;
};

参考サイト

React 新人あるあるのコンポーネント無限レンダリングにはまりました | Zenn