Mutations

Try this lesson on Scrimba

The only way to actually change state in a Vuex store is by committing a mutation. Vuex mutations are very similar to events: each mutation has a string type and a handler. The handler function is where we perform actual state modifications, and it will receive the state as the first argument:

  1. const store = createStore({
  2. state: {
  3. count: 1
  4. },
  5. mutations: {
  6. increment (state) {
  7. // mutate state
  8. state.count++
  9. }
  10. }
  11. })

You cannot directly call a mutation handler. Think of it more like event registration: “When a mutation with type increment is triggered, call this handler.” To invoke a mutation handler, you need to call store.commit with its type:

  1. store.commit('increment')

Commit with Payload

You can pass an additional argument to store.commit, which is called the payload for the mutation:

  1. // ...
  2. mutations: {
  3. increment (state, n) {
  4. state.count += n
  5. }
  6. }
  1. store.commit('increment', 10)

In most cases, the payload should be an object so that it can contain multiple fields, and the recorded mutation will also be more descriptive:

  1. // ...
  2. mutations: {
  3. increment (state, payload) {
  4. state.count += payload.amount
  5. }
  6. }
  1. store.commit('increment', {
  2. amount: 10
  3. })

Object-Style Commit

An alternative way to commit a mutation is by directly using an object that has a type property:

  1. store.commit({
  2. type: 'increment',
  3. amount: 10
  4. })

When using object-style commit, the entire object will be passed as the payload to mutation handlers, so the handler remains the same:

  1. mutations: {
  2. increment (state, payload) {
  3. state.count += payload.amount
  4. }
  5. }

Using Constants for Mutation Types

It is a commonly seen pattern to use constants for mutation types in various Flux implementations. This allows the code to take advantage of tooling like linters, and putting all constants in a single file allows your collaborators to get an at-a-glance view of what mutations are possible in the entire application:

  1. // mutation-types.js
  2. export const SOME_MUTATION = 'SOME_MUTATION'
  1. // store.js
  2. import { createStore } from 'vuex'
  3. import { SOME_MUTATION } from './mutation-types'
  4. const store = createStore({
  5. state: { ... },
  6. mutations: {
  7. // we can use the ES2015 computed property name feature
  8. // to use a constant as the function name
  9. [SOME_MUTATION] (state) {
  10. // mutate state
  11. }
  12. }
  13. })

Whether to use constants is largely a preference - it can be helpful in large projects with many developers, but it’s totally optional if you don’t like them.

Mutations Must Be Synchronous

One important rule to remember is that mutation handler functions must be synchronous. Why? Consider the following example:

  1. mutations: {
  2. someMutation (state) {
  3. api.callAsyncMethod(() => {
  4. state.count++
  5. })
  6. }
  7. }

Now imagine we are debugging the app and looking at the devtool’s mutation logs. For every mutation logged, the devtool will need to capture a “before” and “after” snapshots of the state. However, the asynchronous callback inside the example mutation above makes that impossible: the callback is not called yet when the mutation is committed, and there’s no way for the devtool to know when the callback will actually be called - any state mutation performed in the callback is essentially un-trackable!

Committing Mutations in Components

You can commit mutations in components with this.$store.commit('xxx'), or use the mapMutations helper which maps component methods to store.commit calls (requires root store injection):

  1. import { mapMutations } from 'vuex'
  2. export default {
  3. // ...
  4. methods: {
  5. ...mapMutations([
  6. 'increment', // map `this.increment()` to `this.$store.commit('increment')`
  7. // `mapMutations` also supports payloads:
  8. 'incrementBy' // map `this.incrementBy(amount)` to `this.$store.commit('incrementBy', amount)`
  9. ]),
  10. ...mapMutations({
  11. add: 'increment' // map `this.add()` to `this.$store.commit('increment')`
  12. })
  13. }
  14. }

On to Actions

Asynchronicity combined with state mutation can make your program very hard to reason about. For example, when you call two methods both with async callbacks that mutate the state, how do you know when they are called and which callback was called first? This is exactly why we want to separate the two concepts. In Vuex, mutations are synchronous transactions:

  1. store.commit('increment')
  2. // any state change that the "increment" mutation may cause
  3. // should be done at this moment.

To handle asynchronous operations, let’s introduce Actions.