Vuex ハンズオン

### 前提

端末: MacBook Pro
OS: macOS Catalina 10.15.7
Node.js: v12.18.3
Vue.js: v2.6.11
Vue-CLI: v4.5.13

Node.js, yarn, Vue 既にインストール済み
Vue-CLI を利用して、プロジェクトを作成済み

## Vuex とは

Vue.js アプリケーション用の状態管理ライブラリのことを指す
データの状態管理を一元化して開発効率を上げることを目的として、
アプリケーションで扱うデータセットを Store と呼ばれる領域で一元管理することで、
各コンポーネントは Store にアクセスすれば常に共通の値を参照することができるようになる

## どんな時に使っているのか

中規模から大規模のシステム、 SPA を構築時に使用することを推奨
複数のページにおいて、共通利用する情報を保持したい際に使用する
共通利用する値なのにも関わらず、コンポーネントが親、子、孫、ひ孫・・という関係になった際に、
必要な情報を全て props でバケツリレーするのは非常に面倒であり、バグの元となる
上記の状態になった際に vuex を使用して、どのコンポネからも取得、変更をすることを可能にする

## セットアップ 

yarn でインストール
（Vue-CLI でプロジェクト作成した場合だと、既にインストール済みの可能性あり）

```terminal
$ yarn add vuex
```

package.json に定義されているか確認
※npm, yarn で何かインストールした際は、逐一確認する癖をつけておくと細かいミスが減る

```package.json
{
  "dependencies": {
    "core-js": "^3.6.5",
    "vue": "^2.6.11",
    "vuex": "^3.6.2"
  },
}
```

## Vuex におけるストア

ストアは主にアプリケーションの状態を保持する役割を担う為、 Vuex の根幹とある
ストアを作成するにあたり、定義できる項目は以下の 4 つ

| 項目名 | 概念 |
|:-----------|------------:|
| state | ストアで管理するデータ定義 | 
| getters | state 内のデータから計算された値を返却する |
| mutations | state 内のデータを更新する| 
| actions | mutations の操作を各コンポーネントから呼び出す |

## ストアの state を使用してみる

store.js を /src 配下に作成する

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

// モジュールとして利用する場合は Vue.use() によって明示的に導入する必要がある
Vue.use(Vuex)

// Vuex アプリの中心にあるストアの作成
export default new Vuex.Store({
  state: {
    count: 0
  },
})
```

main.js を修正する

```main.js
import Vue from 'vue'
import App from './App.vue'
import store from './store'

Vue.config.productionTip = false

new Vue({
  // Vue インスタンス生成時にストア情報を定義する
  store,
  render: h => h(App),
}).$mount('#app')
```

App.vue を修正する

```App.vue
<template>
  <div id="app">
    <img alt="Vue logo" src="./assets/logo.png">
    <h1>Hello Vuex Hands on!</h1>

    <div>
      <h2>STORE</h2>
      {{ showCount }}
    </div>
  </div>
</template>

<script>
export default {
  name: 'App',

  computed: {
    // ストアから値を取り出すには computed で返却することが推奨されている
    // （ストア自体はリアクティブのため値が変更時に更新される目的）
    showCount() {
      // state の値は $store から取得可能
      return this.$store.state.count
    }
  },
}
</script>
```

下記のコマンドを叩いてローカルサーバーを起動

```terminal
$ yarn serve
```

ストアに定義した count の値が表示できていることが確認できる

![vuejs_hands_on_vuex_store.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/441218/f96750f0-7fd8-c83a-b275-a4d646e3c514.png)

Vue.js のデベロッパーツールにも Vuex のタブが存在し、
ストアに count が定義されていることが確認できる

![vuejs_hands_on_vuex_devtool.png.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/441218/b4051ea2-91ea-dacf-843a-4caaf2baf2c5.png)

複数の steta の値を使用する際に、全て computed に定義するのは冗長のため、
`mapState` ヘルパーを使用することを推奨している

App.vue を修正する

```App.vue
<template>
  <div id="app">
    <img alt="Vue logo" src="./assets/logo.png">
    <h1>Hello Vuex Hands on!</h1>

    <div>
      <h2>STORE</h2>
      {{ count }}
    </div>
  </div>
</template>

<script>
import { mapState } from 'vuex'

export default {
  name: 'App',

  computed: {
    // state のプロパティ名を指定する
    ...mapState['count'],
  },
 }
 </script>
