100日後にリファクタリングするVuex(Vuexの使い方を間違えた件)


PrAha Inc. CEO、時々エンジニアのdowannaです。

nuxt initしたら勝手に付いてくるため無意識に使いがちなVuexですが、本来の用途を理解せず使うと、ただ労力が増えるだけで、さほど意味のない構成になります。僕は見事にそれをやりました。そんな僕を反面教師に、Vuexとの正しい付き合い方を感じてもらえたら幸いです。

この記事で伝えたいこと

Vuexは「複数のcomponentでstateを共有すること」が本来の意図されたユースケースで、stateをcomponent間で共有しない場合は、無理にvuexを使わなくても構わない

Vuexは「複数componentでstateを共有すること」を想定

公式サイトにもある通り、Vuexは「複数componentでstateを共有する時」を想定しています。

が、僕が作ったアプリケーションではnuxtのpage単位にVuexのstoreを分割しました。

hogehoge.com/a
hogehoge.com/b

という2つのpageがあったとすると、こんな構成でした

nuxt
├──-pages
    ├──a.vue
    ├──b.vue
├──-store
    ├──a.js
    ├──b.js

page/Aからdispatchされたactionはstore/Aでハンドリング
page/Bからdispatchされたactionはstore/Bでハンドリング

こんな具合にstore同士がお互いにやり取りをしない構成になっていましたが、特にcomponentを跨がないデータを取り扱うだけであればpageの中にdataとして持っても構わないわけです。単純にpageのロジックをstoreに移すだけではメリットは少なく、以下のようなデメリットが気になり始めました。

Vuexのデメリットが目立つように

Publicなストアである

Vuexは他のpageからも容易に編集できるグローバルなpublicストアなので、いつどこから編集されるか分かりません。

開発ツールでデバッグして追うことは出来ますが、pageのなかにdataとして持っていた方が、変更の呼び出し箇所が限定されて保守性も高まるでしょう。

グローバルストアに四方八方からガンガンデータを突っ込む、という一番やってはいけない事をやってしまった。

コードジャンプが効かない

Vuexを使う場合はメソッドの呼び出しではなく文字列をキーに使ったイベント送信になるためコードジャンプが効きません。

一見地味なデメリットに見えますが、ずっと開発しているとイライラしてきます。あと一括で変更したい時にRename Symbol(VSCodeならF2)が効かないのでいちいちgrepして置換する必要が生じます。

this.$store.dispatch('a/doSomething') // <- 変更箇所を探るためにはgrepするしかない

dispatchやmutationの記述量が増えた

pageのdataとして保持していればapi呼び出しの一行で終わるところ、VuexだとAPIと通信するたびにdispatchして、mutationして、stateに反映する必要が生じます

// ====== Vuexなし ====== //
const result = await get('/a') // たった1行だ!

// ====== Vuexあり ====== //
// page/a.vue
this.$store.dispatch('a/doSomething')

// store/a.js
export const state = () => ({
  result: ''
})

export const actions = {
  async doSomething({ commit }) {
    const data = await get('/a')
    commit('setSomething', data)
  }
}

export const mutations = {
  setSomething(state, data) {
    state.result = data.result
  }
}

更にpage/a.vueでmapSetterしてstoreからdataに取り出さなければいけない・・・この辺りからVuexを使うのではなくVuexに使われている感覚に陥ってきます

dispatchした結果を待つときにfluxパターンが崩壊する

APIに対するget('/a')の結果を待ってから処理を継続したい時、こんな感じでdispatchを待ちたくなります

const data = await this.$store.dispatch('a/doSomething')

が、これはfluxパターンに違反しています

Vue components から dispatch された actionが逆流することになるからです。正しくfluxパターンでdispatchの結果を待つのであれば、dispatchの結果変化するstateを監視する必要が生じます。が、返却値は必要無いのに、処理を待ちたい時まで無駄なstate変数が増えるのはイマイチです。

こんな感じのデメリットに殴り続けられながら、現状の構成で得られるメリットが殆ど無いことに気づいた時、僕はもう限界だと気づき、Vuexをやめたくなった。

そもそも、どうしてVuexを使い始めたのか?

component間でデータを共有しない以上、ただ手続きを煩雑にするだけのグローバルストアと化してしまったわけですが、そもそもなぜ僕はVuexを使ったのでしょうか。

nuxtに最初から入っていたから自然と使ってしまったと、人のせいにしてみます。

それは冗談ですが、当初は「APIにリクエストを投げる作業とか、ビューから描画以外のロジックを切り出すのにちょうどいいぞ」と考えていました。componentは出来る限りビューの描画に専念させたい。責務を分担したい。清純な動機でVuexを使い始めました。

しかし結論から言うと「APIにリクエストを投げる作業を切り出す」に関してはVuexを入れなくても、APIServiceみたいなクラスをpluginとして定義することや、utilにAPI呼び出しを切り出すことで実現できます。

//infrastructure/apiService.js

export default class APIService {
  constructor(axios) {
    this.axios = axios
  }
  get(url, options = {}) {
    return this.axios.$get(url, options)
  }
}

コイツをextendしたラッパーを用意して...

// infrastructure/settingAPIService.js

import APIService from './apiService'
export default class SettingAPIService extends APIService {
  getUserSettings() {
    return this.get('/settings')
  }
}

pluginとして注入すれば

// plugins/apiService.js
export default ({ $axios }, inject) => {
  const settingAPIService = new SettingAPIService($axios)
  inject('settingAPIService', settingAPIService)
}

componentからapiServiceを呼び出せるようになります。これでAPI呼び出しの詳細(axiosに関する知識とか)はpageから切り出されました。

// pages/a.vue
const setting = await this.$settingAPIService.getUserSettings()

pluginとして定義すると多少はパフォーマンスも悪化すると思いますが、自分たちが書いているコード量しかないので、pluginにしてもさほど変化はないと考えました。ここまでやらなくても、utilに用意したメソッドを呼び出してもいいと思います。

総括

pageのdataで事足りるのにVuexを採用した結果

・Publicなグローバルストアが量産されて、保守性が落ちた
・コードジャンプが効かずイライラした
・無駄にコードの記述量が増えた
・dispatchした結果を待つ時にfluxパターンが崩壊する、もしくは無駄にstateが肥大化して保守性が犠牲になる

教訓:何かを使う時は、それが想定されているユースケースをちゃんと調べよう

100日後にリファクタリングします