はじめに
アプリケーションの規模が大きくなっていくにつれ、状態管理が重要になっていきます。今回の記事では、Vue.jsにおける状態管理ライブラリであるVuexの機能と使い方を紹介します。
Vuexとは
VuexはVue.jsにおける状態管理のためのライブラリです。Flux、Elmアーキテクチャ、Reduxなどの影響を受けており、データフローが単方向になるように設計されています。
データフロー設計において頻出するデザインパターンとして、以下の3つがあがります。
- Single Source of Truth
- 状態の取得、更新のカプセル化
- 単方向データフロー
これらを意識してデータフローを設計することで、変更に強く柔軟なアプリケーションを作ることが容易になります。
Vuexを使うことで、これらのデザインパターンに沿った実装が容易になります。
Single Source of Truth
Single Source of Truth(信頼できる唯一の情報源)とは、データの管理を一箇所に集約することで、管理を容易にすることを目的としたデザインパターンです。これには、以下のような利点があります。
- どのコンポーネントからも同一のデータを参照するため、データの不整合が発生しにくい。
- 複数のデータを組み合わせた処理を比較的容易に実装できる。
- データの変更に関するデバッグがしやすくなる。
Vuexはアプリケーションの状態やそれを管理する処理が一箇所にまとまるように設計されているため、上記を満たすことができます。
状態の取得、更新のカプセル化
状態の取得、更新をカプセル化することにより、状態管理のコストを下げることができます。カプセル化を行うことには以下のような利点があります。
- 状態の取得、変更処理を様々な場所から利用できる。
- データの取得、更新処理をビューから切り離すことで、これらを変更した際の影響範囲を小さくできる。
- デバッグ時に確認する場所を限定できるため、デバッグがしやすくなる。
Vuexでは状態の更新をミューテーションと呼ばれる機能でのみ行い、状態の取得をゲッターと呼ばれる機能でのみ行うため、上記を満たすことができます。
単方向データフロー
データフローを単方向にすることで、状態の取得、更新処理が簡潔になります。これには以下のような利点があります。
- データの取得と更新が同時に行われなくなり、実装やデバッグがしやすくなる。
- データの取得、更新方法が絞られる事により、コードが理解しやすくなる。
先程あげたとおり、Vuexでは状態の更新はミューテーションで、取得はゲッターで行うため、実装が強制的に単方向データフローになります。
Vuexのストア
Vuexではストアの役割を抑えておくことが重要です。ストアは主にアプリケーションの状態を保持する役割をもっています。
Vuexでは前述のSingle Source of Truthを前提に実装されているため、アプリケーション内で常に1つのストアのみが存在するようにします。
ストアは、ステート、ゲッター、ミューテーション、アクションという4つの要素から成り立っています。それぞれについて順次説明します。
ステート
ステートはアプリケーション全体の状態を保持するオブジェクトです。すべてのステートは1つの木構造として表現されます。アプリケーションのすべての状態を1つの木としてステートを保持することで、Single Source of Truthを実現しています。
しかし、すべてのステートを1つのファイルで実装しなければならないというわけではありません。ステート数が多くなった場合はモジュールとして分割管理することで、状態管理が複雑になりすぎないようにします。
また、アプリケーションのすべてのデータをステートで管理すべきというわけではありません。あくまで、アプリケーション全体で使用するデータのみステートで管理すべきです。
ステートでの管理に適したデータとしては以下のようなものがあります。
- メニューの開閉状態を管理するフラグ
- API通信中かどうかのフラグ
- ログイン中のユーザのデータ
コンポーネントで管理したほうが良いデータとしては以下のようなものがあります。
- 入力中のフォームのデータ
- ドラッグ中の要素の座標
ゲッター
ゲッターはステートから別の値を算出するために使われます。例えば、ユーザの操作で商品のリストを絞り込みたい場合は、ゲッターで商品の絞り込みを行ってからリストを返却します。
ゲッターを利用することで、異なるコンポーネント間でロジックを共有しやすくなります。また、算出ロジックをストア内に置くことで、処理を探しやすく、テストがしやすくなるという利点があります。
ゲッターはcomputedと同様に値がキャッシュされるため、よく利用するステートの算出ロジックをゲッターにすることでパフォーマンスの向上が期待できます。
ミューテーション
ミューテーションはステートを更新するために使われます。Vuexでは原則的にミューテーション以外がストアを更新することを禁止しています。ステートの更新をミューテーションのみが行うことで、ステートの変更がいつ、どこで行われたか追跡しやすくなります。
アクション
アクションは非同期処理や外部APIとの通信を行い、最終的にミューテーションを呼び出すために使われます。
Vuexの使用例
それでは、Vuexの使い方をコード例を交えて説明します。
インストール
Vuexはnpmコマンドでインストールすることができます。
1 | $ npm install vuex |
main.jsでストアをインポートし、コンポーネントからストアを利用できるようにします。
1 2 3 4 5 6 7 8 9 | import Vue from 'vue' import App from 'App.vue' import store from './store' new Vue({ el: '#app', store, render: h => h(App) }) |
ステートの定義
簡単なメモアプリケーションを想定してストアを定義します。状態として、日付、タイトル、本文を保持できるようにします。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 | import Vue from 'vue' import Vuex from 'vuex' Vue.use(Vuex) const store = new Vuex.Store({ state: { // 初期データ memos: [ { date: '2019/11/30', title: 'タイトル', body: '本文' } ], }, }) export default store |
テンプレートからは、以下のようにステートを利用します。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 | <template> <div> <h3>メモ一覧</h3> <ul> <li v-for="memo in memos" :key="`${memo.date}`"> <span style="font-weigh: bold;">{{memo.title}}</span> {{memo.body}} </li> </ul> </div> </template> <script> export default { computed: { memos () { // ストアからステートを取得 return this.$store.state.memos } } } </script> |
ミューテーションの定義
メモを登録できるようにミューテーションを定義します。
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 | import Vue from 'vue' import Vuex from 'vuex' Vue.use(Vuex) const store = new Vuex.Store({ state: { // 初期データ memos: [ { date: '2019/11/30', title: 'タイトル', body: '本文' } ], }, // 追加 mutations: { addMemo (state, { date, title, body }) { state.memos.push({ date, title, body }) }, }, }) export default store |
テンプレートからは、以下のようにミューテーションを利用します。
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 35 36 37 38 39 40 41 42 43 44 45 46 | <template> <div> <h3>メモ一覧</h3> <ul> <li v-for="memo in memos" :key="`${memo.date}`"> <span style="font-weigh: bold;">{{memo.title}}</span> {{memo.body}} </li> </ul> <form v-on:submit.prevent="addMemo"> <input type="text" v-model='newDate'> <input type="text" v-model='newTitle'> <input type="text" v-model='newBody'> </form> </div> </template> <script> export default { data () { return { newDate: '', newTitle: '', newBody: '' } }, computed: { memos () { // ストアからステートを取得 return this.$store.state.memos } }, methods: { addMemo () { // ミューテーションをコミット this.$store.commit('addMemo', { date: this.newDate, title: this.newTitle, body: this.newBody }) this.newDate = '' this.newTitle = '' this.newBody = '' }, } } </script> |
ゲッターの定義
メモをタイトルで検索するためのゲッターを定義します。
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 35 36 37 38 39 40 41 42 43 44 45 46 47 | import Vue from 'vue' import Vuex from 'vuex' Vue.use(Vuex) const store = new Vuex.Store({ state: { // 初期データ memos: [ { date: '2019/11/30', title: 'タイトル', body: '本文' } ], // 追加 filter, }, // 追加 getters: { filterdByTitle (state) { if (!state.filter) { return state.memos } return state.memos.filter(memo => { return memo.title.indexOf(state.filter) >= 0 }) } }, mutations: { addMemo (state, { date, title, body }) { state.memos.push({ date, title, body }) }, // 追加 changeFilter (state, { filter }) { state.filter = filter }, }, }) export default store |
テンプレートからは、以下のようにゲッターを利用します。
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 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 | <template> <div> <h3>メモ一覧</h3> <input type="text" v-model="filter"> <ul> <li v-for="memo in memos" :key="`${memo.date}`"> <span style="font-weigh: bold;">{{memo.title}}</span> {{memo.body}} </li> </ul> <form v-on:submit.prevent="search"> <input type="text" v-model='filter'> </form> <form v-on:submit.prevent="addMemo"> <input type="text" v-model='newDate'> <input type="text" v-model='newTitle'> <input type="text" v-model='newBody'> </form> </div> </template> <script> export default { data () { return { newDate: '', newTitle: '', newBody: '', filter: '' } }, computed: { memos () { // ゲッターで取得 return this.$store.getters.filterdByTitle } }, methods: { addMemo () { // ミューテーションをコミット this.$store.commit('addMemo', { date: this.newDate, title: this.newTitle, body: this.newBody }) this.newDate = '' this.newTitle = '' this.newBody = '' }, search () { // ミューテーションをコミット this.$store.commit('changeFilter', { filter: this.filter }) }, } } </script> |
アクションを定義
登録したメモを永続化できるようにアクションを定義します。
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 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 | import Vue from 'vue' import Vuex from 'vuex' Vue.use(Vuex) const store = new Vuex.Store({ state: { // 初期データ memos: [ { date: '2019/11/30', title: 'タイトル', body: '本文' } ], filter, }, getters: { filterdByTitle (state) { if (!state.filter) { return state.memos } return state.memos.filter(memo => { return memo.title.indexOf(state.filter) >= 0 }) } }, mutations: { addMemo (state, { date, title, body }) { state.memos.push({ date, title, body }) }, // 追加 load (state, { memos }) { state.memos = memos }, changeFilter (state, { filter }) { state.filter = filter }, }, // 追加 actions: { save ({ state }) { const data = { memos: state.memos } localStorage.setItem('memos', JSON.stringify(data)) }, load ({ commit }) { const data = localStorage.getItem('memos') if (data) { commit('load', JSON.parse(data)) } } } }) export default store |
テンプレートからは、以下のようにアクションを利用します。
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 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 | <template> <div> <h3>メモ一覧</h3> <input type="text" v-model="filter"> <ul> <li v-for="memo in memos" :key="`${memo.date}`"> <span style="font-weigh: bold;">{{memo.title}}</span> {{memo.body}} </li> </ul> <form v-on:submit.prevent="search"> <input type="text" v-model='filter'> </form> <form v-on:submit.prevent="addMemo"> <input type="text" v-model='newDate'> <input type="text" v-model='newTitle'> <input type="text" v-model='newBody'> </form> <button type="button" v-on:click='save'>Save</button> <button type="button" v-on:click='load'>Load</button> </div> </template> <script> export default { data () { return { newDate: '', newTitle: '', newBody: '', filter: '' } }, computed: { memos () { // ゲッターで取得 return this.$store.getters.filterdByTitle } }, methods: { addMemo () { // ミューテーションをコミット this.$store.commit('addMemo', { date: this.newDate, title: this.newTitle, body: this.newBody }) this.newDate = '' this.newTitle = '' this.newBody = '' }, search () { // ミューテーションをコミット this.$store.commit('changeFilter', { filter: this.filter }) }, save () { // ローカルストレージに保存 this.$store.dispatch('save') }, load () { // ローカルストレージからロード this.$store.dispatch('load') }, } } </script> |
さいごに
Vueの状態管理ライブラリであるVuexの機能と使い方を紹介しました。