Help us understand the problem. What is going on with this article?

Vuexについてまとめ、使ってみる。

この記事は?

Vue.jsでは状態管理ライブラリとしてVuexが提供されています。
この記事では以下のようなことをまとめ、VuexのHello Worldとして、Vue.jsで作成したカウンターアプリにVuexを追加してみようと思います。

  • クライアントサイドでのコンポーネント間における状態管理の必要性
  • Vueのアーキテクチャ

logo.png

クライアントサイドでのコンポーネント間における状態管理の必要性

昨今、SPA(Single Page Application)と呼ばれるWebサイトの構築手法が流行し、旧来サーバーサイドで実行していたロジックの一部がクライアントサイドへ移行してきています。
そのような流れの中で、クライアントサイドの状態管理には以下のような特性が強くなりました。

  • 複数のビューが同じ状態に依存する
  • 異なるビューからのイベントで、同じ状態を更新する

ここで、上記のような特性を考慮した状態管理が行われていないと、画面の一部が更新されないといったようなバグの原因になります。
素のVue.jsも状態管理の機能は持っていますが、MVVMと呼ばれる状態管理パターンを採用しておりこのパターンは複数のコンポーネント間の状態管理を行うことに問題があると考えられています。
そこで登場するのが、Vuexです。

Vuexとは

VuexはVue.jsのために作成された状態管理ライブラリです。ちなみに読み方は「びゅーえっくす」みたいです。

Vue.jsの状態管理との違い

上記のようにVue.jsにも状態管理の機能はあります。これは、MVVMと呼ばれるパターンを採用しており、図示すると以下のようになります。

MVVM.png

このパターンは双方向バインドを可能とし、Viewの更新された場合も、Modelが更新された場合でもお互いにイベントを検知することができます。これはコンポーネントが少ない状態であればよく働きます。
しかし、ここで、コンポーネントが増えていくと以下のような状態になっていきます。

MVVM-complicated.png

状態の依存関係が複雑になりメンテナンス性を落とします。上記の図ではわかりづらく感じるので、どういうことなのかもう少し掘り下げてみます。
Vue.jsはコンポーネントシステムを採用しており、コンポーネントには親子関係が存在します。
ここで、親コンポーネントは子コンポーネントへ状態を受け渡す必要があり、さらに状態はそれぞれのコンポーネントで状態管理されます。

vuecomp.png

このような状況下では以下のような問題が起こります。

  • 親コンポーネントから子コンポーネントへの状態の受け渡しが煩雑になる。
  • 状態変更イベントが複雑化しメンテナンス性がさがる。

後者の一例を示すと、状態変更イベントの通知は下の図のような流れで実施しなければなりません。

vuecomp2.png

例はまだ単純ですが、業務アプリの場合コンポーネントは多数で依存関係はさらに複雑になることがあり得ます。
別画面の更新で、同じ状態を同期的に変更していくのは非常に難しくなっていくでしょう。
このような問題に対してVuexではFlux、Redux、The Elm Architectureから影響を受けた以下のような状態管理を行います。

vueflux.png

グローバルに参照できるストアを用意することで、Vueインスタンスのそれぞれが同じデータにアクセスすることができ、値の受け渡しなどの複雑性を排除します。
また、更新の流れが一方向であり、かつ、更新時の役割を分割することによってメンテナンス性を向上させることができます。

Vuexのアーキテクチャ

VuexではVue.jsの状態管理とは違い以下の概念が登場します。

  • StateとStore
  • Action
  • Mutations
  • Getter

StateとStore

StateはVuexでコンポーネント間でグローバルな状態を持つ単一のステートツリーです。StateはStoreで管理されます。
Stateはアプリケーション内で1つだけ作成されます。そのため、アプリケーションが信頼できる唯一の情報源として機能します。
ここで、Vuexではすべての状態をStateに入れて管理をすべきであるとは言っておらず状態が1つのコンポーネントで完結しているのであればStateに状態を入れておく必要はありません。

Mutations

VuexのStateを変更する唯一の方法はMutationをコミットすることです。

const store = new Vuex.Store({
  state: {
    count: 1
  },
  mutations: {
    increment (state) {
      // 状態を変更する
      state.count++
    }
  }
})

