CH8 Vuex でアプリケーションの状態を管理

CodeSandbox の雛形はこちらです。

https://codesandbox.io/s/pw89zq7kjm

最低限必要なモジュールとファイルのみを追加したものになっています。 Forkして、いろいろ付け足してみてください😺

※ 下のバーからコンソールも使用できます

ストアの参照方法について

このサイトを構築している VuePress で複数のストアを扱っている都合上、このページのコードでは単一ファイルコンポーネントごとに store.js を読み込んでいます。

App.vue 都合上このように読み込んでいる
import store from './store'
export default {
  created() {
    console.log(store.state) // store で参照
  }
}

一般的には、グローバルに登録して使用します。(256ページ参照)

App.vue グローバルに登録していれば import 文不要でこう書ける
export default {
  created() {
    console.log(this.$store.state) // this.$store で参照
  }
}

TIP

パス中の「@」は「src/」のエイリアスです。 もし登録されていない場合は、相対パスとして置き換えてください。

import store from '@/store.js'
import store from './store.js' // main.js からならこうなる

S42 シンプルなストア構造

255ページ

src/store.js
import 'babel-polyfill'
import Vue from 'vue'
import Vuex from 'vuex'
Vue.use(Vuex)

// ストアを作成
const store = new Vuex.Store({
  state: {
    count: 0
  },
  mutations: {
    // カウントアップするミューテーションを登録
    increment(state) {
      state.count++
    }
  }
})
export default store

src/main.js などから src/store.js を読み込んでコンソールログを確認してみましょう。

src/main.js
import store from '@/store.js'

console.log(store.state.count) // -> 0
// incrementをコミットする
store.commit('increment')
// もう一度アクセスしてみるとカウントが増えている
console.log(store.state.count) // -> 1

S43 コアコンセプト

258~263ページ

ステート(state)

const store = new Vuex.Store({
  state: {
    message: 'メッセージ'
  }
})
呼び出し方
store.state.message

ゲッター(getter)

259ページ

src/store.js
const store = new Vuex.Store({
  state: {
    count: 0,
    list: [
      { id: 1, name: 'りんご', price: 100 },
      { id: 2, name: 'ばなな', price: 200 },
      { id: 3, name: 'いちご', price: 300 }
    ]
  },
  getters: {
    // 単純にステートを返す
    count(state, getters, rootState, rootGetter) {
      return state.count
    },
    // リストの各要素の price プロパティの中から最大数値を返す
    max(state) {
      return state.list.reduce((a, b) => {
        return a > b.price ? a : b.price
      }, 0)
    },
    // 引数付きゲッター
    // listからidが一致する要素を返す
    item(state) {
      // 引数を使用するためアロー関数を返している
      return id => state.list.find(el => el.id === id)
    },
    // 別のゲッターを使うこともできる
    name(state, getters) {
      return id => getters.item(id).name
    }
  }
})
呼び出し方
store.getters.count
store.getters.max
呼び出し方(引数付き)
store.getters.item(1)
store.getters.name(1)
src/App.vue
<template>
  <div class="app">
    <h3>引数なし</h3>
    <ol>
      <li>{{ count }}</li>
      <li>{{ max }}</li>
    </ol>
    <h3>引数付き</h3>
    <ol>
      <li>{{ itemA }}</li>
      <li>{{ itemB(1) }}</li>
      <li>{{ nameA }}</li>
      <li>{{ nameB(1) }}</li>
    </ol>
  </div>
</template>

<script>
import store from './store.js'
export default {
  computed: {
    // 引数なしゲッター
    count() { return store.getters.count },   // 1
    max()   { return store.getters.max },     // 2
    // 引数付きゲッター
    itemA() { return store.getters.item(1) }, // 1 👍 いいね
    itemB() { return store.getters.item },    // 2 👎 よくないね
    nameA() { return store.getters.name(1) }, // 3 👍 いいね
    nameB() { return store.getters.name },    // 4 👎 よくないね
  }
}
</script>
DEMO

引数なし

  1. 0
  2. 300

引数付き

  1. { "id": 1, "name": "りんご", "price": 100 }
  2. { "id": 1, "name": "りんご", "price": 100 }
  3. りんご
  4. りんご
guide-ch8-s43-src-App

TIP

引数付きゲッターの itemB / nameB の書き方は便利ですが、結果はキャッシュされません。 算出プロパティを通さない場合も同じです。 何度も使用していたり、コンポーネントの仮想 DOM に変化があるたびに呼び出されてしまうため、コストの高い算出処理をしている場合には注意しましょう!

ミューテーション(mutations)

const store = new Vuex.Store({
  // ...
  mutations: {
    mutationType(state, payload) {
      state.count = payload
    }
  }
})
呼び出し方
store.commit('mutationType', payload)

アクション(actions)

const store = new Vuex.Store({
  // ...
  actions: {
    actionType({ commit }, payload) {
      // アクション内からコミットする
      commit('mutationType')
    }
  }
})
呼び出し方
store.dispatch('actionType', payload)

S44 コンポーネントでストアを使用しよう

264~269ページ

メッセージの状態を管理するストア

264ページ

src/store.js
import Vue from 'vue'
import Vuex from 'vuex'
Vue.use(Vuex)

const store = new Vuex.Store({
  state: {
    message: '初期メッセージ'
  },
  getters: {
    // messageを使用するゲッター
    message(state) {
      return state.message
    }
  },
  mutations: {
    // メッセージを変更するミューテーション
    setMessage(state, payload) {
      state.message = payload.message
    }
  },
  actions: { // メッセージの更新処理
    doUpdate({
      commit
    }, message) {
      commit('setMessage', {
        message
      })
    }
  }
})
export default store

