第4回 – Vuex入門 – パート1

2019.05.27

妻と娘が里帰りして、一人と一匹のわんこで毎日生活している、むつたくです。
ワーワーキャーキャー騒がしかった日々が懐かしいです。
居なくなるとシーン…としてまして、世の一人暮らしをしている方々はよく耐えられるな、
と常々思ってしまいます。

 

あ、でも!静かでは無いですよ!
わんこのイビキが聞こえてますのでw

 


 

 

さて、今回はVue.jsに話を戻しまして、Vuexについて触れていこうと思います。
Vuexは覚えるのは大変ですが、中〜大規模のプロジェクトでは、真価を発揮すると思いますので
習得していて絶対損は無いです。

 

小規模では、あまり発揮しませんが、最初から導入しておくことで、改修に追加に…と、
どんどん規模が大きくなってきても、対応出来ますので、必要であれば、導入しておくことを
オススメします。

 

 

 

やること

  1. Vuexとは?
  2. 状態管理パターン
  3. Vuexを使用する (State / Mutations / Actinos)

 

 

(1) Vuexとは?

 

公式の言葉を拝借しますと、

 

Vuex は Vue.js アプリケーションのための 状態管理パターン + ライブラリです。 これは予測可能な方法によってのみ状態の変異を行うというルールを保証し、アプリケーション内の全てのコンポーネントのための集中型のストアとして機能します。

 

うーん…、何を言っているのだろう、となりますが、簡単に言いますと、
データの管理を一元化するためのライブラリです。

 

数えられる程度のコンポーネントで、それを利用したアプリケーションであれば問題ないのですが、
これが数十個になってくると、このコンポーネントでは今はこのデータを扱っていて、あのコンポーネントはあのデータで、あのデータをそのコンポーネントに渡して…とかやってますと、管理が大変ですよね?

 

親コンポーネントから子、孫、ひ孫コンポーネントに(その逆も然り)データ受け渡しとかのバケツリレー(propsや$emit)も面倒だと思います。

 

それらを解決してくれるのが、Vuexとなります。
自分がどの階層にいて親コンポーネントはこれで、子コンポーネントはあれで、なんて考えなくて済みます。データを一元管理してくれますので。
StoreのStateと呼ばれるところにデータを集約し、それを介してコンポーネント同士でデータのやり取りをさせます。

 

どこにいてもVuexライブラリにアクセスしてしまえば、データを取得できますから。便利ですね!
このことから、少数のコンポーネントで構築したアプリは、恩恵をあまり受けることができないかもしれません。

 

 

(2) 状態管理パターン

 

Vuexは、状態管理を単方向データフローで行っており、公式の図解は以下のようになっています。

 

Actions

 API等と非同期通信を行い、データを取得します。
 取得後、データをコミットし、Mutationsを呼び出します。

Mutations

 Actionsまたは、コンポーネントよりデータがコミットされたら、呼び出されます。
 唯一State内のデータを変更することが出来ます。

State

 各コンポーネントで使用するデータはここに集約します。

 

 

(3) Vuexを使用する

 

Vue-CLI 3.*系を前提とします。インストール手順は以前のブログで紹介しています。

1
vue create <ProjectName>

で行った後、Manually select features を選択すると、使用できるものが一覧化されてますが、この中にVuexがありますので、選択しましょう。

 

後から追加する場合は、

1
vue add vuex

を実行します。

 

 

準備が整いました。

<ProjectName>ディレクトリ直下にsrcディレクトリがあると思いますが、その中にstore.jsがあります。
ない方は、store.jsを作成するなり、storeディレクトリを作成後、index.jsを作成するなりしてください。

 

store.jsの中身は以下のようになっています。

 

1
2
3
4
5
6
7
8
9
10
11
//store.js
import Vue from 'vue'
import Vuex from 'vuex'

Vue.use(Vuex)

export default new Vuex.Store({
  state: {},
  mutations: {},
  actions: {}
})

 

5行目の

1
Vue.use(Vuex)

