Vue.js の状態管理ライブラリ Vuex のはじめかた -『Vue.js』

table of contents
ー

Vuex は Vue.js の状態管理ライブラリです。状態管理ライブラリとは、散らばったコンポーネントそれぞれにデータを持たせるのではなく、Vuexで言うと store の state と呼ぶところにデータを集約させ、それを介してコンポーネント同士にデータをやりとりさせ、処理の流れを一貫させたり、データの見通しを良くするために利用します。

また、MVVMのデザインパターンにおいては、Model の役割を果たします。ざっくり言うと、各コンポーネントの <template> 部分が View 、<script> 部分が ViewModel ですね。ただ、Vuex で MVVM するには、ちょっとルール作りや決め事をする必要があるかもしれません。

Vuexをはじめる

インストールします。

terminal


yarn add vuex --dev

Vuex の Store を理解する

Store には state mutations actions getters を定義します。(sに注意)

Store/index.js


import Vuex from 'vuex'
import Vue from 'vue/dist/vue.common.js'

Vue.use(Vuex)

const store = () => new Vuex.Store({

  state: { ... },
  mutations: { ... },
  actions: { ... },
  getters: { ... }

})

export default store

Store は非常にシンプルな構成になっていますが、後述しますが、階層構造を持たせることで名前空間を持たせ、モジュール化させることも可能です。

Vue インスタンスに Store を渡します。

index.js


import Vue from 'vue/dist/vue.common.js'
import Store from './store/index.js'

// Vue root
new Vue({
  Store,
  // ...
})

Vuex のデータフロー

まず Vuex のデータフローの公式図解を見てみます。

Vuex のデータフローの公式図解

データは state が保持していて、コンポーネントに呼び出される。データの更新が必要なとき、コンポーネントは dispatch メソッドで action を呼び出して、 action は API などと非同期通信をして値を受け取り、その値を commit メソッドで mutation を呼び出して渡す。そして mutation が state の値を更新する。

というようなサイクルを上の図は表しています。 Action が必ず commit して mutation を呼び出すだけでなく、 コンポーネントは直接 commit して mutation を呼び出すことも可能になっています。

イメージしにくいかもですが、重要なのは mutation が state を書き換えることができるということです。

state – 状態

  • store のデータをここで保持
  • componentの data: function() {returen { ... }} のようなもの
  • mutation にのみ変更される

Store/index.js


  state: {
    counter: 0
  }

component.vue


<template>
  <div>
    {{$store.state.counter}}
  </div>
</template>

<script>
  console.log(this.$store.state.counter);
</script>

mutation – stateの変更

  • state を変更する
  • commit で呼び出される

まず Store に定義します。

store/index.js


  mutations: {
    increment (state, payload) {
      state.counter++
    }
  }

payload はオプション引数です。

呼び出し側は、まず Store で定義したincrement と同じ関数名で、コンポーネントの中でも利用したい場合は、

component.vue


import {mapMutations} from 'vuex'

  methods: {
    ...mapMutations([
      'increment'
    ])
  }

のように書きます。

呼び出したコンポーネントで、ローカルな名前をつけたい場合は、


import {mapMutations} from 'vuex'

  methods: {
    ...mapMutations({
      localIncrement: 'increment'
    })
  }

また、このようにも書けます。


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

先に挙げた ...mapMutations で呼び出すと、commit() を省略して書くことができます。

action – 非同期ロジック

  • commit して mutation を呼び出す
  • 非同期処理を書く、Promise を返す
  • APIから値を受け取る
  • ルートのステートやゲッター、別の action にアクセスできる
  • state を変更しない
  • dispatch で呼び出される

store/index.js


  actions: {
    number ({commit, dispatch, state, rootState, getters, rootGetters}, payload) {
      commit('number', payload)
    }
  }

action は受け取る引数が多く、getter や他の action に処理をつなげるなど、複雑な加工ができます。ただし、ここから直接 state を変更しないようにします。

呼び出しは、mutation と同じように書けます。

component.vue


import {mapActions} from 'vuex'

  methods: {
    ...mapActions([
      'number'
    ])
  }


import {mapActions} from 'vuex'

  methods: {
    ...mapActions({
      localNumber: 'number'
    })
  }

action と mutation の違いは、 dispatch() で呼び出すことです。


  methods: {
    number(val) {
      this.$store.dispatch('number', val)
    }
  }

非同期処理のサンプルを書いておきます。

