基礎からメモ: Vue.js CH8-p270 モジュールでストアを分割する

モジュールの機能を使って、ストアを名前空間で分割し管理することができる。

moduleA と moduleB という名前でストアを分割。ストアには modulesオプションに登録する。

import Vue from 'vue'
import Vuex from 'vuex'
Vue.use(Vuex)

const moduleA = {
  state: {
    count: 1
  },
  mutations: {
    update(state) { state.count += 100 }
  }
}
const moduleB = {
  state: {
    count: 2
  },
  mutations: {
    update(state) { state.count += 200 }
  }
}

const store = new Vuex.Store({
  modules: {
    moduleA,
    moduleB
  }
})

export default store

main.jsに2つのストアを出力するコードを書いて確認してみる。

console.log(store.state.moduleA.count)  // -> 1
console.log(store.state.moduleB.count)  // -> 2
store.commit('update')
console.log(store.state.moduleA.count)  // -> 101
console.log(store.state.moduleB.count)  // -> 202

AとB2つのモジュールはupdateという同じ名前のミューテーションを持っている。一度updateにコミットしただけでAとB両方のミューテーションが実行される。

ネームスペースで分けてコミットする

モジュール定義でnamespacedオプションをtrueに設定することで、ネームスペース別にコミットさせることが可能。

const moduleA = {
  state: {
    count: 1
  },
  namespaced: true,
  mutations: {
    update(state) { state.count += 100 }
  }
}
const moduleB = {
  state: {
    count: 2
  },
  namespaced: true,
  mutations: {
    update(state) { state.count += 200 }
  }
}

コミットやディスパッチの際、ディレクトリパスのように/で区切った書き方でネームスペースを付けて呼び出す。

commit('<ネームスペース>/<データ名>')
console.log(store.state.moduleA.count)  // => 1
console.log(store.state.moduleB.count)  // => 2
store.commit('moduleA/update')
console.log(store.state.moduleA.count)  // => 101
console.log(store.state.moduleB.count)  // => 2
store.commit('moduleB/update')
console.log(store.state.moduleA.count)  // => 101
console.log(store.state.moduleB.count)  // => 202

AとBのupdateそれぞれが別個にコミットされているのがわかる。

ゲッターの呼び出しは次のような形になる。

const moduleA = {
  state: {
    count: 1
  },
  namespaced: true,
  getters: {
    count(state) { return state.count }
  }
}
const moduleB = {
  state: {
    count: 2
  },
  namespaced: true,
  getters: {
    count(state) { return state.count }
  }
}
console.log(store.getters['moduleA/count'])  // => 1
console.log(store.getters['moduleB/count'])  // => 2

フォームからのデータ変更をネームスペースで分ける

インプットフォームからの入力でメッセージを変更するコードをメッセージAとメッセージBで別々に変えてみる。

[store.js]

const moduleA = {
  state: {
    message: '初期メッセージA'
  },
  namespaced: true,
  mutations: {
    setMessage: (state, message) => {
      state.message = message
    }
  },
  getters: {
    message: state => state.message
  }
}

const moduleB = {
  state: {
    message: '初期メッセージB'
  },
  namespaced: true,
  mutations: {
    setMessage: (state, message) => {
      state.message = message
    }
  },
  getters: {
    message: state => state.message
  }
}

App.vueのテンプレートにはメッセージをAとBの2つ並べる。算出プロパティで各メッセージを得る場合にネームスペース付きでモジュール別のゲッターを呼ぶ。

[App.vue]

<template>
  <div id="app">
    <p>{{ messageA }}</p>
    <p>{{ messageB }}</p>
    <EditForm/>
  </div>
</template>

<script>
import EditForm from '@/components/EditForm.vue'

export default {
  name: 'App',
  components: {
    EditForm
  },
  computed: {
    messageA() { return this.$store.getters['moduleA/message'] },
    messageB() { return this.$store.getters['moduleB/message'] },
  }
}
</script>

<style>
#app {
  text-align: center;
  color: #2c3e50;
  margin-top: 60px;
}
</style>

フォームのコンポーネントでは、インプットフォームをAとBの2つ用意し、$refsの名前、バインドするメッセージ名、イベントハンドラ名などもAとBで切り分ける。

[EditForm.vue]

<template>
  <div class="edit-form">
    <p>メッセージAを変更: <input type="text" ref="inputA" :value="messageA" @input="setMessageA"></p>
    <p>メッセージBを変更: <input type="text" ref="inputB" :value="messageB" @input="setMessageB"></p>
    <!-- <input type="text" ref="input"> -->
  </div>
</template>

<script>
export default {
  name: 'EditForm',
  computed: {
    messageA() { return this.$store.getters['moduleA/message'] },
    messageB() { return this.$store.getters['moduleB/message'] },
  },
  methods: {
    setMessageA() {
      this.$store.commit('moduleA/setMessage', this.$refs.inputA.value)
    },
    setMessageB() {
      this.$store.commit('moduleB/setMessage', this.$refs.inputB.value)
    }
  }
}
</script>

これでインプットフォームのAとBの入力で別々に表示が同期する。

