オンラインでご相談ください!

ご相談内容が完全に固まっていない場合でも
遠慮なくミーティングの予約をどうぞ

TOP デザイン・技術 【React + gsap】でモーダルを作る!

【React + gsap】でモーダルを作る!

tsutsui tsutsui
【React + gsap】でモーダルを作る!

こんにちは、筒井です。

最近Reactのプロジェクトにアサインされたので、
タイムリーな話題として、Reactで基礎的なUIをどのように作るか書きました。

Reactの基礎知識はあるが、それをどのように活かして、実際にUIを作るか…となっている方向けの記事です。

また、BALANCeでは頻繁にgsapを使ってアニメーションを作るので、gsapも使ってみました。

今回は、実装頻度が高いモーダルを例に、ご紹介します。

生のjs

まずは、普通に生のjsで書くとこんな感じです。
モーダルはクラスとして持っておき、必要なところで以下を呼び出してください。

※cssは省略するので、スタイリングは適当にお願いします!

 

Modal/Controller.js

// モーダルのコントローラー:ロジック部分

import gsap from "gsap/all";
import Renderer from "./Renderer";

export default class Controller {
constructor() {
    // dom
    this.openbtn = document.querySelector(".js-modal_openbtn");
    this.modal = document.querySelector(".js-modal");
    this.closebtn = this.modal.querySelector(".js-modal_closebtn");

   // init
    this.setup();
    this.setEvents();
  }

  setup() {
    this.renderer = new Renderer(this.modal);
  }

 setEvents() { 
    this.openbtn.addEventListener("click", () => { this.open(); }); 
    this.closebtn.addEventListener("click", () => { this.close(); }); 
  }

  open() {
    if (this.tl) this.tl.kill(); 
    this.modal.classList.add("isActive"); // ★

    this.tl = gsap.timeline();
    this.tl.add(this.renderer.open());
  }

  close() {
    if (this.tl) this.tl.kill();
    this.modal.classList.remove("isActive");  // ★

    this.tl = gsap.timeline();
    this.tl.add(this.renderer.close());
  }
}

ロジック部分のコードは上記のとおりです。
.isActiveクラスのつけ外しで、pointer-events: autopointer-events: noneを切り替えています。

コードが長くなってしまったので、表現・演出部分は、以下のRenderer.jsに記載することにします。

 

Modal/Renderer.js

// モーダルのレンダラー:演出・表現部分

import gsap from "gsap/all";

export default class Renderer {
  constructor(modal) {
    this.bg = modal.querySelector(".js-modal_bg");
    this.contents = modal.querySelector(".js-modal_contents");
  }

  open() {
    const tl = gsap.timeline();

    tl
    // bg
    .to(this.bg, 0.5, {
      opacity: 1,
      ease: "expo.out"
    })
    // contents
    .to(
      this.contents, 0.5, {
      opacity: 1,
      ease: "expo.out"
    },0.2);

    return tl;
  }

  close() {
    const tl = gsap.timeline();

    tl
    // bg
    .to(this.bg, 0.5, {
      opacity: 0,
      ease: "expo.out"
    })
    // contents
    .to(
      this.contents, 0.5, {
       opacity: 0,
       ease: "expo.out"
    }, 0.2);

    return tl;
  }
}

なんだか長くなってしまったのですが、

・openbtnをクリックすると、open()が発火
・closebtnをクリックすると、close()が発火

というシンプルなモーダルの最小コードになります。

React

では、上記をReactで書き直してみます。

 

親側 : index.jsx

まずはModalをコンポーネントとして持っておき、必要なページの中で呼び出します。
(※ついでに、ボタンもコンポーネント化しちゃいました。)

import style from "../../style/pages/top/top.module.scss";
import { useState } from "react";
import { Button } from "../../components/Button";
import { Modal } from "./Modal/Modal";

export const Top = () => {
   // ①モーダルの状態を用意
  const [isOpen, setModalStatus] = useState(false); 

   // ②クリックでstate(isOpen)をtrueに
  const onClick = () => {
    setModalStatus(true); 
  };

  return (
    <>
      <div className={style.wrap}>
        <div className={style.inner}>
          <div className={style.btnwrap} onClick={onClick}>
            <Button>Open</Button>
          </div>
        </div>
      </div>
      <Modal isOpen={isOpen} setModalStatus={setModalStatus} />
    </>
  );
};

