vuex-class-componentを使ってVuexをクラススタイルでタイプセーフに書いてみよう

こんにちは。サービスグループの武田です。

引き続きVueの勉強中です。Vueアプリケーションで状態(≒データ)を管理しようと思ったとき、選択肢はいくつかありますよね。その中でまず候補に挙がるのはVuexではないでしょうか。

ただ現状のVuexはTypeScriptとの相性がよいとは言えません。せっかくなのでクラススタイルで書きたいですし、タイプセーフに書きたいです。探してみるとサポートしてくれるモジュールがいくつか見つかりました。その中でも今回はvuex-class-componentを使ってみました。

Vuexって何?という方はぜひ公式ドキュメントを参照してください。

環境

今回の検証環境は次のような環境になっています。

$ node -v
v10.15.3

$ vue -V
3.8.2

$ sw_vers
ProductName:	Mac OS X
ProductVersion:	10.14.5
BuildVersion:	18F132

そのほか、主なモジュールのバージョンは以下となっています。また、vueコマンドがインストールされていない場合は、npm install -g @vue/cliでインストールします。

  • vue@2.6.10
  • vuex@3.1.1
  • vuex-class-component@1.6.0

プロジェクトを作成して必要なモジュールを追加

まずはVue CLIを利用してプロジェクトを作成します。

$ vue create example-vuex-counter-app-class-style

presetはManualにして、すべてデフォルトを選択します。

最後の項目を選択するとインストールが始まります。インストールが終わったら、続いてvuex-class-componentを追加します。

$ cd example-vuex-counter-app-class-style
$ npm install vuex-class-component

これで準備は完了です。

カウンタアプリケーションの実装

今回は、公式ドキュメントからもリンクされているVuexのサンプル実装をvuex-class-componentを使って実装してみます。

Edit fiddle - JSFiddle

またソースコード全体はGitHubに上げてあります。

stateの書き換え

Vuexでは管理する状態を単一オブジェクトで管理します。サンプル実装では次のように書かれています。

  state: {
    count: 0
  },

これをクラススタイルに書き換えると次のようになります。 @getterアノテーションを使っているのが特徴的です。なお元のアプリケーションではnamespaceは利用していませんが、書き換え後は使用しています。特に深い意味はないです。

@Module({ namespacedPath: 'counter/' })
export class CounterStore extends VuexModule {

  @getter
  public count: number = 0;

vuex-class-componentではstore外に公開するかどうかでプロパティを次のように宣言します。

  • 公開しない
    • private count: number = 0;
  • 公開する
    • @getter public count: number = 0;
    • @getter count: number = 0;
    • TypeScriptではpublicがデフォルトのアクセス修飾子なので省略しても同じ

mutationsの書き換え

Vuexではstateの同期的な変更処理をmutaionとして宣言します。引数にstateを取るのがポイントで、このオブジェクト経由で値を変更します。

  mutations: {
  	increment: state => state.count++,
    decrement: state => state.count--
  }

これをクラススタイルに書き換えると次のようになります。 @mutationアノテーションを使っているのが特徴的です。stateが引数で渡されることはなくなり、thisを使って直接操作できます。

  @mutation
  public increment() {
    this.count++;
  }

  @mutation
  public decrement() {
    this.count--;
  }

またmutationの呼び出し方も変わります。サンプル実装を見ても分かりますがmutationの呼び出しにはcommitメソッドを使う必要があります。

  methods: {
    increment () {
      store.commit('increment')
    },
    decrement () {
      store.commit('decrement')
    }
  }

これをクラススタイルに書き換えると次のようになります。commitメソッドは消え、またmutationが文字列ではなく普通のメソッドとして呼び出せます。素敵です。

  private increment() {
    vxm.counter.increment();
  }

  private decrement() {
    vxm.counter.decrement();
  }

vxmという見慣れないオブジェクトが登場していますが、これはREADMEで紹介されている Vuex Manager パターン です(パターンとは書かれてないんですけど)。

export const vxm = {
  counter: CounterStore.CreateProxy(store, CounterStore),
};

書き方が変わったところでmutationの呼び出しにcommitが必要なことは変わりません。そこでvuex-class-componentではプロキシクラスを提供し、プロキシのメソッドを呼び出すと内部的にcommitを呼び出してくれます。作成したプロキシをまとめて保持するオブジェクトをvxmとしてexportしているわけです。

actionsの追加

サンプル実装には書かれていませんが、Vuexでは非同期な処理はactionとして宣言します。また直接stateを変更するようなことはせず、mutationをcommitすることで間接的に変更します。例として、「countが奇数の場合のみインクリメントする」「1秒遅れてインクリメントする」という2個のactionを考えてみます。

  actions: {
    incrementIfOdd ({ commit, state }) {
      if ((state.count + 1) % 2 === 0) {
        commit('increment')
      }
    },
    incrementAsync ({ commit }) {
      setTimeout(() => {
        commit('increment')
      }, 1000)
    }
  }

そしてこれらを呼び出すコードは次のようになります。actionの呼び出しはdispatchメソッドを使います。

    incrementIfOdd () {
      store.dispatch('incrementIfOdd')
    },
    incrementAsync () {
    	store.dispatch('incrementAsync')
    }

これらをクラススタイルで書き換えたものが次のコードです。@actionアノテーションを付与しているだけで、普通のメソッドです。mutationを呼び出すためのcommitは不要となりました。

  @action()
  public incrementIfOdd() {
    if ((this.count + 1) % 2 === 0) {
      this.increment();
    }
  }

  @action()
  public async incrementAsync() {
    return setTimeout(() => this.increment(), 1000);
  }

そしてこのactionを呼び出しているのが次のコードです。dispatchメソッドは不要となり、普通のメソッド呼び出しとして書けます。もちろん内部的にはdispatchメソッドが呼ばれています。素敵です。

  private incrementIfOdd() {
    vxm.counter.incrementIfOdd();
  }

  private incrementAsync() {
    vxm.counter.incrementAsync();
  }

mutationのときと同様にvxm経由でアクセスしていますね。

まとめ

VueとVuexのどちらもまだ勉強中ですが、少しずつ理解が進んできました。Vuexをクラススタイルで書こうとするプロダクトはほかにもいくつか見つかりましたが、vuex-class-componentが一番自分にフィットしたので試してみました。引き続き精進します!