メッセージを使用する

265ページ

src/App.vue
<template>
  <div class="app">
    <h1>{{ message }}</h1>
    <EditForm/>
  </div>
</template>
<script>
import store from './store'
// 子コンポーネントを読み込む
import EditForm from './components/EditForm'
export default {
  name: 'app',
  components: {
    EditForm
  },
  computed: {
    // ローカルの message とストアの message を同期
    message() {
      return store.getters.message
    }
  }
}
</script>

メッセージを更新する

266ページ

「ステートやゲッターに v-model を使用する」もまとめています。

src/components/EditForm.vue
<template>
  <div class="edit-form">
    <h3>バインドとイベントを使った場合</h3>
    <input type="text" :value="message" @input="doUpdate">
    <h3>v-model を使った場合</h3>
    <input v-model="message2">
  </div>
</template>

<script>
import store from '../store'
export default {
  name: 'EditForm',
  computed: {
    message() {
      return store.getters.message
    },
    message2: {
      get() { return store.getters.message },
      set(value) { store.dispatch('doUpdate', value) }
    }
  },
  methods: {
    doUpdate(event) {
      // input の値を持ってディスパッチ
      store.dispatch('doUpdate', event.target.value)
    }
  }
}
</script>
DEMO

初期メッセージ

バインドとイベントを使った場合

v-model を使った場合

guide-ch8-s44-src-App

S45 モジュールで大きくなったストアを分割

270~277ページ

モジュールの使い方

270ページ

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

同一のミューテーションタイプ

271ページ

const moduleA = {
  state: {
    count: 1
  },
  mutations: {
    update(state) {
      state.count += 100
    }
  }
}
const moduleB = {
  state: {
    count: 2
  },
  mutations: {
    update(state) {
      state.count += 200
    }
  }
}
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

ネームスペース

272ページ

※ 書籍ではミューテーション update で引数の state を受け取りわすれていました🙇‍

const moduleA = {
  namespaced: true,
  state: {
    count: 1
  },
  mutations: {
    update(state) {
      state.count += 100
    }
  }
}
const moduleB = {
  namespaced: true,
  state: {
    count: 2
  },
  mutations: {
    update(state) {
      state.count += 200
    }
  }
}
store.commit('moduleA/update') // -> moduleA の update をコミット
store.commit('moduleB/update') // -> moduleB の update をコミット

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

274ページ

const moduleA = {
  namespaced: true,
  getters: {
    test(state, getters, rootState, rootGetters) {
      // 自分自身の item ゲッターを使用 getters['moduleA/item']
      getters.item
      // ルートの user ゲッターを使用
      rootGetters.user

      return [getters.item, rootGetters.user]
    },
    item() { return 'getter: moduleA/item' },
  },
  actions: {
    test({ dispatch, commit, getters, rootGetters }) {
      // 自分自身の update をディスパッチ
      dispatch('update')
      // ルートの update をディスパッチ
      dispatch('update', null, { root: true })
      // ルートの update をコミット
      commit('update', null, { root: true })
      // ルートに登録されたモジュール moduleB の update をコミット
      commit('moduleB/update', null, { root: true })
    },
    update() { console.log('action: moduleA/update') },
  }
}
const moduleB = {
  namespaced: true,
  mutations: {
    update() { console.log('mutation: moduleB/update') }
  }
}

const store = new Vuex.Store({
  modules: {
    moduleA,
    moduleB
  },
  getters: {
    user() { return 'getter: user' }
  },
  mutations: {
    update() { console.log('mutation: update') }
  },
  actions: {
    update() { console.log('action: update') }
  }
})

// 何が呼び出されるか、コンソールログを確認してみよう
store.dispatch('moduleA/test')
console.log(store.getters['moduleA/test'])

モジュールの再利用

277ページ

共通のモジュール
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
  }
})
// 別のデータを読み込んだりする
store.dispatch('moduleA/load', '/path/a.json')
store.dispatch('moduleB/load', '/path/b.json')

主旨はことなるけど管理方法が同じデータ。

材料データ
[
  { "id": 1, "name": "りんご" },
  { "id": 2, "name": "ばなな" }
]
調理道具データ
[
  { "id": 1, "name": "まないた" },
  { "id": 2, "name": "フライパン" }
]

ストアの再利用は、主に管理画面などを作成するときに便利です!

S46 その他の機能やオプション

278~280ページ

ストアの状態を監視する

278ページ

状態の監視
const store = new Vuex.store({ ... })
const unwatch = store.watch(
  (state, getters) => {
    return state.count // 監視したいデータを返す
  },
  (newVal, oldVal) => {
    // 処理
  }
)
コミットやディスパッチの監視
// コミットにフック
store.subscribe((mutation, state) => {
  console.log(mutation.type)
  console.log(mutation.payload)
})
// ディスパッチにフック
store.subscribeAction((action, state) => {
  console.log(action.type)
  console.log(action.payload)
})

Vuexでホットリロードを使用する

279ページ

if (module.hot) {
  module.hot.accept(['@/store/myModule.js'], () => {
    // 更新されたモジュールを読み込む
    const myModule = require('@/store/myModule.js').default
    // 新しい定義をセット
    store.hotUpdate({
      modules: {
        myModule: myModule
      }
    })
  })
}