本文書では、できるだけシンプルな例を通してVuexの説明を行っていきます。Vuexをまだ使ってことがないという入門者の人また下記の図がわからないという人を対象にしています。

vuex diagram
vuex diagram

Vuexとは

Vuexはすべてのコンポーネントでデータを一元管理するための仕組みです。

Vuexはstate management(状態管理)という言葉で説明されているため最初は状態という言葉にこだわらずデータ共有管理という名前で考えたほうがわかりやすいかもしれません。

Vuexがない環境ではコンポーネント間のデータの受け渡しには、propsや$emitによるイベントを利用して行います。しかし、コンポーネント間でのデータ受け渡しが頻繁に行われたり階層が増えてくるとporpsや$emitでのデータ管理が難しくなります。

その問題を解決するための仕組みがVuexです。Vuexという一つの入れ物にデータを入れることでどこからでもVuex内のデータへのアクセスが可能になります。

Vuexでのデータ管理のイメージ
Vuexでのデータ管理のイメージ

vue-cliによるプロジェクトの作成

vuexを使用する環境を構築するためにvue-cliを利用します。


$ vue create vue-learning

vue-cliのインストールの詳細は説明しませんが、Manually select featuresでBableとVuexだけ選択してインストールを行います。

vue-cliを使用しない場合は、vueとは別にvuexをインストールする必要があります。npmを使っている場合は、package.jsonの中を見てvuexがインストール済みかどうか確認してください。

Vuexの基礎

Vuexを使ってHello Vuex

Vuexを使った場合と使わない場合を比較しながらVuexの説明を行なっていきます。

まず最初にVuexを使用しない従来の方法でHello Vuexを表示させるためにsrcディレクトリのApp.vueファイルに下記を記述します。


<template>
  <div id="app">
    <h1>{{ message }}</h1>
  </div>
</template>

<script>
export default {
  name: 'app',
  data: function(){
    return {
      message : 'Hello Vuex'
    }
  }
}
</script>

vue-learningディレクトリでnpm run serveコマンドを実行後、ブラウザでlocalhost:8080にアクセスします。ブラウザにHello Vuexが表示されればvue.jsが正常に動作しています。

これまでの方法でHello Vue
これまでの方法でHello Vue

つぎにVuexを使用してHello Vuexを表示させます。vue-cliでインストールする際にVuexを選択したので追加設定を行うことなくVuexを使用することができます。storeディレクトリの下にあるindex.jsファイルを開き、stateにmessageを追加します。

index.jsファイルの中にはstate以外にもmutations、actionsがありますがこれはのちほど説明を行います。

Vuexのstateはコンポーネントのdataと同じものだと考えてください。dataとは異なる点があるとすれば、Vuexのstateに追加するとすべてのコンポーネントからアクセスが可能となる点です。

dataはコンポーネントファイルの中で定義され、その中でのみ使えるデータを扱う時の名前、stateはVuexでデータを扱う時の名前と覚えてください。。

import Vue from 'vue'
import Vuex from 'vuex'

Vue.use(Vuex)

export default new Vuex.Store({
  state: {
  	message: 'Hello Vuex'
  },
  mutations: {
  },
  actions: {
  }
  modules: {
  }
})

すべてのコンポーネントからアクセスできるとはいえApp.vueのtemplateタグの中でこれまでのように{{ message }}とすればアクセスできるものではなりません。App.vueからのアクセスするためには$store.state.messageと記述する必要があります。

$store.state.messageは少し長いですが、$storeという大きな入れ物の中にさらにstateという入れ物があり、その中にmessageが入っているイメージを持つことができます。


<template>
  <div id="app">
    <h1>{{ $store.state.message }}</h1>
  </div>
</template>

ブラウザでアクセスし下記が表示されれば、Vuexの管理下にあるstateのmessageを使ってHello Vuexを表示することができています。

Vuexを使ってHello Vuex
Vuexを使ってHello Vuex

これまでの方法とVuexを利用した方法でmessageを表示することができました。ここまでの処理ではVuexを使うことでデータの処理が難しくなったという印象はないと思います。