は、書き忘れないようにしてください。このアプリケーション内でVuexを使用しますと言う宣言になります。Vuexが使用できない場合は、描き忘れが無いか確認してください。

 

 

ここから簡単な使用方法をカウンターアプリを作成しつつ説明します。

 

Stateについて

Storeのデータ群をここ(State)で保持

Mutationsでのみ変更される

 

store.jsを以下のように変更します。

 

1
2
3
4
5
6
7
8
9
10
11
12
import Vue from 'vue'
import Vuex from 'vuex'

Vue.use(Vuex)

export default new Vuex.Store({
  state: {
    counter: 0 // ★これを追加
  },
  mutations: {},
  actions: {}
})

 

componentsディレクトリにcounter.vueを作成し、以下のようにします。

 

1
2
3
4
5
6
7
8
9
10
11
//counter.vue
<template>
  <div class="counter">
    {{ $store.state.counter }}
  </div>
</template>
<script>
  export default {
    name: "counter"
  };
</script>

 

$store.state.counter でStore内のStateに登録した変数[counter]にアクセスしています。

 

次にviewsディレクトリにHome.vueを作成して、以下のようにします。

 

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
//Home.vue
<template>
  <div class="home">
    <counter></counter> <!-- ★これを追加 -->
  </div>
</template>
<script>
  import counter from "@/components/counter.vue"; // ★これを追加

  export default {
    name: "home",
    components: { // ★components{...}を追加
      counter
    }
  };
</script>

 

これでcounter.vueが表示されたと思います。この状態だと、「0」が表示されています。
試しに、store.jsのcounterの数値を変更してみてください。
初期値が変更した値になっているはずです。

 

 

もう一つコンポーネントを作成してみましょう。reset.vueとします。

 

1
2
3
4
5
6
7
8
9
10
11
12
13
//reset.vue
<template>
  <div class="reset">
    <p>値のリセット</p>
    {{ $store.state.counter }}
    <br>
  </div>
</template>
<script>
  export default {
    name: "reset"
  };
</script>

 

ついでにcounter.vueからreset.vueが表示できるように、counter.vueを修正します。

 

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
//counter.vue
<template>
  <div class="home">
    {{ $store.state.counter }}
    <br>
    <hr>
    <reset></reset> <!-- ★これを追加 -->
  </div>
</template>
<script>
  import reset from "@/components/reset.vue"; // ★これを追加

  export default {
   name: "counter",
    components: { // ★components{...}追加
      reset
    }
  }
</script>

 

これでどちらのページからもcounter.vueが閲覧できるようになりました。実際にlocalhostへアクセスしてみてください。
propsや$emitを使わずに値をそれぞれのコンポーネントで表示できたかと思います!

 

 

Mutationsについて

Storeを変更する

Storeのデータをコミットすることで呼び出される
同期処理を行う

 

実際にStoreをコミットしてMutationsを呼び出してみましょう。
store.jsを以下のように変更します。

 

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
//store.js
import Vue from 'vue'
import Vuex from 'vuex'

Vue.use(Vuex)

export default new Vuex.Store({
  state: {
    counter: 0
  },
  mutations: {
     increment (state) { // ★increment{...}追加
       // インクリメント用関数
       state.counter++
     },
     reset (state) {     // ★reset{...}追加
      // カウンターリセット用関数
      state.counter = 0;
    }
  },
  actions: {}
})

これでstoreのcounterの状態を管理する関数が作成できました。関数名の通り、[increment]ではカウントアップを、[reset]ではカウンタを0に戻します。
また、各関数(ハンドラ)は第一引数にVuexの状態を取得します。なので、[increment]も[reset]も第一引数に[state]を定義しています。
引数名は当たり前ですが、何でも構いません。ここではあえてstoreと命名しているだけです。

 

ちなみに第二引数もあります。通称ペイロードと呼ばれていますが、任意の値を渡したいときに使用します。基本的にオブジェクトで渡します。
※使い方はMutationsの最後の方で。

 