状態の管理は、useStateを用います。
モーダルは最初閉じてるので、初期値はfalseで、useState(false); ですね。

btnwrapをクリックすると、stateが変わるように組んでやります。

ReactではaddEventlistenrは不要で、onClick={onClick}とかけます。
ちなみに、removeEventListenerに関しては、イベントハンドラをつけると、reactがいい感じにremoveしてくれるので何もしなくてokです。

値はpropsを通して、modalコンポーネントに送ってやります。

 

子側:Modalコンポーネント

生のjsの書き方に合わせて、ロジック部分と演出・表現部分でファイルを切り分けます。

・ロジック部分担当 : Modal/Modal.jsx
・演出部分担当 : Modal/Renderer.js

を用意します。

ですが、Renderer.jsは、生のjsと全く同じコードなので、記載は省略します。
上述のModal/Renderer.jsをご参考ください。

Modal/Modal.jsxは、以下です。

// モーダル:ロジック部分

import style from "../../../style/pages/top/modal.module.scss";
import gsap from "gsap/all";
import classNames from "classnames";
import Renderer from "./Renderer";
import { useEffect, useRef } from "react";
import { Button } from "../../../components/Button";

export const Modal = (props) => {
  // ①propsで親から送られてきた値を取得
  const { isOpen, setModalStatus } = props;

  // ②諸々取得
  const modal = useRef(null); // - modal用意
  const tl = useRef(gsap.timeline()); // - tl用意
  const renderer = useRef(null); // - renderer用意

  // ③rendererを用意(演出・表現部分)
  useEffect(() => {
    renderer.current = new Renderer(modal.current); 
  }, []);

  // ④isOpenに変化があるたびに以下が発火
 //   フラグで発火内容を出し分け
  useEffect(() => { 
    if (isOpen) {
      open();
    } else {
      close();
    }
  }, [isOpen]);

 // ⑤閉じるボタンを押すと、state(isOpen)をfalseにする
  //  そのあとは④が発火
  const onClick = () => { 
   setModalStatus(false);
  };

 // ⑥openとclose用意
  const open = () => {
    tl.current.kill();
    tl.current = gsap.timeline();

    tl.current.add(renderer.current.open());
  };

  const close = () => {
    tl.current.kill();
    tl.current = gsap.timeline();

    tl.current.add(renderer.current.close());
  };

  return (
    <>
      <div
        className={style.modal}
        ref={modal}
        style={isOpen && styles.isActive}
      >
        <div className={classNames([style.bg, "js-modal_bg"])}></div>
        <div className={classNames([style.contents, "js-modal_contents"])}>
          <div className={style.contentsInner}>
            <p className={style.contentsText}>モーダルです</p>
            <div className={style.btnwrap} onClick={onClick}>
              <Button>Close</Button>
            </div>
          </div>
        </div>
      </div>
    </>
  );
};

色々コード内にコメントを残してるのですが、以下で少しだけ補足します。

 

・tlの用意

const tl = useRef(gsap.timeline());

Reactでは、こんな感じでtlを作ります。
tlには、tl.currentでアクセスできます。

なので、生のjsでtlと書いてたところは全て、tl.currentになります。

 

・Rendererの用意

const renderer = useRef(null);

rendererは最初nullで用意します。

useEffect(() => { r
  renderer.current = new Renderer(modal.current); 
}, []);

そして、useEffectの中でインスタンス化します。

何度もインスタンス化する必要はないので、第二引数を空の配列にして、初回レンダリングの時だけ発火するようにします。

 

・stateの変更

useEffect(() => { 
  if (isOpen) { open(); } else { close(); } 
}, [isOpen]);

ボタンがクリックするたびにstate(isOpen)が変わるわけですが、その度に上記が動きます。

あとは、openとcloseの中身を用意してあげるだけです!

 

以上です。

最後までお読みいただきありがとうございました!

DOWNLOAD

未公開実績も多数!
BALANCeの【デザイン・技術】サービス説明資料には、『Javascript』『react』の実績が多数掲載されています。
また、関連した会社資料も無料ダウンロードできますので、ぜひご覧ください!

CONTACT

お見積もりやご提案に関しては、費用は発生いたしません。お気軽に、お問い合わせください。

RECRUIT

BALANCeでは共に働く仲間を随時お待ちしております。
印象に残るプロダクトを一緒に作りませんか?

採用情報はこちら

タグから選ぶ