Vuexとこれまでの方法
Vuexとこれまでの方法

Vuexではデータを集中管理しているので、どのコンポーネントから同じようにアクセスすることができます。

App.vue以外の他のコンポーネントからも同じようにアクセスできるか確認するためにsrc¥componentsディレクトリの中にHelloVuex.vueファイルを作成します。App.vueの子コンポーネントとして利用するためApp.vueファイルでimportを行います。


<template>
  <div id="app">
    <h1>{{ $store.state.message }}</h1>
    <HelloVuex></HelloVuex>
  </div>
</template>

<script>
import HelloVuex from '@/components/HelloVuex.vue'

export default {
  name: 'app',
  data: function(){
  	return {
  		message : 'Hello World'
  	}
  },
   components: {
    HelloVuex
  }
}
</script>

HelloVuex.vueファイルに下記を記述します。Vuexのmessageにアクセスする方法は子コンポーネントでも親コンポーネントでも同じ$store.state.messageです。アクセスの仕方を見てもVuexは独立したデータ保管場所であることがわかるかと思います。


<template>
  <h2>{{ $store.state.message }}</h2>
</template>
<script>
export default {
  name: 'HelloVuex'
}
</script>

子コンポーネントからも$store.state.messageにアクセスできるため下記のようにHello Vuexが2つ表示されます。

子コンポーネントでHello Vuex
子コンポーネントでHello Vuex

computedプロパティを利用してアクセス

{{ $store.state.messag }}を使ってVuexの中のstateを表示させることができましたが、templateタグの中で{{ message }}を使って表示させるためにcomputedプロパティを利用することができます。


<template>
  <div id="app">
    <h1>{{ message }}</h1>
  </div>
</template>

<script>

export default {
  name: 'app',
  computed : {
  	message : function(){
  		return this.$store.state.message
  	}
  }
}
</script>

変更後もブラウザ上にはHello Vuexが表示されたままの状態になります。

Hello Vue
Hello Vue

今後はcomputedプロパティを利用して、Vuexのstateへのアクセスを行います。stateのアクセス方法にはGettersというものもありますが、それは後ほど説明します。

オブジェクトデータの表示(従来の方法)

Vuexのデータへのアクセス方法の理解をさらに深めるためにVuex管理下のオブジェクトデータへのアクセス方法を確認します。まずは従来の方法で説明を行い、その後Vuexを使った場合の方法を説明します。

ユーザ情報が入ったusersオブジェクトを利用し、ユーザ情報をリスト化するといったシンプルなものです。

従来の方法では、dataにusersオブジェクトを設定後にv-bindを使って子コンポーネントへdataを渡します。


<template>
  <div id="app">
    <UserList v-bind:users="users"></UserList>
  </div>
</template>

<script>

import UserList from './components/UserList.vue'

export default {
  name: 'app',
  data : function(){
  	return {
  		users:[
	  		{name: 'John', email:'john@example.com', age:22},
	  		{name: 'Merry', email: 'merry@facebook.com',age:33},
	  		{name: 'Ken', email: 'ken@amazon.com',age:29}
		  ]
  		}
  	},
  components:{
  	 UserList
	}
  }

</script>

子コンポーネントUserListを作成し、propsを使って渡されたdataを受け取りv-forディレクティブで展開します。UserListコンポーネントはsrc¥componentsの下に作成します。


<template>
  <ul>
  	<li v-for="user in users"  v-bind:key="user.name">{{ user.name }} ({{ user.email }})</li>
  </ul>
</template>

<script>
export default {
  props : {
  	users: Object,
  }
}
</script>

ブラウザには下記のようにユーザ一覧が表示されます。

これまでの方法でユーザ一覧
これまでの方法でユーザ一覧

オブジェクトデータの表示(Vuexを利用)

今度はVuexを利用してみましょう。usersオブジェクトをstore¥index.jsのstateに移動します。


