Vue.js と Redux が出会った
engineering

Vue.js と Redux が出会った

Vue.js と Redux が出会った

とある現場で Redux にたどり着いた話

  • あるところにプロトタイピングを繰り返した結果、1 ファイルにベタッと書かれていたソースがありました。
  • なんとかリリースを終えて、運用フェーズに入ったことものの、つらいソースが残されたのです。
  • ファイルを再利用可能にするためコンポーネント化を進めることを決意します。
  • 最初はとても大きな単位でコンポーネントを作っていたため、大量の状態を 1 コンポーネントが抱えていました。
  • コンポーネントの粒度を細かくしていくと、だんだん props のバケツリレーが増えてきます。
  • 親から子へ、子から孫へ、孫からひ孫へ…。
  • 2way binding してるバケツリレーは、もう追っていくのがツラい!
  • ただリレーしてるだけで、中間のコンポーネントでは使ってないものも出てきました。
  • 1 箇所直すと、他に影響が出て怖い。
  • これは Flux, Redux の出番だ!

なぜ Redux を使うのか

HTML5 とか勉強会の資料を見て、書きたくなった!

前段おしまいです。

  • 階層が深くなった時のバケツリレー
  • コンポーネント間のデータ同期

この辺りは、みんな同じところで悩みを抱えていて、最近は Redux に流れ着くんだけど、さらなる悩みを抱えてるようですね。
早くその境地にたどり着きたいです。

今回は Vue.js で使ってみるまでの話で止めておきます。


とりあえず資料読む

先に上げた資料もよいのだけれど、はじめての概念でわからないところだらけです。
正直飲み込むまですごく時間かかりました。

Facebook の Flux 実装やら、Redux のドキュメントやら、
いろいろ読んだけど、最終的にブクマしたのは 1 つでした。

これはダイジェスト版なので、実際には元記事の方をじっくり読むといいです。

いろいろ読んでいる過程で実は Vue.js ガイドにも書いてあったのに気づきました。

「状態管理にイラついたら ストアパターン にたどり着くよね。」(超絶意訳)


Vue.js と組み合わせるなら?

なんとなく理解したところで、どの実装を採用するか検討します。
ざっくりわけると公式の実装 or 生 Redux + α

Vuex (採用した方)

pros:

  • Vue.js オフィシャルな Redux 実装
  • devtools も Vue.js と同じものを使える

cons:

  • Redux / Flux と用語違う
  • vuex.getters とか vuex.actions とか、 Vue コンポーネントと密すぎる
  • vuex-connect というもので疎結合にできそう
  • vue-connect だと props とどう住み分けるかでまた悩みそう

Redux + revue

pros:

  • 生 Redux 使うので、 Vue と密になり過ぎない

cons:

  • 逆に Vue.js と一緒に使うのには面倒
  • バインディングとイベントお手玉とか
  • middleware 選びで死ぬ可能性
  • react-redux のような定番感もない

Vuex サンプル実装

Vuex で簡単な実装をやってみます。
main.js がエントリーポイントで、 App.vue をマウントします。

. ├── App.vue ├── components │   ├── CountDisplay.vue │   └── IncrementButton.vue ├── store │   └── index.js └── main.js
store/index.js
import Vue from "vue";
import Vuex from "vuex"; // Vuex モジュールを読み込む

Vue.use(Vuex); // Vue で使えるようにアクティベート

var store = new Vuex.Store({
  state: {
    // 初期状態を作成
    counter: 0,
  },
  mutations: {
    // 状態を変更するメソッドを宣言
    INCREMENT(state) {
      state.counter++;
    },
  },
});

export default store;
IncrementButton.vue
<template>
  <button @click.prevent="increment">+1</button>
</template>

<script>
  import store from "../store"; // 利用するコンポーネントで読み込む

  export default {
    methods: {
      increment() {
        store.dispatch("INCREMENT"); // `store.state.counter` を直接操作しない
      },
    },
  };
</script>
CountDisplay.vue
<template>
  <span>{{ counter }}</span>
</template>

<script>
  import store from "../store"; // 利用するコンポーネントで読み込む

  export default {
    computed: {
      counter() {
        return store.state.counter;
      },
    },
  };
</script>
App.vue
<template>
  <div id="app">
    <increment></increment>
    <counter></counter>
  </div>
</template>

<script>
  import Counter from "./components/CountDisplay.vue";
  import Increment from "./components/IncrementButton.vue";
  export default {
    components: {
      Counter,
      Increment,
    },
  };
</script>

Vuex 採用後

既存のソースから、状態と状態の操作だけを取り出して、 Store にまとめるの大変でした。
フロントエンドにおける Model って、なんなんだろうという闇に囚われたことも…

サンプルでやったように store.dispatch() をコンポーネントから呼ぶことはないかもしれないです。
今は、状態操作をまとめた action.js を作って、API 通信やデータフォーマットしてから store.dispatch() しています。
つまりは Action Creators がいる状態に落ち着きました。

実は、Store を EventEmitter で自力実装も簡単にできるレベルです。
Vuex を採用しても、お別れすることもすぐにできる。

大事なのは Redux の概念の方だった。

よく見かけるこの図が飲み込めるようになるまで、時間がかかりました。

vuex

採用から数カ月後

自分以外にも関わる人が増えたり、別のプロジェクトでも啓蒙するようになりました。
入門レベルだと、ファイルが分割されて、どこからどう処理が流れるのか迷子になりがちです。
まずは、 state と mutation をまとめること。
それから、 action は作らないでコンポーネントから store.dispatch() するものを作ります。
その後は、規模に応じて action をいれていくという段階を踏むのが、理解には必要な過程みたいです。