Vuex 基本的な使い方

## Prologue

Vuex 何もわからなかったので、基本的なところを学んでみました。
自分用に見返すためのものなので、基本は公式を参考にしてください。
今回は Nuxt.js と TypeScript を使用しています。

### 参考

* https://vuex.vuejs.org/ja/
* https://develop365.gitlab.io/nuxtjs-2.8.X-doc/ja/guide/vuex-store/
* https://reffect.co.jp/vue/understaind-vue-basic

## 環境

* macOS: v12.2
* node.js: v14.18.1
* terminal: iTerm
* エディタ: VS Code
* FW: Nuxt（, TypeScript） 

## プロジェクトを作成

プロジェクトを作成します。今回名前は　`sample-vuex` としています。

``` shell
yarn create nuxt-app sample-vuex
```

``` shell
create-nuxt-app v4.0.0
✨  Generating Nuxt.js project in sample-vuex
? Project name: sample-vuex
? Programming language: TypeScript
? Package manager: Yarn
? UI framework: Buefy
? Nuxt.js modules: (Press <space> to select, <a> to toggle all, <i> to invert selection)
? Linting tools: ESLint, Prettier
? Testing framework: None
? Rendering mode: Single Page App
? Deployment target: Server (Node.js hosting)
? Development tools: (Press <space> to select, <a> to toggle all, <i> to invert selection)
? Continuous integration: None
? Version control system: Git
```

## compositionAPI のインストール

compositionAPI をインストールします。

``` shell
yarn add @nuxtjs/composition-api
```
参考: https://composition-api.nuxtjs.org/getting-started/setup

nuxt.config.js 内に追加します。

```
{
  buildModules: [
    '@nuxtjs/composition-api/module'
  ]
}
```

## vuex 

この時点で `store` ディレクトリが作られています。
Nuxt では2つのモード（モジュールモード/クラシックモード）がありますが、クラシックモードは廃止予定のため、モジュールモードで作っていきます。

モジュールモードでは `store` ディレクトリ内の `js` ファイルが名前付き空間モードに変換されます。

> * デフォルトでは、モジュール内部のアクション、ミューテーション、そしてゲッターはグローバル名前空間の元で登録される
> * モジュールをより自己完結型にまた再利用可能なものにしたい場合は、それを namespaced: true によって名前空間に分けることができる
> * モジュールが登録されると、そのゲッター、アクション、およびミューテーションのすべてが、モジュールが登録されているパスに基づいて自動的に名前空間に入れられる

参考: https://vuex.vuejs.org/ja/guide/modules.html#%E5%90%8D%E5%89%8D%E7%A9%BA%E9%96%93
https://develop365.gitlab.io/nuxtjs-2.8.X-doc/ja/guide/vuex-store/

Nuxt では `store` ディレクトリ内の `js` ファイルが名前付き空間モードに変換されるので、 `namespaced:true` などは不要です。

### state

store 内に index.ts を作成します。
そこに state を作成していきます。

* `store` は、基本的にアプリケーションの 状態（state） を保持するコンテナです。
* `state` を直接変更することはできません。mutation を commit することによってのみ store の状態を変更できます。
参考: https://vuex.vuejs.org/ja/guide/

```ts:store/index.ts
export const state = () => ({
  counter: 0
})
```

pages/count.vue の setup 内で store にアクセスしてみます。

参考: https://vuex.vuejs.org/ja/guide/composition-api.html
https://composition-api.nuxtjs.org/packages/store/

```vuejs: pages/count.vue
import { defineComponent, useStore } from '@nuxtjs/composition-api'

export default defineComponent ({
  name: 'Count',
  setup() {
    const store = useStore()
    console.log(store.state.counter)
  }
})
```

log が見れたので、computed に入れて template 内で表示してみます。


```vuejs: pages/count.vue
<template>
  <div>
    {{ counter }}
  </div>
</template>

<script lang="ts">
import { computed, defineComponent, useStore } from '@nuxtjs/composition-api'

export default defineComponent ({
  name: 'Count',
  setup() {
    const store = useStore()
    const counter = computed(() => store.state.counter)

    return {
      counter
    } 
  }
})
</script>
```

#### オブジェクトの表示

同じようにオブジェクトも表示させてみます。
index.ts に animals を追加します。

