入門者必読、vue.jsの状態管理Vuexがわかる
本文書ではできるだけシンプルな例を通してVuexの説明を行っていきます。Vuexを使った経験がないという入門者の人また下記の図がわからないという人を対象にしています。Vuexが難しいなと感じている人がいるとすれば用語がこれまでのvue.jsよりも増えていることまた記述方法が長かったり短縮形によって複数の書き方があることが原因だと思います。一度理解してしまえば決して難しいものではないので安心して読み進めてください。
Vue3では新たに状態管理のライブラリとしてpiniaが登場しました。Vue3で新しいプロジェクトを作成する場合にはpiniaを利用することが推奨されています。
Vuexとは
Vuexはすべてのコンポーネントでデータを一元管理するための仕組みです。
Vuexがない環境ではコンポーネント間のデータの受け渡しには、propsや$emitによるイベントを利用して行います。しかし、コンポーネント間でのデータ受け渡しが頻繁に行われたり階層が増えてくるとporpsや$emitでのデータ管理が難しくなります。
複雑に構成されたコンポーネントでのデータ管理の難しさを解決するための仕組みが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が正常に動作しています。
つぎにVuexを使用してHello Vuexを表示させます。vue-cliでインストールする際にVuexを選択したので追加設定を行うことなくVuexを使用することができます。storeディレクトリの下にあるindex.jsファイルを開き、stateにmessageを追加します。
index.jsファイルの中にはstate以外にもmutations、actionsがありますがこれはのちほど説明を行います。
Vuexのstateはdataプロパティと同じものだと考えてください。dataプロパティとは異なる点があるとすれば、Vuexのstateにプロパティを追加することですべてのコンポーネントからアクセスが可能となる点です。
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を利用した方法でmessageの内容をブラウザ上に表示することができました。ここまでの動作確認ではアクセスするための文字列は長くなりましたが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つ表示されます。stateにあるデータがどのコンポーネントからも同じ方法でアクセスすることができることが理解できました。
computedプロパティを利用してアクセス
{{ $store.state.messag }}を使ってVuexの中のstateを表示させることができましたが、templateタグの中で{{{ $store.state.messag }}を{ 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が表示されたままの状態になります。
今後は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>
v-bindやpropsの記述がなくなったことからも先程のmessageの例よりもVuexを使う場合と使わない場合の違いが明確になりVuexのデータ管理がこれまでとは異なることが理解できたかと思います。
Gettersの使い方の確認
computedプロパティと同様の働きをするGettersの使用方法を確認します。
まず、computedプロパティを使ってusersのオブジェクトの中からageが30以下のユーザ情報のみ表示させます。
computed:{
users: function(){
return this.$store.state.users.filter(user => user.age < 30);
}
}
ageが30以下の2名のユーザのみ表示されます。
次に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を利用する前と同じユーザリストが表示されます。
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の更新方法を確認します。データのアクセス方法が理解できたと思うので更新が理解できればVuexの理解度は100%になります。あと少しです。
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(ミューテーション)を使う必要があります。store¥index.jsにcountの数を増やすmutationsを追加します。mutationsの引数にはstateが渡されます。
export default new Vuex.Store({
state: {
count: 0
},
mutations: {
increment : function(state) {
state.count++
}
},
actions: {
}
})
mutationsの追加が完了したらmutationsの実行方法を知っておく必要があります。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ずつ増えます。
mutationsはcommitで更新を行うだけではなくstateの状態の変化を追うことができます。それは次で説明を行います。
stateの状態の変化を確認
stateの状態の変化を確認するためにChromeのdevtoolを開いてVuexのアイコンをクリックします。
devtoolを開いたままUPボタンを押すとCountが増えるのと同時に実行したmutationsが増えていくのを確認することができます。このようにmutationsを介してstateの状態(値)の変化を追うことができます。
stateの状態の変化が見れるだけではなく選択したmutationsまで戻ったり、選択したmutationsまでの処理を削除することもできます。
- Commit This Mutation
- Revert This Mutation
- Time Travel to This State
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では通常のvue.jsのmethodsに対応するactions(アクション)が準備されています。actionsもmutationsのようにstateを更新するために使用されるのですが、大きく2つの違いがあります。
- actionsはstateを直接変更するのではなくmutationsを経由して(commit)stateを更新する
- 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の値を取得することもできます。
actionsの追加が完了したので、App.vueファイルのincrementメソッドを更新します。incrementメソッドからactionsを実行します。mutationsを実行するのはcommitでしたがactionsを実行するためにはdispatchメソッドを使用します。
methods: {
increment : function(){
this.$store.dispatch('incrementOne')
}
},
actionsの追加後も先ほどと同様にブラウザ上のボタンをクリックするとcountの数が1ずつ増えます。
actionsにはビジネスロジックを記述し、mutationsではstateを更新する処理を記述します。簡単にいうとactionsには複雑な処理を記述しmutationsはある一つのstateに注目しそのstateの変更を行う処理だけの役割を持つものと考えることができます。
コンポーネントのmethodsからcommitを使ってmutationsを実行するのではなくシンプルな処理でもactionsを通してmutationsを実行することが推奨されているようです。またactionsの中では複数のmutationsを実行することも別のactionsを呼ぶことも可能です。
またcontextの中でcommitしか使わないのであればES6のDestructuringを使って下記のように短縮して記述することも可能です。
actions: {
incrementOne: function({commit}){
commit('increment')
}
}
(2)についてですが、actionsには非同期の処理を入れることができるということはmutationsには非同期処理を入れることができないということを意味します。
Actionsの非同期処理とは
非同期処理というのはaxiosを使ってサーバからデータを取得するような場面で頻繁に使われる処理です。非同期処理のイメージをもってもらうためにsetTimeout関数を使うと下記のような処理になります。3秒後にmutationsのincrementが実行されcount数が1増えます。
mutations: {
increment : function(state) {
state.count++
}
},
actions: {
incrementOne: function(context){
setTimeout(() => {
context.commit('increment')
}, 3000)
}
},
axiosを使って非同期でデータを取得
axiosを使って非同期でデータを取得する流れの中でどのようにActionsとMutationsが利用されるのか確認しておきます。これを見るとActionsの中で非同期処理を入れることとMutationsの中に入れないことが理解できると思います。
axiosがインストールされていない場合はnpmコマンドでインストールを行います。
$ npm install axios
実際にデータを取得したほうが実践的だと思いますので、無料サービスJSONPlaceholderを利用します。https://jsonplaceholder.typicode.com/usersにアクセスするとユーザ一覧を取得することができます。
インストールしたaxiosを利用するためにstore¥index.jsファイルの上部でaxiosのimportを行います。
import Vue from 'vue'
import Vuex from 'vuex'
import axios from 'axios'
store¥index.jsファイル内のactionsでaxiosを使って非同期でユーザ一覧を取得します。下記の中で非同期処理で取得したデータをcommitでmutationsのsetUsersを実行しています。
actions: {
getUsers: function({commit}){
return axios.get('https://jsonplaceholder.typicode.com/users')
.then(response => {
commit('setUsers',response.data)
})
}
},
mutationsでsetUsersを追加します。setUsersでは非同期処理は行わずただpayloadで受け取ったユーザデータを同期処理でstateのusersに入力しているだけです。
state: {
users:[],
},
mutations: {
setUsers : function(state,users) {
state.users = users
}
},
store¥index.jsに記述した全体は下記のようになります。
import Vue from 'vue'
import Vuex from 'vuex'
import axios from 'axios'
Vue.use(Vuex)
export default new Vuex.Store({
state: {
users:[],
},
mutations: {
setUsers : function(state,users) {
state.users = users
}
},
actions: {
getUsers: function({commit}){
return axios.get('https://jsonplaceholder.typicode.com/users')
.then(response => {
commit('setUsers',response.data)
})
}
},
})
次にApp.vueファイルからdispatchでactionsのgetUsersを実行します。
<template>
<div id="app">
<h1>ユーザ一覧</h1>
<div v-for="user in users" :key=user.id>
{{ user.name }}
</div>
</div>
</template>
<script>
export default {
name: 'app',
computed : {
users : function(){
return this.$store.state.users
}
},
mounted(){
this.$store.dispatch('getUsers')
}
}
</script>
ブラウザで確認するとユーザ一覧が表示されます。
この動作確認から非同期処理が含まれたActionsとstateの更新処理を同期処理で行うmutationsについて理解することができました。
本文書で動作確認に利用したコード
本文書で最終的に作成したコードは下記の通りです。
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の例を元に下記の図の流れを追ってみましょう。
- ユーザがボタンをクリックするとclickイベントによりローカルメソッドが実行され、そのメソッドの中でdispatchメソッッドが実行されます。
- dispatchによりActionsが実行され、Actionsの中のcommitメソッドによりmutationsが実行されます。
- mutationsはstateであるcountの値を変更します。
- アップされた値は、ブラウザに再描写され、ユーザはアップしたcountの値を確認することができます。
ここまでVuexを理解することができましたが、Vuexを使いこなす上で必須ではありませんが本書で触れていないmapState, mapMutations, mapGetters, mapActionsの理解も必要です。下記の文書を参考にしてください。
その他の機能
モジュール化
ここまでの説明ではVuexに保存するstate, mutations, actions, gettersはstore¥index.jsファイルの中に保存していました。アプリケーションが小さい場合はindex.jsファイルだけでVuexの状態を管理することができますがアプリケーションが増えるとコード量も多くなり管理が難しくなってきます。そのためVuexではモジュール化することができます。モジュール化することで機能に応じてファイルを分けることができます。例えばアプリケーションにユーザ認証の機能がある場合はuser.jsファイルという独立したファイルを作成します。機能毎にファイルをわけることでコード管理が楽になります。
storeフォルダの下にmodulesフォルダを作成してuser.jsファイルを作成します。
export const user = {
namespaced: true,
state: {
user: null,
},
mutations: {
setUser(state, payload) {
state.user = payload;
},
},
actions: {
login({ commit }) {
//ログイン処理
},
logout({ commit }) {
//ログアウト処理
},
},
getters:{
},
};
作成したuserモジュールはstore¥index.jsファイルで下記のように設定することができます。
import Vue from 'vue'
import Vuex from 'vuex'
import { user } from './modules/user';
Vue.use(Vuex)
export default new Vuex.Store({
modules: {
user,
}
})
アクセスする際は$store.state.user.userとなります。またdispatchによってlogin処理を実行したい場合はdispatch(‘user/login’)となります。namespaced:trueを設定しているためloginではなくuser/loginとなり名前空間を使ってactionsを設定することができます。namespacedを設定していない場合はdispatch(‘login’)となります。