頑張らないオレオレVuex規約を作った話

# はじめに

この記事は、[Vue.js Advent Calendar 2019 #20日目](https://qiita.com/advent-calendar/2019/vue) の記事です。
担当は @is_ryo でお送りします。

## Vuexの治安悪すぎ問題

みなさん Vuex の設計ってどうされていますか？
規約を設けずに自由な感じで Vuex を使っているとこんなことありませんか？

- 巨大化する State…
- 直接呼ばれるmutation…
- getters？actions？なにそれおいしいの？
- modulesが分けられてない…
- とりあえず Vuex に突っ込んでおくかーみたいな風潮

といった悩みを抱えてませんか？私は日々悩まされています…
Vuex はかなり便利でいろんな使い方ができるので治安が悪くなりがちなのかなと思います。
実際に私のチームでも Vuex 周りの治安がかなり悪くなってしまいました…

## 規約を作ろう

やっぱり治安を良くするためにはまず規約を作らないといけませんよね。
でもあんまりがちがちな規約を作ってしまうと守ってもらえなくなってしまう…
なので下記3点を解決できるような、かつあんまり頑張らない規約を作りました。

- Storeの中身を整理したい
- 整理した上でComponentがどのStoreを使っているのかわかりやすくしたい
- Storeに `Store.hoge` でアクセスしたくない

## どんなの作った？

ざっと下記の5つ

- `stores/` 以下にVuex関係のソースを置く
- namespaceを切って、それぞれのnamespace(Module)ファイルを生成する
- Moduleの構成要素は、`namespaced / state / mutations / actions (/ getters)`
- 直接 `mutations` を使わない
- createNamespacedHelpersを使って、Storeとやり取りする

## `stores/` 以下にVuex関係のソースを置く

ディレクトリ構成はこんな感じ

```
src/stores/
├── flags.ts
├── store.ts
└── types.d.ts
```

Storeの親ファイル。`modules` 内にModuleファイルをimportしていく。
modules内にModuleを入れていく。

```ts
import Vue from "vue"
import Vuex, { StoreOptions } from "vuex"
import { RootState } from "./types"
import { flags } from "./flags"

Vue.use(Vuex)

const store: StoreOptions<RootState> = {
  state: {
    version: ""
  },
  modules: {
    flags  // ←ここにModuleを突っ込む。この場合のnamespace名はflags
  }
}

export default new Vuex.Store<RootState>(store) // ←Storeを生成
```

### flags.ts

StoreのModuleファイル。ここにModuleの内容を書いていく。
Moduleを変数に入れてexportする。そのときの変数名がnamespace名となる。

```ts
import { Module, ActionContext } from "vuex"
import { FlagsState, RootState } from "./types"

export const flagsNamespacedHelper = createNamespacedHelpers("flags")

export const flags: Module<FlagsState, RootState> = {
  /* 省略 */
}
```

### types.d.ts

Store関係の型ファイル。

## namespaceを切って、それぞれのnamespace(Module)ファイルを生成する

すべてひとつのStoreとして扱うとStateがゴミ箱みたいになってくるので、namespaceを切ります。
namespaceを切ることでModuleとしてStoreを分割することができます。
Storeで扱いたいデータの種別ごとで切るといい感じになると思います。
例えばフラグ関係とか描画するデータ関係とか…

## Moduleの構成要素は、`namespaced / state / mutations / actions (/ getters)`

Moduleの構成要素は、`namespaced / state / mutations / actions (/ getters)`になります。(gettersは必要ないかも)
例: `stores/flags.ts`

```ts
import { Module, ActionContext, createNamespacedHelpers } from "vuex"
import { FlagsState, RootState } from "./types"

export const flagsNamespacedHelper = createNamespacedHelpers("flags")

export const flags: Module<FlagsState, RootState> = {
  namespaced: true,
  state: {
    hiddenToolbarItems: false,
    overlay: false
  },
  mutations: {
    setHiddenToolbarItems(state: FlagsState, value: boolean): void {
      state.hiddenToolbarItems = value
    },
    setOverlay(state: FlagsState, value: boolean): void {
      state.overlay = value
    }
  },
  getters: {
    hiddenToolbarItems(state: FlagsState): boolean {
      return state.hiddenToolbarItems
    },
    overlay(state: FlagsState): boolean {
      return state.overlay
    }
  },
  actions: {
    setHiddenToolbarItems(
      { commit }: ActionContext<FlagsState, RootState>,
      value: boolean
    ) {
      commit("setHiddenToolbarItems", value)
    },
    setOverlay(
      { commit }: ActionContext<FlagsState, RootState>,
      value: boolean
    ) {
      commit("setOverlay", value)
    }
  }
}
```

## 直接 `mutations` を使わない

直接 `mutations` を使わないようにします。要するに `Store.commit("hoge")` って書かないようにします。
あくまで個人の感覚なんですが `mutations` はprivateな関数として扱った方がいいのかなって思っています。
逆に `actions` はpublicな関数として扱うという形で `mutations` と `actions` の棲み分けをします。
`actions` 経由で `mutations` を使いましょう。書き方は下記のような感じです。

```ts
Store.dispatch("hogehoge")
```

## NamespacedHelpersを使って、Storeとやり取りする

というかそもそも `Store.hoge` って書くのをやめましょう。
毎回 `import Store from "@/stores/store"` って書くのめんどくさすぎるし、どのModuleを使ってるのかわからないくて困るし、namespaceを切っているので、namespaceを間違ったり書き忘れたりするので…
そういうときに役立つのが `NamespacedHelpers` です。
Moduleを定義しているファイルの中で `createNamespacedHelpers()` を定義して export します。

```ts
import { createNamespacedHelpers } from "vuex"

export const flagsNamespacedHelper = createNamespacedHelpers("flags")
```

こうやってModuleに対してやり取りをできるようにできます。
Stateを購読する場合は、computedでmapStateを展開します。スプレッド構文で書く必要があるので注意。展開したあとは `this` から参照できます。
例: `App.vue` （抜粋）

```ts
import Vue from "vue"
import { createNamespacedHelpers } from "vuex"
import { flagsNamespacedHelper } from "@/stores/flags"

export default Vue.extend({
  computed: {
    ...flagsNamespacedHelper.mapState({
      hiddenToolbarItems: (state: any) => state.hiddenToolbarItems,
      overlay: (state: any) => state.overlay
    })
  }
})
```

Actionを使う場合は、methodsにmapActionsを展開します。こちらもスプレッド構文で書く必要があります。また、一度変数に入れないと型推論をしてくれないので注意。展開したあとは `this` から参照できます。
例: `viwes/home/script.ts` （抜粋）

```ts
import Vue from "vue"
import { HomeComponentState } from "@/types"
import { flagsNamespacedHelper } from "@/stores/flags"

// Vuex store helpers
const flagsMapActions = flagsNamespacedHelper.mapActions([
  "setHiddenToolbarItems",
  "setOverlay"
])

export default Vue.extend({
  data(): HomeComponentState {
    return {
      text: "hoge"
    }
  },
  created() {
    this.setHiddenToolbarItems(true)
    this.setOverlay(false)
  },
  methods: {
    ...flagsMapActions,
    getText(): string {
      return this.text
    }
  }
})
```

## まとめ

今回作った規約をまとめると、

- ちゃんとModule単位に分割することでStoreを整理する
- NamespacedHelperを使うことでComponentがどのModuleを使っているのかimportでわかりやすくする
- `mapState` と `mapActions` を使ってStoreにアクセスする

といったところを重きに置いてます。
規約はそれぞれのチームでどのような規約が必要になってくるのか変わってくると思います。
これから規約を作らないと…という感じで困っている人の参考になれば嬉しいです！
ではまた！！！