state: {
	  users:[
	    {name: 'John', email:'john@example.com', age:22},
	    {name: 'Merry', email: 'merry@facebook.com',age:33},
	    {name: 'Ken', email: 'ken@amazon.com',age:29}
	  ]
}

usersオブジェクトをVuexに移動させたので、App.vueファイルではusersオブジェクトが必要でなくなったので削除を行います。またusersオブジェクトがないためv-bindも必要ありません。


<template>
  <div id="app">
    <UserList></UserList>
  </div>
</template>

<script>

import UserList from './components/UserList.vue'

export default {
  name: 'app',
  components:{
  	 UserList
	}
  }
</script>

子コンポーネント側ではpropsではなくcomputedプロパティを使ってVuexのstateのusersにアクセスを行い、その値を元にユーザ一覧を表示することができます。


<template>
  <ul>
  	<li v-for="user in users">{{ user.name }} ({{ user.email }})</li>
  </ul>
</template>

<script>
export default {
  computed :{
  	users : function(){
  		return this.$store.state.users
  	}
  }
}
</script>
compotedプロパティを利用せず、v-forディレクティブ内のusersを$store.state.usersとしても表示される結果は同じになります。

v-bindやpropsの記述がなくなったことからも先程のmessageの例よりもVuexを使う場合と使わない場合の違いが明確になりVuexのデータ管理がこれまでとは異なることが理解できたかと思います。

Gettersの使い方の確認

computedプロパティと同様の働きをするGettersの使用方法を確認します。

まず、computedプロパティを使ってageが30以下のユーザ情報のみ表示させます。


computed:{
	users: function(){
		return this.$store.state.users.filter(user => user.age < 30);
	}
}

ageが30以下の2名のユーザのみ表示されます。

computedプロパティでfilterを使う
computedプロパティでfilterを使う

次にGettersを利用して同じことを実行します。index.jsにgettersを追加し、ユーザリストの中からageが30以下のユーザ情報のみ表示させるコードを記述します。


export default new Vuex.Store({
  state: {
	  users:[
	    {name: 'John', email:'john@example.com', age:22},
	    {name: 'Merry', email: 'merry@facebook.com',age:33},
	    {name: 'Ken', email: 'ken@amazon.com',age:29}
	  ]
  },
  getters: {
  	users : function(state){
  		return state.users.filter(user => user.age < 30);
  	}
  },
})

UserListコンポーネントの中で先ほど実行していた処理を削除し、gettersを使った処理に変更を行います。Gettersにはthis.$store.gettersでアクセスすることができます。


computed:{
	users: function(){
		return this.$store.getters.users;
	}
}

ブラウザで確認するとGettersを利用する前と同じユーザリストが表示されます。

computedプロパティでfilterを使う
gettersを利用してユーザリストを取得

gettersをcomputedプロパティではなく下記のように直接記述しても結果は同じです。


<li v-for="user in $store.getters.users"  v-bind:key="user.name">{{ user.name }} ({{ user.email }})</li>

computedプロパティだと記述したそのコンポーネント内でしか利用できません。他のコンポーネントで同じ処理を行いたい場合は同じコードをコンポーネント毎に記述する必要があります。しかしGettersを利用するとVuexのstoreの中に保存されているので、他のコンポーネントからも同じ方法で利用することができます。

Vuex内にあるデータの更新方法

Vuex管理下のstate(データ)へのアクセス方法とGettersの使い方がわかったので、次はVuexで管理されているstateの更新方法を確認します。

store¥index.jsの中のstateにcountを追加し、その値を0とします。


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

通常はコンポーネント内のメソッドを使ってデータ更新を行います。Vuexのstateの値もコンポーネントのmethodsに更新用のメソッドを追加し、下記のようにVuexのstateを直接実行するのではと思うかもしれません。Vuexのstateに対して直接更新することは可能ですが直接更新は行ってはいけません。


//コンポーネントのmethods内で下記のように記述してもいい??
this.$store.state.count++

stateの更新を行うためには、mutations(ミューテーション)を使う必要があります。index.jsにcountの数を増やすmutationsを追加します。mutationsの引数にはstateが渡されます。

