TypescriptでVuex。direct-vuexでシンプルに

## Vuex with T

TypescriptでVuexを使うためのライブラリはいろいろあって、悩みどころです。
その中の一つ、[direct-vuex](https://www.npmjs.com/package/direct-vuex)は普通のVuexの書き方に近いので、Vuexのサイトを見たことがある人なら理解しやすいと思います。

## カウンターを作ってみる

Vue Cli (@vue/cli) で作ったプロジェクトで、定番のカウンターを作ってみます。
説明がほぼありませんが、ソースコードを見ればわかると思います。

### ストアの作成

`state`の書き方に制限があります。[参照](https://www.npmjs.com/package/direct-vuex#a-limitation-on-how-to-declare-a-state)

```ts:src/store/index.ts
import Vue from 'vue'
import Vuex from 'vuex'
import { createDirectStore } from 'direct-vuex'

Vue.use(Vuex)

export interface CounterState {
  count: number
}

const { store, rootActionContext, moduleActionContext } = createDirectStore({
  state: (): CounterState => {
    return {
      count: 0,
    }
  },
  mutations: {
    INCREMENT(state) {
      state.count += 1
    },
  },
  actions: {
    // 値を返す時は、戻り値の型を書く必要がある
    increment(context): number {
      const { commit, state } = rootActionContext(context)
      commit.INCREMENT()
      return state.count
    },
  },
})

export default store
export { rootActionContext, moduleActionContext }

export type AppStore = typeof store
declare module 'vuex' {
  interface Store<S> {
    direct: AppStore
  }
}
```

### ストアの登録箇所を変更

`store.original`とする必要があります。

```ts:src/main.ts
import Vue from 'vue'
import App from './App.vue'
import router from './router'
import store from './store'

Vue.config.productionTip = false

new Vue({
  router,
  store: store.original, // 変更
  render: (h) => h(App),
}).$mount('#app')
```

### コンポーネントの作成

ポイントは、`this.$store.direct`で型付けされたラッパーを取り出すところです。

```vue:src/components/Counter.vue
<template>
  <div>
    <span>{{ count }}</span>
    <button @click="increment">+</button>
  </div>
</template>

<script lang="ts">
import Vue from 'vue'
export default Vue.extend({
  computed: {
    count() {
      return this.$store.direct.state.count
    },
  },
  methods: {
    async increment() {
      const count = await this.$store.direct.dispatch.increment()
      console.log(count)
    },
  },
})
</script>
```

### 実行

とりあえず動作確認のため、`src/views/Home.vue`にCounterを２つ張り付けてみます。

```vue:src/views/Home.vue
<template>
  <div class="home">
    <Counter />
    <Counter />
    <img alt="Vue logo" src="../assets/logo.png" />
    <HelloWorld msg="Welcome to Your Vue.js App" />
  </div>
</template>

<script>
// @ is an alias to /src
import HelloWorld from '@/components/HelloWorld.vue'
import Counter from '@/components/Counter.vue'

export default {
  name: 'home',
  components: {
    HelloWorld,
    Counter,
  },
}
</script>
```

![direct-vue3.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/548641/7b5b8d2d-883b-6181-c42f-1149d671a6a7.gif)

うまく動作しています。

## カウンターのストアをモジュールに切り出してみる

`src/store/counter.ts`にモジュールを作成します。

```ts:src/store/counter.ts
import { createModule } from 'direct-vuex'
import { moduleActionContext } from './index'

export interface CounterState {
  count: number
}

const counter = createModule({
  namespaced: true,
  state: (): CounterState => {
    return {
      count: 0,
    }
  },
  mutations: {
    INCREMENT(state) {
      state.count += 1
    },
  },
  actions: {
    increment(context): number {
      const { commit, state } = counterActionContext(context) // rootCommitなどもあります
      commit.INCREMENT()
      return state.count
    },
  },
})

export default counter
export const counterActionContext = (context: any) => moduleActionContext(context, counter)
```

### モジュールを登録

`src/store/index.ts`を書き換えて、`modules`で登録します。

```ts:src/store/index.ts
import Vue from 'vue'
import Vuex from 'vuex'
import { createDirectStore } from 'direct-vuex'
import counter from './counter'

Vue.use(Vuex)

const { store, rootActionContext, moduleActionContext } = createDirectStore({
  modules: {
    counter,
  },
})

export default store
export { rootActionContext, moduleActionContext }

export type AppStore = typeof store
declare module 'vuex' {
  interface Store<S> {
    direct: AppStore
  }
}
```

### コンポーネントの変更

`namespaced: true`でモジュールを作ったので、`namespace`経由でアクセスするように変更して完成です。

```vue:src/components/Counter.vue
<template>
  <div>
    <span>{{ count }}</span>
    <button @click="increment">+</button>
  </div>
</template>

<script lang="ts">
import Vue from 'vue'
export default Vue.extend({
  computed: {
    count() {
      return this.$store.direct.state.counter.count
    },
  },
  methods: {
    async increment() {
      const count = await this.$store.direct.dispatch.counter.increment()
      console.log(count)
    },
  },
})
</script>
```

## Composition API を使う場合

ストアのラッパーを`context.root.$store.direct`で取得するだけです。

```vue:src/components/Counter.vue
<template>
  <div>
    <span>{{ count }}</span>
    <button @click="increment">+</button>
  </div>
</template>

<script lang="ts">
import Vue from 'vue'
import VueCompositionApi, { createComponent, computed } from '@vue/composition-api'
Vue.use(VueCompositionApi) // src/main.ts とかに書きます。

export default createComponent({
  setup(props, context) {
    const store = context.root.$store.direct

    const count = computed(() => store.state.counter.count)
    async function increment() {
      const count = await store.dispatch.counter.increment()
      console.log(count)
    }

    return {
      count,
      increment,
    }
  },
})
</script>
```

## まとめ

簡単。