Vuex で TypeScript を使用する - inokawablog

Vuex で TypeScript を使用する

ドメイン駆動

こちらのサイトが非常に参考になりました。

複数画面共通処理に関してはstoreで状態を保持しても良いこととする。

コンポーネントやページだけで完結するデータはdataとしてコンポーネント側でもつ。

特徴

  • UIにビジネスロジックが混ざらないのでテスタビリティが高い

  • UIがサーバーサイドの影響を一切受けないので、APIの修正や置き換えに対しても柔軟に対応できる

vuex.jpg

責務

state

各種APIのレスポンスをそのまま保持する

mutations

APIレスポンスを受け取ってそれをstateに格納

actions

UI側から都度要求を受け取り、複雑なリクエストパラメータの構築を吸収

getter

VuexStoreの中身をfilterするために使う。UIにデータをフィルターして提供する唯一の場所

実装

普通に実装すると、IDEでの補完がきかないので、補完が効くようにします。

vuex-module-decoratorsをインストール

$ yarn add -D vuex-module-decorators

適当なstoreを作成します。

store/user.ts

import { Module, VuexModule, Mutation, Action } from 'vuex-module-decorators'

interface UserInterface {
  id: number
  name: string
}

// stateFactory: true → Vuex をモジュールモードで扱うために指定
@Module({ stateFactory: true, namespaced: true, name: 'user' })
export default class User extends VuexModule {
  user: Partial<UserInterface> = {}

  @Mutation
  setUserData(user: Partial<UserInterface>) {
    this.user = user
  }

  @Action({ rawError: true })
  setUser(user: Partial<UserInterface>) {
    this.setUserData(user)
  }

  get getUser(): Partial<UserInterface> {
    return this.user
  }
}

Partialは全てのパラメータを省略可能にします。つまり、以下のようになります。

interface UserInterface {
  id!: number
  name!: string
}

次に、storeにアクセスするためのアクセサを作成します。

utils/store-accessor.ts

/* eslint-disable import/no-mutable-exports */
import { Store } from 'vuex'
import { getModule } from 'vuex-module-decorators'
import User from '@/store/user'
// import New from '@/store/new'

let userStore: User
// let newStore: New

function initializeStores(store: Store<any>): void {
  userStore = getModule(User, store)
  // newStore = getModule(New, store)
}

export {initializeStores, userStore }
// export {initializeStores, userStore, newStore }

作成したアクセサを読み込みます。

store/index.ts

import { Store } from 'vuex'
import { initializeStores } from '@/utils/store-accessor'

const initializer = (store: Store<any>) => initializeStores(store)
export const plugins = [initializer]
export * from '@/utils/store-accessor'

これで,store内のactionやgetterの補完をすることができます。

コンポーネントで使用する場合は、storeを読み込んで使用します。

HogePage.vue

<template>
// sample
</template>
<script lang="ts">
import Vue from 'vue'
import { userStore } from '@/store'

export default Vue.extend({
  computed: {
    getUser() {
      return userStore.getUser
    }
  }
})

テストに関して

テストのカバレッジは、90%くらい網羅できれば良い。全部書く必要はありません。

何をモックして何をテストするかの背景を理解する方がよっぽど重要。

それぞれのテスト

state

必要ありません

mutation

パラメータに応じてstateを更新すること

action

APIリクエストパラメータを正しく組み立てること
適切なmutaionをcommitすること

getter

APIレスポンスから生成したモデルが正しく取得できていること

備考

エラーハンドリング

vuexのエラーハンドリングはコンポーネント側で行っても良いと思います。

store側でtry/catchしている場合、コンポーネントでstoreのactionを呼び出し、エラーが起こったときtry/catchをしても処理はそのまま続行されます。

store

  @Action({ rawError: true })
  setUser(user: Partial<UserInterface>) {
    try {
      this.setUserData(user)
      // some error
    } catch(err) {
      // 出力される
      console.log(err)
    }
  }

component側

methods: {
  someErrorHandling() {
    try {
      userStore.setUser(user)
    } catch(err) {
      // 出力されない
      console.log(err)
    }
  }
}

そのため、コンポーネント側でエラーハンドリングをしたい場合は、store側でエラーハンドリングをしないか、catchの中でさらにエラーを発生させるかする必要があります。

actionの使い方

Vuexはactionに複数の引数を渡すことができないので注意。複数渡したい場合は、オブジェクトで渡す。

/** ダメな例 */
@Action({ rawError: true })
async hoge(param1, param2) {
  console.log(param1) // 出力される
  console.log(param2) // 出力されない
}

this.hoge('hoge', 'fuga')
/** 良い例 */
@Action({ rawError: true })
async hoge(params: {param1, param2}) {
  console.log(params.param1) // 出力される
  console.log(params.param2) // 出力される
}

this.hoge({param1:'hoge', param2:'fuga'})

今後の展望

vue3でデフォルト実装されるsetup()を使うことでvuexのようなことを実装することができる。しかし、nuxt,typescript対応がまだまだ先だと思われるので、今のところはvuexで実装をする。