アプリケーションの規模が大きくなっていくにつれ、状態管理が重要になっていきます。今回の記事では、Vue.jsにおける状態管理ライブラリであるVuexの機能と使い方を紹介します。
VuexはVue.jsにおける状態管理のためのライブラリです。Flux、Elmアーキテクチャ、Reduxなどの影響を受けており、データフローが単方向になるように設計されています。
データフロー設計において頻出するデザインパターンとして、以下の3つがあがります。
これらを意識してデータフローを設計することで、変更に強く柔軟なアプリケーションを作ることが容易になります。
Vuexを使うことで、これらのデザインパターンに沿った実装が容易になります。
Single Source of Truth(信頼できる唯一の情報源)とは、データの管理を一箇所に集約することで、管理を容易にすることを目的としたデザインパターンです。これには、以下のような利点があります。
Vuexはアプリケーションの状態やそれを管理する処理が一箇所にまとまるように設計されているため、上記を満たすことができます。
状態の取得、更新をカプセル化することにより、状態管理のコストを下げることができます。カプセル化を行うことには以下のような利点があります。
Vuexでは状態の更新をミューテーションと呼ばれる機能でのみ行い、状態の取得をゲッターと呼ばれる機能でのみ行うため、上記を満たすことができます。
データフローを単方向にすることで、状態の取得、更新処理が簡潔になります。これには以下のような利点があります。
先程あげたとおり、Vuexでは状態の更新はミューテーションで、取得はゲッターで行うため、実装が強制的に単方向データフローになります。
Vuexではストアの役割を抑えておくことが重要です。ストアは主にアプリケーションの状態を保持する役割をもっています。
Vuexでは前述のSingle Source of Truthを前提に実装されているため、アプリケーション内で常に1つのストアのみが存在するようにします。
ストアは、ステート、ゲッター、ミューテーション、アクションという4つの要素から成り立っています。それぞれについて順次説明します。
ステートはアプリケーション全体の状態を保持するオブジェクトです。すべてのステートは1つの木構造として表現されます。アプリケーションのすべての状態を1つの木としてステートを保持することで、Single Source of Truthを実現しています。
しかし、すべてのステートを1つのファイルで実装しなければならないというわけではありません。ステート数が多くなった場合はモジュールとして分割管理することで、状態管理が複雑になりすぎないようにします。
また、アプリケーションのすべてのデータをステートで管理すべきというわけではありません。あくまで、アプリケーション全体で使用するデータのみステートで管理すべきです。
ステートでの管理に適したデータとしては以下のようなものがあります。
コンポーネントで管理したほうが良いデータとしては以下のようなものがあります。
ゲッターはステートから別の値を算出するために使われます。例えば、ユーザの操作で商品のリストを絞り込みたい場合は、ゲッターで商品の絞り込みを行ってからリストを返却します。
ゲッターを利用することで、異なるコンポーネント間でロジックを共有しやすくなります。また、算出ロジックをストア内に置くことで、処理を探しやすく、テストがしやすくなるという利点があります。
ゲッターはcomputedと同様に値がキャッシュされるため、よく利用するステートの算出ロジックをゲッターにすることでパフォーマンスの向上が期待できます。
ミューテーションはステートを更新するために使われます。Vuexでは原則的にミューテーション以外がストアを更新することを禁止しています。ステートの更新をミューテーションのみが行うことで、ステートの変更がいつ、どこで行われたか追跡しやすくなります。
アクションは非同期処理や外部APIとの通信を行い、最終的にミューテーションを呼び出すために使われます。
それでは、Vuexの使い方をコード例を交えて説明します。
Vuexはnpmコマンドでインストールすることができます。
$ npm install vuex
main.jsでストアをインポートし、コンポーネントからストアを利用できるようにします。
import Vue from 'vue' import App from 'App.vue' import store from './store' new Vue({ el: '#app', store, render: h => h(App) })
簡単なメモアプリケーションを想定してストアを定義します。状態として、日付、タイトル、本文を保持できるようにします。
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
テンプレートからは、以下のようにステートを利用します。
<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>
メモを登録できるようにミューテーションを定義します。
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
テンプレートからは、以下のようにミューテーションを利用します。
<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>
メモをタイトルで検索するためのゲッターを定義します。
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
テンプレートからは、以下のようにゲッターを利用します。
<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>
登録したメモを永続化できるようにアクションを定義します。
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
テンプレートからは、以下のようにアクションを利用します。
<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の機能と使い方を紹介しました。