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の概念の方だった。

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


採用から数カ月後

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