```ts: store/index.ts
export const state = () => ({
  count: 0,
  animals: [
    { name: 'mike', type: 'cat' },
    { name: 'pochi', type: 'dog' },
    { name: 'mimi', type: 'cat' },
    { name: 'tama', type: 'cat' },
    { name: 'leo', type: 'dog' },
  ]
})
```

count.vue には以下のように追加します。

```vuejs: pages/count.vue
<div v-for="animal in animals" :key="animal.index">
  {{ animal.name }}
</div>
//

const animals = computed(() => store.state.animals) 

return {
  count,
  animals
}
```

設定した動物の名前が表示されます。


### getters

store の状態を算出したい時に使用します。
共通の関数を複数のコンポーネントで使用するケースなどで使えます。

参考： https://vuex.vuejs.org/ja/guide/getters.html

getters で cat のみのデータをフィルタリングします。
index.ts に以下を追加します。

```ts: store/index.ts
export const getters = {
  animals: (state) => state.animals.filter(animal => animal.type === 'cat')
}
```

count.vue で表示します。

```vuejs: pages/count.vue
<div v-for="cat in cats" :key="cat.index">
  {{ cat.name }}
</div>
//
const cats = computed(() => store.getters.animals)

return {
  count,
  animals,
  cats
} 
```

cat でフィルタリングした結果が表示されます。

### mutations

state を直接更新してはいけません。
state の更新を行うためには、mutations を使います。

state を直接変更してはいけない理由は、状態の変更を明確に追跡するためです。
参考: https://vuex.vuejs.org/ja/guide/
https://vuex.vuejs.org/ja/guide/mutations.html

mutations の引数には state が渡されます。
index.ts に mutations を追加します。

```ts: store/index.ts
export const mutations = {
  increment(state:any) {
    state.count++
  }
}
```

mutations は commit を使って実行します。

count.vue に以下を追加します。

```
<button @click="countUp">UP</button>
//
const countUp = () => store.commit('increment')

return {
  count,
  animals,
  cats,
  countUp
} 
```

UP ボタンを押すと `count` が1つずつアップすることを確認します。

#### commit に値を渡す

commit には `payload` という値を渡すことができます。
index.ts の mutations 内に以下を追加します。

```ts: store/index.ts
incrementNum(state, number) {
  state.count = state.count + number
}
```

count.vue に以下を追加します。

```vuejs: pages/count.vue
<button @click="countUpTen">10UP</button>
//
const countUpTen = () => store.commit('incrementNum', 10)

return {
  count,
  animals,
  cats,
  countUp,
  countUpTen
}
```

UP ボタンを押すと1ずつ、10UP ボタンを押すと10ずつ増えることを確認します。

### actions

* actions は mutations を経由して state を更新する
* actions には非同期の処理を入れることができる
参考: https://vuex.vuejs.org/ja/guide/actions.html

actions の中で commit を使って mutations を実行して、 mutations から count を更新します。
index.ts に actions を追加します。

```ts: store/index.ts
export const actions = {
  increment(context) {
    context.commit('increment')
  }
}
```

Count.vue の increment 関数を以下のように変更します。
actions を実行するのは `dispatch` を使います。

```vuejs: pages/count.vue
const countUp = () => store.dispatch('increment')
```

動作に変更がないことを確認します。


## TS のエラーを解消

ここまで TS のエラーを無視してきたので、その解消を行います。

index.ts 内の getters で起こっているエラー

```
Parameter 'state' implicitly has an 'any' type.
```
↓
state の型が any ということで、state の型を作成します。

```ts: store/index.ts
type State = {
  count: number
  animals:[{name: string, type: string}]
}

export const getters = {
  animals: (state:State) => state.animals.filter(animal => animal.type === 'cat')
}
```

mutations の引数（state, number）でも同じことが起こっているため、追加します。

```
export const mutations = {
  increment(state:State) {
    state.count++
  },
  incrementNum(state:State, number:number) {
    state.count = state.count + number
  }
}
```

### context

参考: https://typescript.nuxtjs.org/ja/cookbook/store/#%E5%9F%BA%E6%9C%AC%E7%9A%84%E3%81%AA%E3%82%BF%E3%82%A4%E3%83%94%E3%83%B3%E3%82%B0
（「基本的なタイピング」の項）

index.ts に追加します。

```ts: store/index.ts
export type RootState = ReturnType<typeof state>

export const actions:ActionTree<RootState,RootState> = {
    increment(context) {
        context.commit('increment')
    }
}
```