mutationsを含め聞き慣れない新しい名称が出てくるため最初は圧倒されますが、使ううちに慣れるものなので安心してください。またmutateには変更という意味があり、その名詞形のmutationsが使われています。

export default new Vuex.Store({
  state: {
  	count: 0
  },
  mutations: {
    increment : function(state) {
      state.count++
    }
  },
  actions: {
  }
})

mutationsの追加が完了したらmutationsの実行方法を知っておく必要があります。mutationsはcommitを使って実行します。

公式のドキュメントにもVuex storeのstateを更新することができる唯一の方法がmutationsをcommitすることだと記載されています。

//下記のように実行できそうだがダメ
this.$store.mutations.increment

// commitを使ってmutationを実行
this.$store.commit('increment')

stateはmutationsを利用して更新することがわかったので、ブラウザ上にボタンを表示させクリックイベントを設定します。そのボタンをクリックするとmutationsのincrementメソッドが実行されcountの数字が1増えるというコードを追加します。コードは下記のようになりApp.vueファイルに記述します。


<template>
  <div id="app">
  	<p><button v-on:click="increment">UP</button>
    <h1>Count:{{ count }}</h1>
  </div>
</template>

<script>

export default {
  name: 'app',
  methods: {
  	increment : function(){
  		this.$store.commit('increment')
  	}
  },
  computed : {
  	count : function(){
  		return this.$store.state.count
  	}
  }
}
</script>

ブラウザ上に表示されているUPボタンを押すとcount数が1ずつ増えます。

mutationをcommitしてcount更新
mutationをcommitしてcount更新

mutationsはcommitで更新を行うだけではなくstateの状態の変化を追うことができます。

stateの状態の変化を確認

stateの状態の変化を確認するためにChromeのdevtoolを開いてVuexのアイコンをクリックします。

devtoolでvuexを確認
devtoolでvuexを確認

devtoolを開いたままUPボタンを押すとCountが増えるのと同時に実行したmutationsが増えていくのを確認することができます。このようにmutationsでstateの状態の変化をおうことができます。

mutationsの実行履歴
mutationsの実行履歴

stateの状態の変化が見れるだけではなく選択したmutationsまで戻ったり、選択したmutationsまでの処理を削除することもできます。

  • Commit This Mutation
  • Revert This Mutation
  • Time Travel to This State
commit this mutations
commit this mutations
Revert this mutations
Revert this mutations
Time Travel to this mutations
Time Travel to this mutations

payloadでmutationsに値を渡す