```

## ストアの getters を使用してみる

ストアに getters を定義しておくことで、 Vuex 内にて算出プロパティー（computed）として使用することが可能

store.js を修正する

```store.js
// Vuex アプリの中心にあるストアの作成
export default new Vuex.Store({
  // 定義したデータは、どのコンポーネントからも使用することが可能
  state: {
    count: 2
  },

  // computed の様に state の値を算出することが可能
  getters: {
    doubleCount: state => state.count * 2,
    tripleCount: state => state.count * 3
  }
})
```

App.vue を修正する

```App.vue
<template>
  <div id="app">
    <img alt="Vue logo" src="./assets/logo.png">
    <h1>Hello Vuex Hands on!</h1>

    <div>
      <h2>STORE</h2>
      STATE: {{ count }}<br>
      GETTER: {{ doubleCount }}, {{ tripleCount }}
    </div>
  </div>
</template>

<script>
import { mapState, mapGetters } from 'vuex'

export default {
  name: 'App',

  computed: {
    ...mapState(['count']),
    ...mapGetters(['doubleCount', 'tripleCount']),
  },
}
</script>
```

ストアに定義した getters の値が表示できていることが確認できる

![vuejs_hands_on_vuex_getters.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/441218/7ee7b1a8-71fd-6267-d881-44984424ed8c.png)

デベロッパーツールの Vuex のタブにも、
getters の値が定義されていることが確認できる

![vuejs_hands_on_vuex_getters_devtool.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/441218/f2e35bda-b2f6-af72-3ad7-f983e99e3746.png)

## ストアの mutations を使用してみる

ストアの state の値を変更、更新する際に、下記のように直接更新・削除を行なうことを禁止されており、
mutations のみが行える原則

```javascript
created() {
  // やろうと思えばできなくはない（エラー等も発生しない）が非推奨
  this.$store.state.count = 100 
}
```

store.js を修正する

```store.js
// Vuex アプリの中心にあるストアの作成
export default new Vuex.Store({
  // 定義したデータは、どのコンポーネントからも使用することが可能
  state: {
    count: 2
  },

  // computed の様に state の値を算出することが可能
  getters: {
    doubleCount: state => state.count * 2,
    tripleCount: state => state.count * 3
  },

  // ストアの state の値を変更、更新する場合は mutations を使用する
  // mutations 内で行う処理は全て同期的にする必要がある（非同期の場合は actions を使用する）
  mutations: {
    // 呼び出し元にて store.commit の第二引数に値を定義すると、ハンドラーの第二引数に渡される
    // 上記の値のことをペイロードと呼ぶ仕様。ペイロードはオブジェクトで渡されることが推奨されている
    update(state, payload) {
      state.count = payload.value
    }
  }
})
```

App.vue を修正する

```App.vue
<script>
import { mapState, mapGetters, mapMutations } from 'vuex'

export default {
  name: 'App',

  created() {
    this.update({
      value: 30
    })
  },

  computed: {
    ...mapState(['count']),
    ...mapGetters(['doubleCount', 'tripleCount']),
  },

  methods: {
    ...mapMutations(['update']),
  }
}
</script>
```

ストアに定義した stete, getters の値が更新できていることが確認できる

![vuejs_hands_on_vuex_mutations.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/441218/75c86c0b-dcc9-ab9c-8da6-983c8d8879e6.png)

デベロッパーツールの Vuex のタブにも、
mutations の値が定義、呼ばれていることが確認できる

![vuejs_hands_on_vuex_mutations_devtool.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/441218/95a1a6f8-255e-5155-a821-4983969cd4af.png)

## ストアの actions を使用してみる

actions は状態を変更するのではなく、 mutations をコミットする役割
外部 API とのやり取りや、非同期処理を含む際は actions に定義する

store.js を修正する

```store.js
// Vuex アプリの中心にあるストアの作成
export default new Vuex.Store({
  // 定義したデータは、どのコンポーネントからも使用することが可能
  state: {
    count: 2
  },

  // computed の様に state の値を算出することが可能
  getters: {
    doubleCount: state => state.count * 2,
    tripleCount: state => state.count * 3
  },

  actions: {
    update(context) {
      // actions ハンドラにおける context.commit を呼び出すことで mutations にコミットすることが可能
      context.commit('update', { value: 30 })
    }
  },

  // ストアの state の値を変更、更新する場合は mutations を使用する
  // mutations ないで行う処理は全て同期的にする必要がある（非同期の場合は actions を使用する）
  mutations: {
    // 呼び出し元にて store.commit の第二引数に値を定義すると、ハンドラーの第二引数に渡される
    // 上記の値のことをペイロードと呼ぶ仕様。ペイロードはオブジェクトで渡されることが推奨されている
    update(state, payload) {
      state.count = payload.value
    }
  }
})
```

App.vue を修正する

```App.vue
<script>
import { mapState, mapGetters, mapActions } from 'vuex'

