【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: autoとpointer-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の中身を用意してあげるだけです!
以上です。
最後までお読みいただきありがとうございました!