1
2
3
4
5
6
7
8
9
10
11
12
13
14
//store.js
// ...省略...
  mutations: {
     increment (state, payload) {
       // インクリメント用関数
       state.counter += payload.incrementval
     },
     reset (state, payload) {
      // カウンターリセット用関数
      state.counter = payload.resetval;
    }
  },
  actions: {}
})

 

次に、mutationsに登録した関数をそれぞれのコンポーネントから呼び出せるようにします。

 

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
//counter.vue
<template>
 
<div class="home">
    {{ $store.state.counter }}
    <br>
    <button type="button" name="button" @click="increment">カウントアップ</button><!-- ★これを追加 -->
    <br>
    <hr>
    <reset></reset>
  </div>
</template>
<script>
  import reset from "@/components/reset.vue";

  export default {
    name: "counter",
    components: {
      reset
    },
    methods: { // ★methods:{...}追加
      increment() {
        this.$store.commit('increment');
      }
    }
  }
</script>

 

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
//reset.vue
<template>
  <div class="reset">
    <p>値のリセット</p>
    {{ $store.state.counter }}
    <br>
    <button type="button" name="button" @click="reset">値リセット</button> <!-- ★これを追加 -->
  </div>
</template>
<script>
  export default {
    name: "reset",
    methods: { // ★methods:{...}追加
      reset() {
        this.$store.commit('reset');
      }
    }
  };
</script>

 

ここまで出来ましたら、再度localhostにアクセスしてみましょう。各コンポーネントにボタンがそれぞれ追加されてます。
試しにcounter.vue側のボタン[カウントアップ]をクリックします。すると、counter.vueと、reset.vueのcounter変数値がインクリメントされてます。
ついでにreset.vue側のボタン[リセット]ボタンもクリックしてみます。どちらのコンポーネントのcounter変数値が初期値に戻ってると思います。

 

 

無事、propsや$emitに頼らず、コンポーネント間での値参照に成功しました。
大事なのは、storeで定義したデータを更新させるのはstore内でしか行えず、storeの関数をコールするには、commitします。その際にコールするmutationsの関数名を定義してあげます。そうすることで、mutationsによる値更新を行うことができるようになります。

 

下記のコードのように、直接store値を変更したり、mutations関数をコールすると、エラーになるので、注意してください。

 

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
//counter.vue
//...省略...
<script>
  import reset from "@/components/reset.vue";

  export default {
    name: "counter",
    components: {
      reset
    },
    methods: {
      comp_increment() { //見分けるために関数名変更
        increment();
        //または
        this.$store.counter++;
      }
    }
  }
</script>

 

また、mutationsの関数に任意の値を渡したい場合、commitの第二引数を以下のように設定します。

 

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
//counter.vue
//...省略...
<script>
  import reset from "@/components/reset.vue";

  export default {
    name: "counter",
    components: {
      reset
    },
    methods: {
      increment() {
        this.$store.commit('increment', { incrementval: 2 }); // ★ここを変更
    }
  }
</script>

 

こうすることによって、任意の数値分インクリメント(ここでは2ずつ)されるようになりました。
さらに任意の値を渡したい場合は、オブジェクトの中にどんどん追記してあげれば、コールした関数先でも参照することが出来ます。

 

 

Actions

commitして、mutationsをコールする
非同期処理を行う
Promiseをリターンする
stateは変更しない
dispatchで呼び出される

 

mutationsと似ていますが、Actionsはdispatchでコールされるのと、非同期処理を行い、それをPromiseでリターンするという役割を担っています。
では、実際にコードを書いてみましょう。…これ非同期にする必要なくね?とかは受け付けません!

 

まずは、actionsをdispatchするコンポーネントを作成します。countup.vueとしましょう。

 

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
//countup.vue
<template>
  <div id="timer">
    <button type="button" name="button" @click="countup">重たい処理</button>
  </div>
</template>

<script>
  export default {
    name: 'timer',
    methods: {
      countup() {
        //actionsをコール(Promiseがリターンされるので、ここではこの形式)
        this.$store.dispatch('act_countup', { timer: 2000 })
        .then(() => {
          console.log('countup complete');
        })
        .catch(() => {
          console.log('countup failed...');
        });
      }
    }
  }
</script>

 

ついでにcountup.vueをcounter.vueに組み込みましょう。それから、store.jsのacrionsの箇所も変更しちゃいます。

 

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
//counter.vue
<template>
  <div class="home">
    {{ $store.state.counter }}
    <br>
    <button type="button" name="button" @click="increment">カウントアップ</button>
    <br>
    <hr>
    <reset></reset>
    <br>
    <hr>
    <countup></countup> <!-- ★ここ追加 -->
  </div>
</template>

<script>
  import reset from "@/components/reset.vue";
  import countup from "@/components/countup.vue"; // ★ここ追加

  export default {
    name: "counter",
    components: {
      reset,
      countup // ★ここ追加(1行上のカンマも忘れずに)
    },
    methods: {
      increment() {
        this.$store.commit('increment', {
          val: 2
        });
      }
    }
  }
</script>

 

1
2
3
4
5
6
7
8
9
10
11
12
13
//store.js
//...省略...
  actions: {
    act_countup({ state }, payload) {
      return new Promise((resolve, reject) => {
        setTimeout(() => {
          state.counter++;
          resolve();
        }, payload.timer);
      })
    }
  }
})

 