export default {
  name: 'App',

  created() {
    this.update()
  },

  computed: {
    ...mapState(['count']),
    ...mapGetters(['doubleCount', 'tripleCount']),
  },

  methods: {
    ...mapActions(['update'])
  }
}
</script>
```

ストアに定義した stete, getters の値が actions 経由で更新できていることが確認できる

![vuejs_hands_on_vuex_actions.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/441218/4a11ace7-d82d-cb51-351f-bb23630aa1b7.png)

デベロッパーツールの Vuex のタブにも、
mutations の値が actions 経由で定義、呼ばれていることが確認できる

![vuejs_hands_on_vuex_actions_devtool.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/441218/16d16f6a-8a69-02d8-fbd6-ca4044bd91fe.png)

コンポーネントから、 actions に引数を渡したい場合は、第二引数の payload を定義する
store.js を修正する

```store.js
actions: {
  // コンポーネントから引数を受け取る場合は、第二引数の payload にて取得可能
  update(context, payload) {
    // actions ハンドラにおける context.commit を呼び出すことで mutations にコミットすることが可能
    context.commit('update', { value: payload.value })
  }
},

// ストアの state の値を変更、更新する場合は mutations を使用する
// mutations ないで行う処理は全て同期的にする必要がある（非同期の場合は actions を使用する）
mutations: {
  // 呼び出し元にて store.commit の第二引数に値を定義すると、ハンドラーの第二引数に渡される
  // 上記の値のことをペイロードと呼ぶ仕様。ペイロードはオブジェクトで渡されることが推奨されている
  update(state, payload) {
    state.count = payload.value
  }
}
```

App.vue を修正する

```App.vue
<script>
export default {
  name: 'App',

  created() {
    this.update({
      value: 30
    })
  },

  computed: {
    ...mapState(['count']),
    ...mapGetters(['doubleCount', 'tripleCount']),
  },

  methods: {
    ...mapActions(['update'])
  }
}
</script>
```

コンポーネントから actions へ渡した引数で更新できていることが確認できる


![vuejs_hands_on_vuex_actions.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/441218/4a11ace7-d82d-cb51-351f-bb23630aa1b7.png)

## ストアをモジュールに分割する

アプリケーションが大きくなるにつれ、ストアで管理したいデータも増えていく
そういった際に、ストアをモジュールで分割することが可能（koto や luxa でも対応済み）

今まで使用していた store.js から下記のソースを抜き出し、 count.js を作成する
（store 専用のディレクトリを作成するのが好ましいが、今回は /src 配下に作成する）

```count.js
export default {
  // 下記の定義がないと、各コンポーネントから参照する際にエラーが発生する
  namespaced: true,

  // 定義したデータは、どのコンポーネントからも使用することが可能
  state: {
    count: 2
  },

  // computed の様に state の値を算出することが可能
  getters: {
    doubleCount: state => state.count * 2,
    tripleCount: state => state.count * 3
  },

  actions: {
    update(context) {
      // actions ハンドラにおける context.commit を呼び出すことで mutations にコミットすることが可能
      context.commit('update', { value: 30 })
    }
  },

  // ストアの state の値を変更、更新する場合は mutations を使用する
  // mutations ないで行う処理は全て同期的にする必要がある（非同期の場合は actions を使用する）
  mutations: {
    // 呼び出し元にて store.commit の第二引数に値を定義すると、ハンドラーの第二引数に渡される
    // 上記の値のことをペイロードと呼ぶ仕様。ペイロードはオブジェクトで渡されることが推奨されている
    update(state, payload) {
      state.count = payload.value
    }
  }
}
```

store.js を修正する

```store.js
import Vue from 'vue'
import Vuex from 'vuex'
import count from './count';

// モジュールとして利用する場合は Vue.use() によって明示的に導入する必要がある
Vue.use(Vuex)

// Vuex アプリの中心にあるストアの作成
export default new Vuex.Store({
  modules: {
    count
  }
})
```

App.vue を修正する

```App.vue
<script>
import { mapState, mapGetters, mapActions } from 'vuex'

export default {
  name: 'App',

  created() {
    this.update()
  },

  computed: {
    // 第一引数に読み込むモジュールを定義する
    ...mapState('count', ['count']),
    ...mapGetters('count', ['doubleCount', 'tripleCount']),
  },

  methods: {
    ...mapActions('count', ['update'])
  }
}
</script>
```

問題なく、 ストアのデータを更新、取得できていることが確認できる

![vuejs_hands_on_vuex_module.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/441218/9a8140b5-46bb-c50f-4f27-537573e722be.png)