store/index.js


  actions: {
    increment ({commit}) {
      commit('increment')
    },
    number ({commit}, value) {
      commit('number', value)
    },
    async timers ({dispatch}) {
      let delay = await dispatch('timer', 1000)
      console.log(delay)
      delay = await dispatch('timer', 3000)
      console.log(delay)
      console.log('finish')
    },
    timer ({commit}, delay) {
      return new Promise((resolve, reject) => {
        setTimeout(() => {
          resolve(delay)
        }, delay)
      })
    }
  }

component.vue


<template>
  <button @click="timers">timer</button>
</template>

<script>
  import {mapActions} from 'vuex'
  export default {
    methods: {
      ...mapActions([
        'timers'
      ])
    }
  }
</script>

getter – stateのフィルタ

  • コンポーネントに値を返す
  • state をフィルタリングする
  • 値の加工をここでしないように注意

store/index.js


getters: {
  count(state, getters, rootState) { ... }
}

引数を渡したい場合は、このように書きます。


getters: {
  count: (state) => (arg) => state.count[arg]
}

memo

MVVM パターンでは、Model に getter は書かないという原則がありますが、Vue には getter があります。

これは、storeの値を切り出したり、加工して使いたいとき、処理を各コンポーネントのに書いたり、それを import して使い回すことに対してのヘルパー的な機能です。

ただここで、getter に色々な役割を持たせると、ViewModel と Model に処理が散らばる可能性が大きいので、store をフィルタリングする、一部を切り分けて取り出すことのみに利用する、というようにルール付けしておくのが有効です。

Vuex の応用的なTips

双方向バインディング (v-model)

computed 算出プロパティにセッターを定義して、v-model などのバインディングを実装します。

component.vue


<template>
  <div>
    <input type="text" v-model="counter">
    <button @click="increment">{{ counter }}</button>
  </div>
</template>

<script>
  import {mapActions} from 'vuex'

  export default {
    methods: {
      ...mapActions([
        'increment'
      ])
    },
    computed: {
      counter: {
        get: function () { return this.$store.state.counter },
        set: function (val) { this.$store.dispatch('number', val) }
      }
    }
  }
</script>

store/index.js


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

Vue.use(Vuex)

const store = () => new Vuex.Store({

  state: {
    counter: 0
  },
  mutations: {
    increment (state) {
      state.counter++
    },
    number (state, value) {
      state.counter = value
    }
  },
  actions: {
    increment ({commit}) {
      commit('increment')
    },
    number ({commit}, value) {
      commit('number', value)
    }
  }

})

export default store

Storeのモジュール化 (名前空間)

namespaced: true でStoreを入れ子に、階層を持たせ名前空間を確保できます。


const Layout = {
  namespaced: true,
  state: {
    layoutData: 'this is layout'
  },
  mutations: {},
  actions: {
    layoutAction({commit}) {
      // root の Mutation を呼び出す
      commit('rootMutation', null, {root: true})
    }
  }
};

const Meta = {
  state: {},
  mutations: {},
  actions: {},
  getters: {}
};

export default new Vuex.Store({
  state: {
    rootData: 'this is root'
  },
  mutations: {
    rootMutation(state) {
      console.log(state.rootData);
    }
  },
  modules: {
    Layout,
    Meta
  }
})

呼び出すときは、第一引数にパスを与えます。


methods: {
  mapActions('Layout', [
    'layoutAction'
  ])
}

Vuex の導入にあたって

Vue.js で綺麗に MVVM を実装するのには Vuex はとても有用で、大規模開発にいいという情報もおおいですが、単一コンポーネントで開発するなら迷わず導入してしまって構わないと思います。普段から慣れてしまって、Vuex のデータフローに馴染んでおくことが大事だなーとも思います。

MVVM のまとめ (おまけ)

View

  • UI と UI の描画に必要なロジック
  • ViewModel に依存
  • なにかを実行するとき ViewModel の コマンドを叩く

ViewModel

  • Presentation ロジックと State (データ)
  • Model に依存
  • View が叩いたコマンドを Model へ Dispatch (発信)する
  • Modelを監視する
  • View と双方向データバインディング
  • Viewに揮発性のあるデータを送るときはMessengerを発行

Model

  • Business ロジックと Domain を持つ
  • View と ViewModel に依存しない
  • 自身を更新するための関数
  • 自身が更新された場合はイベントを発火する

おわります。

share
ー

related
ー