commitには値を渡すことができ、payloadと呼ばれます。先程はclickで1つ値が増えましたが、一度に10増やしたい場合は10をpayloadに設定するとVuexのmutationsに設定した値を渡すことができます。


  methods: {
  	increment : function(){
  		this.$store.commit('increment',10)
  	}

mutation側でもpayloadを取得できるように引数を増やす変更が必要です。


  mutations: {
    increment : function(state, number) {
      state.count = state.count + number
    }
  }

これでボタンを押すと10アップするプログラムに変更できます。

Actionsを使ってstateを更新

Vuexでは、methodsに対応するactions(アクション)が準備されています。actionsもmutationsのようにstateを更新するために使用されるのですが、大きく2つの違いがあります(マニュアル記載)。

  1. actionsはstateを変更するのではなくmutationsをcommitする
  2. actionsには非同期の処理を入れることができる

1については先ほど使用したcountの例を使って説明を行います。

countを更新する場合はmutationsを使うことを説明しました。actionsを使う場合は、actionsの中でcommitを使ってmutationsを実行し、mutationsからcountを更新させます。つまりstateの値をactionsで更新する場合は必ずmutationsを経由して行うことになります。

下記がactionsを追加したコードです。actionsの中でcommitの引数にmutationsのメソッドが入っていることがわかります。actionsでは引数にcontextというオブジェクトが渡されます。


mutations: {
	increment : function(state) {		
		state.count++
    }
},
actions: {
	incrementOne: function(context){
		context.commit('increment')
	}
}

contexはstoreインスタンスが持つプロパティ、メソッドを保持するオブジェクトです。context.state.countでcountの値を取得することもできます。

contextはstate, commit以外にもdispatch, gettersメソッドも使えます。

actionsの追加が完了したので、App.vueファイルのincrementメソッドを更新します。incrementメソッドからactionsを実行します。mutationsを実行するのはcommitでしたがactionsを実行するためにはdispatchメソッドを使用します。


  methods: {
  	increment : function(){
  		this.$store.dispatch('incrementOne')
  	}
  },

actionsの追加後も先ほどと同様にブラウザ上のボタンをクリックするとcountの数が1ずつ増えます。

mutationをcommitしてcount更新
actionsを追加してcount更新

actionsにはビジネスロジックを記述し、mutationsではstateを更新する処理を記述します。コンポーネントのmethodsからcommitを使ってmutationsを実行するのではなくシンプルな処理でもactionsを通してmutationsを実行することが推奨されているようです。またactionsの中では複数のmutationsを実行することも別のactionsを呼ぶことも可能です。

またcontextの中でcommitしか使わないのであれば下記の記述も可能です。


actions: {
	incrementOne: function({commit}){
		commit('increment')
	}
}

(2)についてですが、actionsには非同期の処理を入れることができるということはmutationsには非同期処理を入れることができないということを意味します。

実際にはmutationsに非同期処理は入れても動作するので、mutationsでは非同期処理を入れないというのが正しい説明です。stateもmutationsを使わず直接更新ができたようにactionsでもstateを直接更新ができないわけではありません。Vuexは複数のコンポーネントから共有されていることもあり、同時に複数からの処理を正確に処理するためには守らなければいけないルールがあると認識しておく必要があります。

非同期処理というのはaxiosを使ってサーバからデータを取得するような場面で頻繁に使われる処理です。非同期処理のイメージをもってもらうためにsetTimeout関数を使うと下記のような処理になります。3秒後にmutationsのincrementが実行されcount数が1増えます。


actions: {
	incrementOne: function(context){
	    setTimeout(() => {
	      context.commit('increment')
	    }, 3000)
  
	}
},

本文書で最終的に作成したコードは下記の通りです。

App.vueファイル


<template>
  <div id="app">
    <p><button v-on:click="increment">UP</button>
    <h1>Count:{{ count }}</h1>
    </div>
</template>

<script>
export default {
  name: 'app',
  methods: {
    increment : function(){
      this.$store.dispatch('incrementOne');
    }
  },
  computed : {
    count : function(){
      return this.$store.state.count;
    }
  }
}
</script>

store¥index.jsファイル


import Vue from 'vue'
import Vuex from 'vuex'

Vue.use(Vuex)

export default new Vuex.Store({
  state: {
    count: 0,
  },
  mutations: {
    increment: function(state){
      state.count++;
    }
  },
  actions: {
    incrementOne: function(context){
      setTimeout(()=>{
        context.commit('increment');
      },3000);
    }
  },
  modules: {
  }
})

まとめ

ここまでの説明を通して下記の図にあるActions, Mutations, Stateの説明をすべて行いました。下記の図が理解できていればVuexの概念はほぼ理解できていると思います。

理解を確認するため、文書の中で度々出てきたcountの例を元に下記の図の流れを追ってみましょう。

  1. ユーザがボタンをクリックするとclickイベントによりローカルメソッドが実行され、そのメソッドの中でdispatchメソッッドが実行されます。
  2. dispatchによりActionsが実行され、Actionsの中のcommitメソッドによりmutationsが実行されます。
  3. mutationsはstateであるcountの値を変更します。
  4. アップされた値は、ブラウザに再描写され、ユーザはアップしたcountの値を確認することができます。
vuex diagram
vuex diagram

ここまでVuexを理解することができましたが、Vuexを使いこなす上で本書で触れていないmapState, mapMutations, mapGetters, mapActionsの理解も必要です。下記の文書を参考にしてください。