Mutationはイベントに近い概念を持っておりタイプとハンドラを持ちます。
例えば上記のコードではincrementがタイプで、メソッド部分を含めるとハンドラになります。
Mutationは直接呼び出すことはできず下記のようにミューテーションタイプを指定しコミットすることになります。

store.commit('increment')

また、状態変更を非同期に組み合わせることは、プログラムの動きを予測することを非常に困難にするため、Mutationでは非同期な処理を記述することはできません。
Vuexすべての状態変化は同期的に行うということが作法となっています。

Action

アクションではMutationのコミットを行います。
アクションではMutationと違い非同期な処理を書くことができます。

const store = new Vuex.Store({
  state: {
    count: 0
  },
  mutations: {
    increment (state) {
      state.count++
    }
  },
  actions: {
    increment (context) {
      context.commit('increment')
    }
  }
})

アクションではcontextオブジェクトを受けとります。context.commitでMutationをコミットすることもでき、context.getterでgetterにアクセスすることも可能です。
アクションそのものは、store.dispatchがトリガーとして実行されます。

store.dispatch('increment')

context.commitを直接呼び出さないのはAction内で非同期な処理が記述されることが可能であるからです。

actions: {
  incrementAsync ({ commit }) {
    setTimeout(() => {
      commit('increment')
    }, 1000)
  }
}

Getter

例えばStateの状態からフィルタリングして値と取り出したいと考えたときに、Getterが使えます。


const store = new Vuex.Store({
  state: {
    todos: [
      { id: 1, text: '...', done: true },
      { id: 2, text: '...', done: false }
    ]
  },
  getters: {
    doneTodos: state => {
      return state.todos.filter(todo => todo.done)
    }
  }
})

Getterは以下のように呼び出すことが可能です。

store.getters.doneTodos

Getterを利用することにより、処理の再利用性を高めることができます。

Hello World

Vue.js製のカウンターアプリにVuexを追加してみたいと思います。

環境

  • Vue CLI(v3.11.0)
  • Node(v11.9.0)
  • Chrome

前回の記事で作ったカウンターアプリ

Vue.jsについてまとめて、カウンターアプリを作ってみる。 」で作成したカウンターアプリに対してVuexを導入します。そこまで複雑なアプリでないので、この記事を先に読むことは必須ではないです。しかし、作成されているカウンターアプリの構造に把握していることを前提に進めますので、ソースコードに一度目を通してみてください。(そんなに時間はかからないと思います。)

Vuexの導入

まずはVuexをインストールします。

$ npm install vuex --save

package.jsonvuexの依存が追加されました。

  "dependencies": {
    "core-js": "^2.6.5",
    "vue": "^2.6.10",
    "vuex": "^3.1.1"
  }

次にStoreを作成します。StoreはVuexのStateを管理するためのコンテナです。
counter.jsという名前でファイルを作成し以下のコードを記述します。

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

Vue.use(Vuex)

export default new Vuex.Store(
    {
        state:{
            num: 0
        },
        mutations:{
            increment(state) {
                state.num++
            },
            decrement(state) {
                state.num--
            }
        },
        actions:{
            increment(context) {
                context.commit('increment')
            },
            decrement(context) {
                context.commit('decrement')
            }
        }
    }
)

まず、Vue.use(Vuex)の箇所ではVue.jsに対しプラグインの設定を行いってます。今回はVuexのプラグインを使うのでその登録を行っています。
Vuex.Store(....)の箇所で実際にStoreを作成します。statemutationsactionsをそれぞれ定義しています。今回はgetterは必要ないので作成しませんでした。

上記で作成したStoreをmain.jsで読み込ませます。


import Vue from 'vue'
import App from './App.vue'
import store from './store'

Vue.config.productionTip = false

new Vue({
    store,
  render: h => h(App),
}).$mount('#app')

その後作成したStoreのActionを呼び出すDispatch部分と、初期値のStateをStoreから取り出す部分の記述を行います。

<template>
    <div id="counter">
        <span>{{ num }} </span>
        <button @click="increment">up</button>
        <button @click="decrement">down</button>
        <p v-if="num === 0">
            Press 'up' or 'down' button!!!!
        </p>
    </div>
</template>