### vue ファイル

store.state で以下のようなエラーが起こっています。

```
Object is of type 'unknown'.
```

index.ts の type State を export して以下のように追加

```vuejs: pages/count.vue
const count = computed(() => (store.state as State).count)
const animals = computed(() => (store.state as State).animals)
```

こちらでも解消されます。

```vuejs: pages/count.vue
const store = useStore<State>()

```

ここまでで TS エラーは解消されました。動作に変更がないことを確認します。

## その他

actions の commit は以下のように短縮して書くことも可能です。

```ts: store/index.ts
increment({commit}) {
    commit('increment')
},
```

また、引数を使って書く場合には以下のようになります。
incrementNum も actions に書いてみました。

```ts: store/index.ts
incrementNum({commit}, num:number) {
    commit('incrementNum', num)
},
```

### 名前空間付きモジュール

参考: https://develop365.gitlab.io/nuxtjs-2.8.X-doc/ja/guide/vuex-store/
https://vuex.vuejs.org/ja/guide/modules.html
（「モジュールモード」の項参考）

これまで使ってきた `store/index.ts` を `store/count.ts` に rename します。
名前がつくことによりネストが1階層増えるので、以下のように書き換えていきます。

* state は `store.state.count` で対応する
  * count で `Property 'count' does not exist on type 'State'.` というエラーが出る
  * State の型とは別に、名前空間で使う type を作成する
    ```ts: store/count.ts
    export type TypeState =  { 
      count: {
        countNum: number
        animals:[{name: string, type: string}]
      }
    }
    ```
    これを count.vue で使用する

    ```
    const store = useStore<TypeState>()
    ```
* actions は `count/increment` で対応
* getters は `['count/animals']` で対応

state で使っていた `count`　という名前が重複してわかりにくくなったため、 `countNum` に変更し、全体の内容は以下のような感じになりました。

```vuejs: pages/count.vue
const store = useStore<TypeState>()
const count = computed(() => store.state.count.countNum)
onMounted(() => {
  store.dispatch('count/resetCount')
})

const animals = computed(() => store.state.count.animals) 
const cats = computed(() => store.getters['count/animals'])

const countUp = () => store.dispatch('count/increment')
const countUpTen = () => store.dispatch('count/incrementNum', 10)
```

同じように animal の store も分けます。
新しく `store/animal` を作成したものが以下となります。

```ts: store/animal.ts
import { ActionTree } from 'vuex'

export type State = { 
  animals:[{name: string, type: string}]
}

export type AnimalState =  { 
  animal: {
    animals:[{name: string, type: string}]
  }
}

export const state = () => ({
  animals: [
    { name: 'mike', type: 'cat' },
    { name: 'pochi', type: 'dog' },
    { name: 'mimi', type: 'cat' },
    { name: 'tama', type: 'cat' },
    { name: 'leo', type: 'dog' },
  ]
})

export const getters = {
  animals: (state:State) => state.animals.filter(animal => animal.type === 'cat')
}
```

count.vue は以下のようになります。
名前空間で作った count.ts の type 名も修正しています。

```vuejs: pages/count.vue
const countStore = useStore<CountState>()
const animalStore = useStore<AnimalState>()
const count = computed(() => countStore.state.count.countNum)
onMounted(() => {
  countStore.dispatch('count/resetCount')
})
const animals = computed(() => animalStore.state.animal.animals) 
const cats = computed(() => countStore.getters['animal/animals'])

const countUp = () => countStore.dispatch('count/increment')
const countUpTen = () => countStore.dispatch('count/incrementNum', 10)
```

以上となります。

最終的な画面はこんな感じになりました。

![スクリーンショット 2022-03-19 12.18.11.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/558989/b402c8c9-b2cb-65ef-f753-1310eed5011c.png)

## Epilogue　　　　　　　　　　　　　

Vue ではなく Nuxt を選んだ意味としては以下となります。
* Vuex のインストールや設定周りを意識しなくて済むこと（Nuxt CLI でプロジェクト作成するとすぐ使える）
* 名前空間も Nuxt の方で行ってくれること

以上の理由により Vuex の使い方を学ぶことに集中できました。楽でしたね...
TS エラー周りは割と苦戦しました。おそらくベストなものではないと思います。レビューや提案いただけると助かります。
慣れたら Vue の方もやってみたいなと思います。