ここまで出来ましたら、localhostを再起動します。[重たい処理]ボタンが追加されてます。
クリックすると、dispatchによりactionsに定義した関数[act_countup]がコールされ、2秒後、ログに「countup complete」が出力されてると思います。
非同期処理なので、その間もカウントアップ、値リセットは動きます(使い方が違いますが…)

 

actionsもmutationsと同様に、第一引数及び、第二引数しかありません。第二引数はmutationsと同じになりますが、第一引数が異なります。
contextと呼ばれますが、オブジェクトになっており、以下6種類あります。

 

state `store.state` と同じか、モジュール内にあればローカルステート
getters `store.getters` と同じか、モジュール内にあればローカルゲッター
rootState `store.state` と同じ。ただしモジュール内に限る
rootGetters `store.getters` と同じ。ただしモジュール内に限る
commit `store.commit` と同じ。mutationsをコールする時に使用。
dispatch `store.dispatch` と同じ。actionsをコールする時に使用。

 

ローカル〇〇とか出てきますが、同じjsファイルで定義している場合、そこを参照することになります。今回も、これに該当します。同じjsファイルにstateもmutationsもactionsもありますので。
actions側ではこんな感じになります。

1
act_countup({state, getters, rootState, rootGetters, commit, dispatch}, payload) { ... }

 

 

さて、このように非同期処理をactionsでは書けますが、別にmutationsで書くこともできると思います。ただ、actionsで書いた方が融通が利きます。
なぜなら、コードでは書いてませんが、別のactionsへdispatchが出来たり、mutationsのcommitもできるので、処理を柔軟に組み合わせることが出来ます。
ちなみに、async/awaitシンタックスを書くことも出来るので同期的な処理も行えます。

 

 

 

まとめ

 

Vuexは、Vue.jsのライブラリの一つで状態管理をしてくれる。つまり、データをStoreで一元管理してくれる。
状態管理には3種類あり、state / mutations / actions である。

 

stateは、Store内のデータを保持してくれる。ここでは、データの変更は出来ない。

 

mutationsは、stateで保持しているデータの変更を行ってくれる。変更するときはcommitでコールする。

 

actionsは、stateで保持しているデータの変更を行ってくれる。変更するときはdispatchでコールする。
mutationsと似ているが、最大の違いは、非同期処理を行い、Promiseをリターンすることである。

 

詳しくは公式ドキュメント(日本語版)がありますので、そちらを参照してください。

 

 

おわりに

 

今回は、Vuexのこと、StoreのState、Mutations、Actionsについて簡単ですが、紹介しました。
少しでもVuexについて理解が深まれば、幸いです。

 

次回は、Vuexのgetterや、モジュールについて紹介していきます。
それでは、またの機会に!


Top