Vuex入門：初心者向けガイド をやってみた

以下のページがVuexの勉強でとても参考になりましたので、勉強させていただきました。m(_ _)m

- [Getting Started with Vuex: a Beginner’s Guide](https://www.sitepoint.com/vuex-beginner-guide/)

**※この投稿にかいてあることは全て↑のリンクにかいてあることと同じです。自分の備忘録として投稿させていただきました。**

# webpackで環境作成

参考サイトでは`@vue/cli`で環境作成を行っていましたが、私はwebpackで作成しました。

【[自分で試したサンプル](https://github.com/okumurakengo/vuex-guide)】

```bash:bash
yarn init -y
yarn add vue vuex
yarn add -D webpack webpack-cli webpack-dev-server vue-loader vue-template-compiler vue-style-loader css-loader babel-loader @babel/core @babel/preset-env
```

```js:./webpack.config.js
const VueLoaderPlugin = require("vue-loader/lib/plugin");

module.exports = {
    mode: "development",
    module: {
        rules: [
            {
                test: /\.vue$/,
                loader: "vue-loader"
            },
            {
                test: /\.js$/,
                loader: "babel-loader"
            },
            {
                test: /\.css$/,
                use: ["vue-style-loader", "css-loader"]
            }
        ]
    },
    plugins: [new VueLoaderPlugin()],
    resolve: {
        extensions: [".vue", ".js"],
        alias: {
            vue$: "vue/dist/vue.esm.js"
        }
    },
    devServer: {
        port: 9000,
        contentBase: "./",
        publicPath: "/dist/",
        open: "Google Chrome"
    }
};
```

```json:./.babelrc
{
    "presets": [
        [
            "@babel/preset-env",
            {
                "targets": {
                    "node": "current"
                }
            }
        ]
    ]
}
```

```js:./src/index.js
import Vue from "vue"
import App from "./App"
import store from './store'

new Vue({
    store,
    el: "#app",
    template: "<App/>",
    components: { App }
})
```

```vue:./src/App.vue
<template>
  <div id="app">
    <h1>Sample Counter</h1>
    <Counter />
  </div>
</template>

<script>
import Counter from "./components/Counter";

export default {
  components: {
    Counter
  }
};
</script>
```

```vue:./src/components/Counter.vue
<template>
  <div>
    <p>Count: {{ count }} !</p>
  </div>
</template>

<script>
export default {
  data() {
    return {
      count: 0
    };
  }
};
</script>
```

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

Vue.use(Vuex)

export default new Vuex.Store({
    state: {
        // put variables and collections here
    },
    mutations: {
        // put sychronous functions for changing state e.g. add, edit, delete
    },
    actions: {
        // put asynchronous functions that can call one or more mutation functions
    }
})
```

```html:./index.html
<!DOCTYPE html>
<meta charset=utf-8>
<title>sample</title>
<script src=dist/main.js defer></script>
<div id=app></div>
```

```bash:bash
yarn webpack-dev-server
```

この実行環境をもとにガイドを進めていきます。

<img width="612" alt="Screen Shot 2019-07-07 at 4.21.38.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/142910/1dc40432-fd7b-86b4-6b69-8e0f3f91a493.png">


# Store

アプリケーション全体で参照するデータのかたまりの部分です。

`store.js`で以下のように設定してみます。

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

Vue.use(Vuex)

export default new Vuex.Store({
    state: {
        products: [],
        count: 5,
        loggedInUser: {
            name: 'John',
            role: 'Admin'
        }
    },
})
```

### `$store`を使ってストアを参照する

storeに全てのデータを集約させるので、`<script>`タグの`data`の部分は削除します

```vue:./src/components/Counter.vue
<template>
  <div>
    <p>Count: {{ $store.state.count }} !</p>
  </div>
</template>

<script>
export default {};
</script>
```

`computed`を使うことでスッキリ書くことができます。

```vue:./src/components/Counter.vue
<template>
  <div>
    <p>Count: {{ count }} !</p>
  </div>
</template>

<script>
export default {
  computed: {
    count() {
      return this.$store.state.count;
    }
  }
};
</script>
```

<img width="612" alt="Screen Shot 2019-07-07 at 4.27.33.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/142910/bee370ac-ad3d-0235-9237-8457cefe8a43.png">

### `mapState` Helper

`mapState`を使うことで`computed`をさらに簡潔に書けます

```vue:./src/components/Counter.vue
<template>
  <div>
    <p>Welcome, {{ loggedInUser.name }}.</p>
    <p>Count: {{ count }} !</p>
  </div>
</template>

<script>
import { mapState } from "vuex";

export default {
  computed: mapState({
    count: state => state.count,
    loggedInUser: state => state.loggedInUser
  })
};
</script>
```

また、↑のようにただstoreの値とするだけなら、文字列だけを`mapState`に渡すことでさらに簡潔に書けます。
↓で同じ意味となります。

```vue:./src/components/Counter.vue
<template>
  <div>
    <p>Welcome, {{ loggedInUser.name }}.</p>
    <p>Count: {{ count }} !</p>
  </div>
</template>

<script>
import { mapState } from "vuex";

export default {
    computed: mapState([
        "count", "loggedInUser"
    ])
};
</script>
```

<img width="612" alt="Screen Shot 2019-07-07 at 4.35.31.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/142910/c2095469-de6b-093d-fd15-9cf97deb4cb2.png">

---

`computed`に`store`以外の値を渡したい場合は、スプレッド構文を使えばうまくいきます。

```vue:./src/components/Counter.vue
<template>
  <div>
    <p>Welcome, {{ loggedInUser.name }}.</p>
    <p>Count: {{ count }} ! Count is {{ parity }}.</p>
  </div>
</template>

<script>
import { mapState } from "vuex";

export default {
  computed: {
    ...mapState([
      "count", "loggedInUser"
    ]),
    parity () {
      return this.count % 2 === 0 ? "even" : "odd";
    }
  }
};
</script>
```

<img width="612" alt="Screen Shot 2019-07-07 at 4.39.44.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/142910/84810146-2acd-25f4-836b-deacad43e6c7.png">

# Getters

vuexの`sotre`で`getter`を設定すると、`computed`と同じように扱うことができます。

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

Vue.use(Vuex)

export default new Vuex.Store({
    state: {
        products: [
            { id: 1, name: "Hoge", stock: 0 },
            { id: 2, name: "Fuga", stock: 3 },
            { id: 3, name: "Piyo", stock: 0 },
        ],
        count: 5,
        loggedInUser: {
            name: 'John',
            role: 'Admin'
        }
    },
    getters: {
        depletedProducts: state => {
            return state.products.filter(product => product.stock <= 0)
        }
    },
})
```

```vue:./src/components/Counter.vue
<template>
  <div>
    <ul>
      <li v-for="(product, i) in depletedProducts" :key="i">
          name: {{ product.name }}
      </li>
    </ul>
  </div>
</template>

<script>
export default {
  computed: {
    depletedProducts() {
      return this.$store.getters.depletedProducts;
    }
  }
};
</script>
```

<img width="612" alt="Screen Shot 2019-07-07 at 4.54.27.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/142910/c55d58d0-948a-8d0e-1531-474d58d8480e.png">

### `mapGetters` Helper

こちらも`mapGetters`というヘルパーが用意されており、簡潔に書くことができます。

```vue:./src/components/Counter.vue
<template>
  <div>
    <ul>
      <li v-for="(product, i) in depletedProducts" :key="i">
        name: {{ product.name }}
      </li>
    </ul>
  </div>
</template>

<script>
import { mapGetters } from 'vuex'

export default {
  computed: {
    ...mapGetters([
      'depletedProducts',
      'anotherGetter'
    ])
  }
};
</script>
```

---

関数を返すことで、`getter`に引数を渡すこともできます。

```js
        getProductById: state => id => {
            return state.products.find(product => product.id === id);
        }
```

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

Vue.use(Vuex)

export default new Vuex.Store({
    state: {
        products: [
            { id: 1, name: "Hoge", stock: 0 },
            { id: 2, name: "Fuga", stock: 3 },
            { id: 3, name: "Piyo", stock: 0 },
        ],
        count: 5,
        loggedInUser: {
            name: 'John',
            role: 'Admin'
        }
    },
    getters: {
        depletedProducts: state => {
            return state.products.filter(product => product.stock <= 0)
        },
        getProductById: state => id => {
            return state.products.find(product => product.id === id);
        }
    },
})
```

```vue:./src/components/Counter.vue
<template>
  <div>
    product1: {{ getProductById(2).name }}
    <ul>
      <li v-for="(product, i) in depletedProducts" :key="i">
        name: {{ product.name }}
      </li>
    </ul>
  </div>
</template>

<script>
import { mapGetters } from "vuex";

export default {
  computed: {
    ...mapGetters([
        "depletedProducts", 
        "getProductById",
    ])
  }
};
</script>
```

<img width="612" alt="Screen Shot 2019-07-07 at 5.06.28.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/142910/745ab33a-54d3-6747-dc8b-0c4967007046.png">

# Mutations

`store`の内容を直接変更してはいけません。
必ず`mutations`から変更するようにしましょう。

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

Vue.use(Vuex)

export default new Vuex.Store({
    state: {
        count: 1
    },
    mutations: {
        increment(state) {
            state.count++
        }
    }
})
```

```vue:./src/components/Counter.vue
<template>
  <div>
    <p>Count: {{ $store.state.count }} !</p>
    <button @click="updateCount">increment</button>
  </div>
</template>

<script>
export default {
  methods: {
    updateCount() {
      this.$store.commit("increment");
    }
  }
};
</script>
```

<img width="612" alt="Screen Shot 2019-07-07 at 5.20.03.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/142910/20eebaca-da90-ea62-97b1-9fc9bf726c63.png">

---

パラメータを渡すこともできます

```js
  incrementBy(state, n) {
    state.count += n;
  }
```

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

Vue.use(Vuex)

export default new Vuex.Store({
    state: {
        count: 0
    },
    mutations: {
        incrementBy(state, n) {
            state.count += n;
        }
    }
})
```

```vue:./src/components/Counter.vue
<template>
  <div>
    <p>Count: {{ $store.state.count }} !</p>
    <button @click="updateCount">increment</button>
  </div>
</template>

<script>
export default {
  methods: {
    updateCount() {
      this.$store.commit('incrementBy', 25);
    }
  }
};
</script>
```

<img width="612" alt="Screen Shot 2019-07-07 at 5.23.44.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/142910/bab18462-30a7-15a4-cf23-67791c6146ad.png">

---

オブジェクトをパラメータに渡すこともできる。

```js
    mutations: {
        incrementBy(state, { amount }) {
            state.count += amount;
        }
    }
```

```js
  methods: {
    updateCount() {
      this.$store.commit('incrementBy', { amount: 25 });
    }
  }
```

---

このようなオブジェクトを渡しても大丈夫です。

```js
  methods: {
    updateCount() {
      this.$store.commit({
        type: "incrementBy",
        amount: 25
      });
    }
  }
```

### `mapMutations` Helper

`mapMutations`を使って簡潔に書くことができます。

```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++;
        },
        incrementBy(state, { amount }) {
            state.count += amount;
        },
    }
})
```

```vue:./src/components/Counter.vue
<template>
  <div>
    <p>Count: {{ $store.state.count }} !</p>
    <button @click="increment">increment</button>
    <button @click="incrementBy({ amount: 2 })">increment + 2</button>
  </div>
</template>

<script>
import { mapMutations } from "vuex";

export default {
  methods: {
    ...mapMutations([
        "increment", 
        "incrementBy"
    ])
  }
};
</script>
```

# Actions

`mutation`をcommitするときに、間に入って実行する機能です。
非同期処理を行うときはActionsからcommitします。

こちらが参考になりました
参考：https://vuex.vuejs.org/guide/actions.html#composing-actions

```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++;
        },
    },
    actions: {
        increment: context => {
            return new Promise(resolve => setTimeout(() => {
                context.commit("increment");
                resolve(context.state.count)
            }, 1000))
        }
    }
})
```

```vue:./src/components/Counter.vue
<template>
  <div>
    <p>Count: {{ $store.state.count }} !</p>
    <button @click="increment">increment</button>
  </div>
</template>

<script>
export default {
  methods: {
    async increment() {
      console.log(await this.$store.dispatch("increment"));
    }
  }
};
</script>
```

<img width="768" alt="Screen Shot 2019-07-07 at 6.12.14.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/142910/46f9cd9b-3d0f-6321-c1de-e84734d7655f.png">

---

```js
        increment: context => {
            return new Promise(resolve => setTimeout(() => {
                context.commit("increment");
                resolve(context.state.count)
            }, 1000))
        }
```

`context`は以下それぞれ受け取ることができます。

- `context.commit` 
    - `mutation`を`commit`する
- `context.state` 
    - `state`を取得
- `context.getters` 
    - `getters`を取得する

なのでこのように書くと簡潔になります

```js
        increment: ({ commit, state, getters}) => {
          // ...
        },
```

### `mapActions` Helper

```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++;
        },
    },
    actions: {
        increment: ({ commit }) => commit("increment"),
        incrementAsync: ({ commit, state }) => {
            return new Promise(resolve => setTimeout(() => {
                commit("increment");
                resolve(state.count)
            }, 1000))
        }
    }
})
```

```vue:./src/components/Counter.vue
<template>
  <div>
    <p>Count: {{ $store.state.count }} !</p>
    <button @click="increment">increment</button>
    <button @click="incrementAsync">incrementAsync</button>
  </div>
</template>

<script>
import { mapActions } from  "vuex"

export default {
  methods: {
    ...mapActions([
      "increment",
      "incrementAsync",
    ])
  }
};
</script>
```

<img width="612" alt="Screen Shot 2019-07-07 at 6.26.07.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/142910/ea54c057-f616-aa70-bbd5-d05817444357.png">

# Vuexを使ったカウンター作成

ここまでのを組み合わせたカウンターを作成します。

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

Vue.use(Vuex)

export default new Vuex.Store({
  state: {
    count: 0
  },
  getters: {
    parity: state => state.count % 2 === 0 ? "even" : "odd"
  },
  mutations: {
    increment(state) {
      state.count++;
    },
    decrement(state) {
      state.count--;
    }
  },
  actions: {
    increment: ({ commit }) => commit("increment"),
    decrement: ({ commit }) => commit("decrement"),
    incrementIfOdd: ({ commit, getters }) => getters.parity === "odd" ? commit("increment") : false,
    incrementAsync: ({ commit }) => {
      setTimeout(() => { commit("increment") }, 1000);
    }
  }
});
```

```vue:./src/components/Counter.vue
<template>
  <div>
    <p>Clicked {{ count }} times! Count is {{ parity }}.</p>

    <button @click="increment">Increment</button>
    <button @click="decrement">Decrement</button>
    <button @click="incrementIfOdd">Increment if Odd</button>
    <button @click="incrementAsync">Increment Async</button>
  </div>
</template>

<script>
import { mapState, mapGetters, mapActions } from "vuex";

export default {
  name: "Counter",
  computed: {
    ...mapState(["count"]),
    ...mapGetters(["parity"])
  },
  methods: mapActions([
    "increment",
    "decrement",
    "incrementIfOdd",
    "incrementAsync"
  ])
};
</script>
```

<img width="612" alt="Screen Shot 2019-07-07 at 6.36.42.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/142910/ce30c32c-9ba4-5881-56d0-89cfa8930270.png">

---

最後まで読んでいただいてありがとうございました。m(_ _)m