Help us understand the problem. What is going on with this article?

VueとReact(Hooksも)をアニメーション実装から比較する

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.

Vue
<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.

React
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.

ReactHooks
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とか代替の方法もあると思うので楽な方法を検討すればいい

Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
Comments
No comments
Sign up for free and join this conversation.
If you already have a Qiita account
Why do not you register as a user and use Qiita more conveniently?
You need to log in to use this function. Qiita can be used more conveniently after logging in.
You seem to be reading articles frequently this month. Qiita can be used more conveniently after logging in.
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
ユーザーは見つかりませんでした