なるはや Vue SPA入門：Vuex

Vue CLI3 で作成した SPA（Single Page Application）プロジェクト上で、段階的に Vue.js を学んで行きましょう。

[目次はこちら](https://qiita.com/tsubo/items/24df49ef2f1ba87984d1)

今回は Vuex 編です。

## 前提事項

[Bootstrap 編](https://qiita.com/tsubo/items/fdee5fd998b0399546b9) が完了していること。 

## ページの追加

- Counter.vue を作成
- router.js を修正
- App.vue にナビゲーションを追加

やり方を忘れてしまった人は「[Vue Router 編](https://qiita.com/tsubo/items/fe91e95e52ee82e05681)」を振り返ってください。

```vue:src/views/Counter.vue
<template>
  <div class="counter">
    <h1>Counter</h1>
    <p>{{ count }}</p>
    <div>
      <button
        class="btn btn-sm btn-primary"
        @click="increment"
      >+</button>
      <button
        class="ml-1 btn btn-sm btn-secondary"
        @click="decrement"
      >-</button>
    </div>
  </div>
</template>

<script>
export default {
  data() {
    return {
      count: 0
    };
  },
  methods: {
    increment() {
      this.count++;
    },
    decrement() {
      this.count--;
    }
  }
};
</script>
```

Counter ページを表示して動作を確認してみてください。`+` ボタンを押すとカウントアップされて、`-` ボタンを押すとカウントダウンします。

## Vuex

Vuex はコンポーネントを横断した状態の格納やそれらを操作する為のメソッドを格納する場所です。先程作成したカウンタページを Vuex を使って書き換えてみます。

### ステートとミューテーション

```js:src/store.js
import Vue from 'vue'
import Vuex from 'vuex'

Vue.use(Vuex)

export default new Vuex.Store({
  // 状態
  state: {
    count: 0,
  },
  // ストアの状態を変更するメソッド（同期処理のみ）
  mutations: {
    increment(state) {
      state.count++
    },
    decrement(state) {
      state.count--
    },
  },
  // ストアの状態を変更するメソッド（非同期処理もOK）
  actions: {},
})
```
ステートに `count` 変数を設け、ミューテーションに `increment` と `decrement` を設けました。ステートはミューテーションを通して変更します。ただし、同期処理に限ります。

```vue:src/views/Counter.vue
<script>
export default {
  computed: {
    count() {
      // vuex 内の count ステートを参照
      return this.$store.state.count
    }
  },
  methods: {
    increment() {
      // vuex 内の increment ミューテーションを実行
      this.$store.commit('increment')
    },
    decrement() {
      // vuex 内の decrement ミューテーションを実行
      this.$store.commit('decrement')
    }
  }
};
</script>
```
vue コンポーネントでは `$store` 属性で vuex を参照することができます。ミューテーションを実行するには `$store` の `commit` メソッドを使用します。 

### ゲッター

vuex にゲッターを追加してみます。count の内容を２倍して返す `double` を設けます。

```js:store.js
export default new Vuex.Store({
  // ...
  // ゲッター
  getters: {
    double(state) {
      return state.count * 2
    },
  },
  // ...
})
```

```vue:Counter.vue
<template>
    <!-- ... -->
    <p>count: {{ count }}</p>
    <p>double: {{ double }}</p>
    <!-- ... -->
</template>

<script>
export default {
  computed: {
    count() {
      // vuex 内の count ステートを参照
      return this.$store.state.count;
    },
    double() {
      // vuex 内の double ゲッターを参照
      return this.$store.getters.double;
    }
  },
  // ...
};
</script>
```

画面の `double` 欄には `count` を２倍した値が表示されるはずです。

### アクション

アクションはミューテーションと似ていますが、下記の点で異なります

- アクションは、状態を変更するのではなく、ミューテーションをコミットします
- アクションは任意の非同期処理を含むことができます

vuex にアクションを追加してみます

```js:store.js
import Vue from 'vue'
import Vuex from 'vuex'

Vue.use(Vuex)

export default new Vuex.Store({
  // ...
  // ストアの状態を変更するメソッド（非同期処理も可、ミューテーションをコミットする）
  actions: {
    increment(context) {
      context.commit('increment')
    },
    // 引数に ES2015 の引数分割束縛を使った例（context を省略できる）
    decrement({ commit }) {
      commit('decrement')
    },
  },
})
```
アクションのメソッドは `context` オブジェクトを引数に受け取ります。`context.commit` を実行することでミューテーションをコミットできます。

```vue:Counter.vue
<script>
export default {
  // ...
  methods: {
    increment() {
      // vuex 内の increment ミューテーションを実行
      // this.$store.commit("increment");

      // vuex 内の increment アクションを実行
      this.$store.dispatch("increment");
    },
    decrement() {
      // vuex 内の decrement ミューテーションを実行
      // this.$store.commit("decrement");

      // vuex 内の decrement アクションを実行
      this.$store.dispatch("decrement");
    }
  }
};
</script>
```

アクションを実行するには `$store` の `dispatch` メソッドを使用します。

### ヘルパー

vue ファイルの中で、毎回 `this.$state.xxx` と書くの面倒ですよね。そのために、ヘルパーが用意されています。

```vue:Counter.vue
<script>
import { mapState } from "vuex"; // ステートのヘルパーをインポート
import { mapGetters } from "vuex"; // ゲッターのヘルパーをインポート
import { mapActions } from "vuex"; // アクションのヘルパーをインポート

export default {
  computed: {
    ...mapState([
      "count" // this.count を this.$store.state.count にマップ
    ]),
    ...mapGetters([
      "double" // this.double を this.$store.state.getters.double にマップ
    ])
    /* DEL mapState, mapGetters で置き換え
    count() {
      // vuex 内の count ステートを参照
      return this.$store.state.count;
    },
    double() {
      // vuex 内の double ゲッターを参照
      return this.$store.getters.double;
    }
    */
  },
  methods: {
    ...mapActions([
      "increment", // this.increment() を this.$sotre.dispatch("increment") にマップ
      "decrement" // this.decrement() を this.$store.dispatch("decrement") にマップ
    ])
    /* DEL mapActions で置き換え
    increment() {
      // vuex 内の increment ミューテーションを実行
      // this.$store.commit("increment");

      // vuex 内の increment アクションを実行
      this.$store.dispatch("increment");
    },
    decrement() {
      // vuex 内の decrement ミューテーションを実行
      // this.$store.commit("decrement");

      // vuex 内の decrement アクションを実行
      this.$store.dispatch("decrement");
    }
    */
  }
};
</script>
```

## ミューテーションやアクションへの引数の渡し方

この記事では例がシンプル過ぎた為、ミューテーションやアクションへの引数の渡し方を取り上げていません。詳細は[公式サイト](https://vuex.vuejs.org/ja/)を参照してください。

## 動作確認

![vuex.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/14476/4e71dcba-caaa-286d-4744-ab854e606397.png)

カウントアップ、カウントダウンが正しく動作しているか確認してください。devtools で vuex の内容を確認してみてください。

## 次回

[ライフサイクル](https://qiita.com/tsubo/items/a0c9ded90bba1370131c)