ミューテーション

実際に Vuex のストアの状態を変更できる唯一の方法は、ミューテーションをコミットすることです。Vuex のミューテーションはイベントにとても近い概念です: 各ミューテーションはタイプハンドラを持ちます。ハンドラ関数は Vuex の状態(state)を第1引数として取得し、実際に状態の変更を行います:

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

直接ミューテーションハンドラを呼び出すことはできません。この mutations オプションは、どちらかいうと "タイプが increment のミューテーションがトリガーされたときに、このハンドラが呼ばれる" といったイベント登録のようなものです。ミューテーションハンドラを起動するためにはミューテーションのタイプを指定して store.commit を呼び出す必要があります:

store.commit('increment')

追加の引数を渡してコミットする

store.commit に追加の引数を渡すこともできます。この追加の引数は、特定のミューテーションに対するペイロードと呼びます:

// ...
mutations: {
  increment (state, n) {
    state.count += n
  }
}
store.commit('increment', 10)

ほとんどの場合、ペイロードはオブジェクトにすべきです。そうすることで複数のフィールドを含められるようになり、またミューテーションがより記述的に記録されるようになります:

// ...
mutations: {
  increment (state, payload) {
    state.count += payload.amount
  }
}
store.commit('increment', {
  amount: 10
})

オブジェクトスタイルのコミット

また type プロパティを持つオブジェクトを使って、ミューテーションをコミットすることもできます:

store.commit({
  type: 'increment',
  amount: 10
})

オブジェクトスタイルでコミットするとき、オブジェクト全体がペイロードとしてミューテーションハンドラに渡されます。したがってハンドラの例は上記と同じです:

mutations: {
  increment (state, payload) {
    state.count += payload.amount
  }
}

Vue のリアクティブなルールに則ったミューテーション

Vuex ストアの状態は Vue によってリアクティブになっているので、状態を変更すると、状態を監視している Vue コンポーネントは自動的に更新されます。これは Vuex のミューテーションは、通常の Vue と動作させているときと同じく、リアクティブな値に関する注意が必要であることを意味します:

  1. あらかじめ全ての必要なフィールドによって、ストアの初期状態を初期化することが望ましいです

  2. 新しいプロパティをオブジェクトに追加するとき、以下のいずれかが必要です:

  • Vue.set(obj, 'newProp', 123) を使用する。あるいは

  • 全く新しいオブジェクトで既存のオブジェクトを置き換える。例えば、スプレッドシンタックス(object spread syntax) を使用して、次のように書くことができます:

    state.obj = { ...state.obj, newProp: 123 }
    

ミューテーション・タイプに定数を使用する

いろいろな Flux 実装において、ミューテーション・タイプに定数を使用することが共通して見られるパターンです。これはコードに対してリントツールのようなツールを利用できるという利点があり、また単一ファイルに全ての定数を設定することによって、共同で作業する人に、アプリケーション全体で何のミューテーションが可能であるかを一目見ただけで理解できるようにします:

// mutation-types.js
export const SOME_MUTATION = 'SOME_MUTATION'
// store.js
import Vuex from 'vuex'
import { SOME_MUTATION } from './mutation-types'

const store = new Vuex.Store({
  state: { ... },
  mutations: {
    // 定数を関数名として使用できる ES2015 の算出プロパティ名(computed property name)機能を使用できます
    [SOME_MUTATION] (state) {
      // 状態を変更する
    }
  }
})

定数を使用するかどうかは好みの問題です。多くの開発者による大規模なプロジェクトで役に立ちますが、完全にオプションなので、もしお気に召さなければ使用しなくても構いません。

ミューテーションは同期的でなければならない

ひとつの重要なルールを覚えておきましょう。それはミューテーションハンドラ関数は同期的でなければならないということです。なぜか?次の例で考えてみましょう:

mutations: {
  someMutation (state) {
    api.callAsyncMethod(() => {
      state.count++
    })
  }
}

いま、開発ツールのミューテーションのログを見ながら、アプリケーションのデバッグを行っていることを想像してください。全てのミューテーションをログに記録するためには、ミューテーションの前後の状態のスナップショットを捕捉することが必要です。しかし、上の例にあるミューテーション内の非同期コールバックは、それを不可能にします: そのコールバックは、ミューテーションがコミットされた時点ではまだ呼び出されていません。そして、コールバックが実際にいつ呼び出されるかを、開発ツールは知る術がありません。いかなる状態変更でも、コールバック内で起きる場合は本質的に追跡不可能です。

コンポーネント内におけるミューテーションのコミット

this.$store.commit('xxx') と書くか、もしくはコンポーネントのメソッドを store.commit にマッピングする mapMutations ヘルパーを呼び出すこと(ルートの store の注入が必要)で、コンポーネント内でミューテーションをコミットできます:

import { mapMutations } from 'vuex'

export default {
  // ...
  methods: {
    ...mapMutations([
      'increment', // `this.increment()` を `this.$store.commit('increment')` にマッピングする

      // mapMutations はペイロードサポートする:
      'incrementBy' // `this.incrementBy(amount)` を `this.$store.commit('incrementBy', amount)` にマッピングする
    ]),
    ...mapMutations({
      add: 'increment' // `this.add()` を `this.$store.commit('increment')` にマッピングする
    })
  }
}

アクションへ向けて

状態変更を非同期に組み合わせることは、プログラムの動きを予測することを非常に困難にします。例えば、状態を変更する非同期コールバックを持った 2つのメソッドを両方呼び出すとき、それらがいつ呼び出されたか、どちらが先に呼び出されたかを、どうやって知ればよいのでしょう?これがまさに、状態変更と非同期の 2つの概念を分離したいという理由です。Vuex では全てのミューテーションは同期的に行うという作法になっています:

store.commit('increment')
// "increment" ミューテーションによる状態変更は、この時点で行われるべきです

非同期的な命令を扱うためにアクションを見てみましょう。