Vueで、アニメーションするコンポーネントを作ったので、
ついでにReactでも作ってみると実装方法が違ったので比較する
作ったものは、ハンバーガーボタン(押したらバツになるやつ)
svgをGSAPのTweenMaxでclickイベントをトリガーにアニメーションさせることでハンバーガーボタンを作成する
とりあえずsvgをただTweenMaxでアニメーション
DOMを取得してTweenMaxでアニメーションする例
ボタンを押せばアニメーション
See the Pen SvgTween by Saito Takashi (@7_asupara) on CodePen.
Vueで作成する
See the Pen VueHamburger by Saito Takashi (@7_asupara) on CodePen.
<template>
<div class="button" v-on:click="toggle"> <!-- クリックイベントを付与 -->
<svg :viewbox="viewbox" :width="size" :height="size" style="overflow: visible">
<!-- svgの各属性に変数をバインディング -->
<line
x1="0"
:y1="line1Y1"
:x2="size"
:y2="getTopLimit()"
:stroke="stroke"
:stroke-width="strokeWidth"
/>
<line
:x1="line2X1"
:y1="halfSize"
:x2="size"
:y2="halfSize"
:stroke="stroke"
:stroke-width="strokeWidth"
/>
<line
x1="0"
:y1="line3Y1"
:x2="size"
:y2="getBottomLimit()"
:stroke="stroke"
:stroke-width="strokeWidth"
/>
</svg>
</div>
</template>
<script>
export default {
data() { // dataオブジェクト 変更を通知したい変数とかはここで定義
return {
size: 50,
stroke: 'black',
strokeWidth: 6,
speed: 0.4,
line1Y1: 0,
line2X1: 0,
line3Y1: 0,
menuCloseFlg: false
};
},
computed: { // 加工した返り値の変数を作りたい場合はここで定義
viewbox: function () {
return `0 0 ${this.size} ${this.size}`
},
halfSize: function () {
return this.size / 2
}
},
mounted () { // mountedではcomputedが動かないのでmethodで初期化
this.line1Y1 = this.getTopLimit()
this.line3Y1 = this.getBottomLimit()
},
methods: {
getTopLimit () {
return this.strokeWidth / 2
},
getBottomLimit () {
return this.size - (this.strokeWidth / 2)
},
toggle () { // クリックイベント
if (this.menuCloseFlg) {
TweenMax.to(
this.$data,
this.speed,
{
line1Y1: this.getTopLimit(),
line2X1: 0,
line3Y1: this.getBottomLimit(),
ease: Expo.easeIn
}
)
} else {
TweenMax.to(
this.$data,
this.speed,
{
line1Y1: this.getBottomLimit(),
line2X1: this.size,
line3Y1: this.getTopLimit(),
ease: Expo.easeIn
}
)
}
this.menuCloseFlg = !this.menuCloseFlg
}
}
}
</script>
Vueでは、dataオブジェクトを変更すると、自動で画面にも反映(rerender)してくれる
なので、svgの動かしたい属性にdataオブジェクトのプロパティを付与してその値を変更すれば勝手に画面に反映してくれる
この例の場合は、toggleメソッドでDOMではなく、svg属性に割り当てたdataオブジェクトの値を直接TweenMaxで変更してアニメーションさせている
この特性のおかげで値の変更が直感的にできるので、アニメーションを扱う上でとてもVueはいいと思う
svgを使用した動的なUIが簡単に作れそう
Reactで作成する
とりあえずClassComponentを使って作成する
See the Pen ReactHamburger by Saito Takashi (@7_asupara) on CodePen.
import React from 'react';
class App extends React.Component {
constructor(){
super();
// 必要な変数の定義
this.size = 50;
this.speed = 0.4;
this.strokeWidth = 6;
this.halfSize = this.size / 2;
this.halfStrokeWidth = this.strokeWidth / 2;
this.topLimit = this.halfStrokeWidth;
this.bottomLimit = this.size - this.halfStrokeWidth;
this.viewbox = `0 0 ${this.size} ${this.size}`;
this.stroke = 'black'
// 変更を通知したい変数はここでstateとして定義
this.state = { closeFlg: false };
// DOMノードを取得するための準備
this.line1Ref = null;
this.line2Ref = null;
this.line3Ref = null;
}
handleClick = () => {
// svgの属性ではなく、DOMノードを直接Tweenさせる
if (!this.state.closeFlg) {
TweenMax.to(this.line1Ref, this.speed, { attr: { y1: this.bottomLimit }, ease: Expo.easeIn })
TweenMax.to(this.line2Ref, this.speed, { attr: { x1: this.size }, ease: Expo.easeIn})
TweenMax.to(this.line3Ref, this.speed, { attr: { y1: this.topLimit }, ease: Expo.easeIn })
} else {
TweenMax.to(this.line1Ref, this.speed, { attr: { y1: this.topLimit }, ease: Expo.easeIn })
TweenMax.to(this.line2Ref, this.speed, { attr: { x1: 0 }, ease: Expo.easeIn })
TweenMax.to(this.line3Ref, this.speed, { attr: { y1: this.bottomLimit }, ease: Expo.easeIn })
}
// Reactでは変更の通知はsetStateが必須
this.setState(prevState => ({
closeFlg: !prevState.closeFlg,
}))
}
// svgのlineタグにrefを付与してDOMノードを取得する
render() {
return(
<div className="button" onClick={this.handleClick}>
<svg viewBox={this.viewbox} width={this.size} height={this.size} style={{ overflow: 'visible' }}>
<line
ref={ c => this.line1Ref = c}
x1="0"
y1={this.topLimit}
x2={this.size}
y2={this.topLimit}
stroke={this.stroke}
strokeWidth={this.strokeWidth}
/>
<line
ref={ c => this.line2Ref = c}
x1="0"
y1={this.halfSize}
x2={this.size}
y2={this.halfSize}
stroke={this.stroke}
strokeWidth={this.strokeWidth}
/>
<line
ref={ c => this.line3Ref = c}
x1="0"
y1={this.bottomLimit}
x2={this.size}
y2={this.bottomLimit}
stroke={this.stroke}
strokeWidth={this.strokeWidth}
/>
</svg>
</div>
);
}
}
Reactでは、変数(state)変更の通知をsetStateを使って行って初めて画面に反映(rerender)される
(Reactはstate変更の通知をするかどうかをプログラマーがコントロールしたいので、setStateを実行することを採用している)
なので、Vueのように値を変更するだけでは画面に反映されない
TweenMaxのようなトゥイーン系のライブラリはフレーム毎の値の変更をループでよしなにやってくれるが、svgの属性値の変更をする場合、この中にsetStateをねじ込むことができないので変更の通知ができなくアニメーションされないはず
そこで、ReactでTweenしたい場合は、Refを使用してDOMノードを取得しDOMに対してTweenMaxでアニメーションする(要はjQueryとかと同じで昔ながらの方法)
Vueより手間が多くなり、複雑なアニメーションはめんどくさそうだ
ReactHooksで作成する
Reactでは、ClassComponentが滅びてReactHooksとかいうのを使うのがスタンダードになるらしいのでこいつのもついでに作ったが、結構めんどくさかった
ReactにはFunctinalComponentとClassComponentがあって、ClassComponentでしかstateが利用できなかったが、FunctinalComponentでもReactHooksを利用してstateを扱えるようになったらしい
See the Pen ReactHooksHambergur by Saito Takashi (@7_asupara) on CodePen.
import React from 'react';
function App() {
const size = 50;
const speed = 0.4;
const strokeWidth = 6;
const halfSize = size / 2;
const halfStrokeWidth = strokeWidth / 2;
const topLimit = halfStrokeWidth;
const bottomLimit = size - halfStrokeWidth;
const viewbox = `0 0 ${size} ${size}`;
const stroke = 'black'
// React.useStateで、stateのgetterとsetterの定義
// const [getter, setter] = React.useState(デフォルト値)
const [closeFlg, setCloseFlg] = React.useState(false);
const [clicked, setClicked] = React.useState(null);
// useRefでDOMノードの取得 ClassComponentとだいたい同じ
const line1Ref = React.useRef(null);
const line2Ref = React.useRef(null);
const line3Ref = React.useRef(null);
// クリックイベント closeFlgをトグルするだけ
const toggle = () => {
setCloseFlg(!closeFlg);
};
// useEffect SideEffect(副作用?)を実行するやつ
React.useEffect(() => {
if (closeFlg) {
TweenMax.to(line1Ref.current, speed, { attr: { y1: bottomLimit }, ease: Expo.easeIn })
TweenMax.to(line2Ref.current, speed, { attr: { x1: size }, ease: Expo.easeIn})
TweenMax.to(line3Ref.current, speed, { attr: { y1: topLimit }, ease: Expo.easeIn })
} else {
TweenMax.to(line1Ref.current, speed, { attr: { y1: topLimit }, ease: Expo.easeIn })
TweenMax.to(line2Ref.current, speed, { attr: { x1: 0 }, ease: Expo.easeIn})
TweenMax.to(line3Ref.current, speed, { attr: { y1: bottomLimit }, ease: Expo.easeIn })
}
}, [closeFlg]);
return (
<div className="button" onClick={toggle}>
<svg viewBox={viewbox} width={size} height={size} style={{ overflow: 'visible' }}>
<line
ref={line1Ref}
x1="0"
y1={topLimit}
x2={size}
y2={topLimit}
stroke={stroke}
strokeWidth={strokeWidth}
/>
<line
ref={line2Ref}
x1="0"
y1={halfSize}
x2={size}
y2={halfSize}
stroke={stroke}
strokeWidth={strokeWidth}
/>
<line
ref={line3Ref}
x1="0"
y1={bottomLimit}
x2={size}
y2={bottomLimit}
stroke={stroke}
strokeWidth={strokeWidth}
/>
</svg>
</div>
);
}
Refの使い方とstateをsetしないといけないのはだいたいClassComponentと同じ
ただ、クリックイベントを普通にFunctionalComponentのメソッドとして定義してもライフサイクルが考慮されないのか、そこでDOMノードにアクセスしてTweenしようとしても何も実行されない(画面にレンダリングされる前に定義されるからかな?よくわからん)
ReactHooksでは、useEffectとかいうのがComponentのライフサイクル(ClassComponentでいうcomponentDidMountとかcomponentDidUpdateとか)を管理しているみたいなので、これを利用する
クリックイベントには、stateのcloseFlgのトグル処理のみ記述し、
useEffectで実行したい処理(第一引数)と監視するstate(第二引数)を指定し、closeFlgが変更されたら実行されるようにする(componentDidUpdateにあたるかな, VueならWatcher使えば同じような実装になるような)
アニメーションに限定していえば、慣れたらいけるかもやけど全然直感的じゃないのでめんどくさく感じたし、useEffectがなんか慣れない
まとめ
両者を比較すると、Vueに比べてReactはデータを厳格に扱うことを目指していると思われる
その分、Vueは今回の場合に限らず直感的にコードが書けると思う
ページ数が小規模でアニメーションが多めのインタラクティブなLP、コーポレートサイトが作りたければVueを使うべきだと思う
一方Reactは、大規模なシステム等でデータを厳格に扱いたい場合は優位だと思う
これらの中間のものは好きな方を勝手に選ぼう
ただ、今回はsvgのTweenでアニメーションしたので違いがでたが、CSSとか代替の方法もあると思うので楽な方法を検討すればいい