<script>
    import {mapState, mapActions} from 'vuex'

    export default {
        name: 'Counter',
        computed: mapState({
            num: state => state.num
        }),
        methods: mapActions([
            'increment',
            'decrement'
        ])
    }
</script>

computedは算出プロパティーと呼ばれるもので、この内部で定義されているnumが変更された際に依存関係にあるコンポーネントの部分が更新される仕組みになっています。
computedではさらにmapStateと呼ばれるVuexで用意されているヘルパー関数を利用しています。このヘルパー関数を利用することによって複数のStateを利用する際の冗長なコードを削減することができます。
例えば、mapStateを使わないと複数Stateを扱いたい場合に以下のような実装になります。

// mapStateを使わない場合
computed:{
    hoge(){
        return this.$store.state.hoge
    },
    fuga(){
        return this.$store.state.fuga
    }
}

// 使った場合
computed: mapState({
            hoge: state => state.hoge,
            fuga: fuga => state.fuga 
        }),

今回は、Stateが1つしかないのであまり使う意味はなかったかもしれません。。。

次に、methodsですが、ここではActionの呼び出しを行っています。ここでもヘルパー関数のmapActionを利用しています。
mapActionsを使わない場合this.$store.dispatch("ACTION_TYPE")を呼び出す関数をそれぞれ定義することになりこれも冗長になります。

ひとまず、ここまでで以前作成したカウンターアプリにVuexを導入できました。
ただ、これだけだとVuexを導入した面白味に欠けるので、1つコンポーネントを作成して、numプロパティを複数個所から読み込めるようにしてみたいと思います。
numに任意の数字を足し引きできるフォームを追加します。
まずはcounter.jsにActionとMutationを追加します。


mutations:{
    increment(state) {
        state.num++
    },
    decrement(state) {
        state.num--
    },
    // 追加
    calc(state, n){
        state.num +=n
    }
},
actions:{
    increment(context) {
        context.commit('increment')
    },
    decrement(context) {
        context.commit('decrement')
    },
    // 追加
    calc(context, n){
        context.commit('calc', n)
    }
}

次にフォーム用のVueコンポーネントを1つ追加します。
CalcNumForm.vueを作成しまします。

<template>
    <div>
        <input v-model="calcNum" placeholder="type number">
        <button @click="calc(calcNum)" >calculate</button>
    </div>
</template>

<script>
    import {mapState} from 'vuex'

    export default {
        name: "CalcNumForm",
        computed: mapState({
            num: state => state.num
        }),
        methods: {
            calc: function (calcNum) {
                this.$store.dispatch('calc', parseInt(calcNum))
            }
        }
    }
</script>

まず、templateタグのinputの部分ですが、v-modelを利用しています。
v-modelはVue.jsのタグディレクティブでinput要素などで、双方向バインディングを作成してくれます。
次にscriptタグの中で、ステートのバインディングとActionの呼び出しを行っています。
今回、Actionの引数として、入力された値を渡したかったので、呼び出しにはmapActionsを使わずにthis.$store.dispatchを呼び出しています。
(mapActionsでもできるのかもしれませんが、やり方がいまいちわからなかったです。)
そして、作成したコンポーネントをApp.vueから呼び出して表示します。
これで動くはずです。

Devサーバを起動して動かしてみます。

counter1.JPG
counter2.JPG
counter3.JPG

動きました。
実はこのアプリは入力ボックスに数値以外のものを入れてしまうとNaNになってしまうのですが、ここではその対応までは作りこみません。
一旦、VuexへのHello Worldはできた気がします。

感想

今回使用したカウンターアプリではVuexの利点はわかりづらいかもしれませんが、業務レベルの巨大なアプリケーションになればもう少しVuexの利点を身近に感じれるのではないかと感じました。
あとは、今回は触りませんでしたが、Stateが大きくなってきた際にその定義を分割できるモジュールと呼ばれる機能もあるようです。

参考資料

Why do not you register as a user and use Qiita more conveniently?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
Comments
Sign up for free and join this conversation.
If you already have a Qiita account
Why do not you register as a user and use Qiita more conveniently?
You need to log in to use this function. Qiita can be used more conveniently after logging in.
You seem to be reading articles frequently this month. Qiita can be used more conveniently after logging in.
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away