名前空間以外は、呼び出すミューテーション名が同じなので記述を簡略化できる。

ヘルパーでネームスペースを指定する

コンポーネントでヘルパーを使う際に、/で区切った形でネームスペースを指定するか、第一引数にネームスペースを指定する。

methods: {
  //  `/`で区切った形でネームスペースを指定
  ...mapActions({ add: 'moduleA/add'})
  // 第一引数にネームスペースを指定
  ...mapActions('moduleA', ['add', 'update', 'remove'])
}

先の例に使ってみる。データを更新するストアのアクション名はシンプルにsetで統一してみた。第二引数にイベントオブジェクトが渡されることに注意する。

[store.js]

const moduleA = {
  state: {
    message: '初期メッセージA'
  },
  namespaced: true,
  mutations: {
    setMessage(state, payload) {
      state.message = payload
    }
  },
  actions: {
    set({commit}, ev) {
      commit('setMessage', ev.target.value)
    }
  },
  getters: {
    message: state => state.message
  }
}

const moduleB = {
  state: {
    message: '初期メッセージB'
  },
  namespaced: true,
  mutations: {
    setMessage(state, payload) {
      state.message = payload
    }
  },
  actions: {
    set({commit}, ev) {
      commit('setMessage', ev.target.value)
    }
  },  
  getters: {
    message: state => state.message
  }
}

フォームコンポーネントでヘルパーをネームスペース付きで呼び出す形にする。

[EditForm.vue]

<template>
  <div class="edit-form">
    <p>メッセージAを変更: <input type="text" ref="inputA" :value="messageA" @input="setA"></p>
    <p>メッセージBを変更: <input type="text" ref="inputB" :value="messageB" @input="setB"></p>
  </div>
</template>

<script>
import { mapActions } from 'vuex'

export default {
  name: 'EditForm',
  computed: {
    messageA() { return this.$store.getters['moduleA/message'] },
    messageB() { return this.$store.getters['moduleB/message'] },
  },
  methods: {
    ...mapActions({setA: 'moduleA/set', setB: `moduleB/set`}),
  }
}
</script>

モジュールのネスト

モジュールのmodulesオプションに登録することで、モジュールからさらに別のモジュールを読み込むことができる。

const modyleA = {
  namespaced: true,
  modules: {
    moduleC
  }
}

ディレクトリと同じように、/で区切った表現で呼び出す。

store.commit('moduleA/moduleC/update')

ネームスペース付きモジュールから外部にアクセス

moduleAがネームスペースを持っていると仮定すると、ゲッターのentriesオプションに渡される引数で、自分のstategetters、さらにルートのステートやゲッターが渡される。

getters: {
  entries(state, getters, rootState, rootGetters) {
    // 自分自身のitemゲッター( getters['moduleA/item'] )
    getters.item
    // ルートのuserゲッター
    rootGetters.user
  }
}

アクションでは第一引数のオブジェクトからrootGettersを受け取ることができる。コミットやディスパッチする場合に、第3引数のrootオプションをtrueにする。

actions: {
    actionType({ dispatch, commit, getters, rootGetters}) {
    // 自分のupdateアクションをディスパッチ
    dispatch('update')
    // ルートのupdateをディスパッチ
    dispatch('update', null, {root: true})
    // ルートのupdateをコミット
    commit('update', null, {root: true})
    // ルートに登録されたモジュールBのupdateをコミット
    commit('moduleB/update', null, {root: true})
  }
}

自分自身に登録されている別のモジュールは相対パスのような書き方でアクセスできる。

commit('moduleC/update')

モジュールをファイルごとに分ける

モジュールは個別にファイルに分割して読み込むこともできる。 Vuexモジュールはsrcディレクトリ直下に、慣例として「store」とか「vuex」といった名前のサブディレクトリにまとめる。

例えばモジュールAの定義をsrc/store/a.js

export default {
  state: { .... },
  mutations: { .... }
}

ストアのルートはsrc/store.jsとかsrc/store/index.jsなので、その中で各モジュールを読み込みmodulesオプションに登録する。

import moduleA from '@/store/a.js'
import moduleB from '@/store/b.js'
const store = new Vuex.Store({
  modules: {
    moduleA,
    moduleB
  }
})

モジュールの再利用

モジュールもコンポーネントと同様にstateを関数にすることで再利用できる。

const myModule = {
  namespaced: true,
  state() {
    return {
      entries: []
    }
  },
  mutations: {
    set(state, payload) { state.entries = payload }
  },
  actions: {
    load({ commit }, file) {
      axios.get(file).then(response => {
        commit('set', response.data)
      })
    }
  }
}

ルートのストアにモジュールを登録する。

const store = new Vuex.Store({
  modules: {
    moduleA: myModule,
    moduleB: myModule
  }
})

モジュールAとモジュールBはともにmyModuleという同じモジュール定義。呼び出す際にネームスペース付きで、渡すパラメータを替えるなどして呼び出すことで定義を使いまわすことができる。

store.dispatch('moduleA/load', 'path/a.json')
store.dispatch('moduleB/load', 'path/b.json')