Vuex - Do not mutate vuex store state outside mutation handlers

Why do I get this error:

Error [vuex] Do not mutate vuex store state outside mutation handlers.

What does it mean?

It happens when I try to type in the edit input file.

pages/todos/index.vue

<template>
  <ul>
    <li v-for="todo in todos">
      <input type="checkbox" :checked="todo.done" v-on:change="toggle(todo)">
      <span :class="{ done: todo.done }">{{ todo.text }}</span>
      <button class="destroy" v-on:click="remove(todo)">delete</button>

      <input class="edit" type="text" v-model="todo.text" v-todo-focus="todo == editedTodo" @blur="doneEdit(todo)" @keyup.enter="doneEdit(todo)" @keyup.esc="cancelEdit(todo)">

    </li>
    <li><input placeholder="What needs to be done?" autofocus v-model="todo" v-on:keyup.enter="add"></li>
  </ul>
</template>

<script>
import { mapMutations } from 'vuex'

export default {
  data () {
    return {
      todo: '',
      editedTodo: null
    }
  },
  head () {
    return {
      title: this.$route.params.slug || 'all',
      titleTemplate: 'Nuxt TodoMVC : %s todos'
    }
  },
  fetch ({ store }) {
    store.commit('todos/add', 'Hello World')
  },
  computed: {
    todos () {
      // console.log(this)
      return this.$store.state.todos.list
    }
  },
  methods: {
    add (e) {

      var value = this.todo && this.todo.trim()
      if (value) {
        this.$store.commit('todos/add', value)
        this.todo = ''
      }

    },
    toggle (todo) {
      this.$store.commit('todos/toggle', todo)
    },
    remove (todo) {
      this.$store.commit('todos/remove', todo)
    },

    doneEdit (todo) {
      this.editedTodo = null
      todo.text = todo.text.trim()
      if (!todo.text) {
        this.$store.commit('todos/remove', todo)
      }
    },
    cancelEdit (todo) {
      this.editedTodo = null
      todo.text = this.beforeEditCache
    },
  },
  directives: {
    'todo-focus' (el, binding) {
      if (binding.value) {
        el.focus()
      }
    }
  },
}
</script>

<style>
.done {
  text-decoration: line-through;
}
</style>

stores/todos.js

export const state = () => ({
  list: []
})

export const mutations = {
  add (state, text) {
    state.list.push({
      text: text,
      done: false
    })
  },
  remove (state, todo) {
    state.list.splice(state.list.indexOf(todo), 1)
  },
  toggle (state, todo) {
    todo.done = !todo.done
  }
}

Any ideas how I can fix this?


It could be a bit tricky to use v-model on a piece of state that belongs to Vuex.

and you have used v-model on todo.text here:

<input class="edit" type="text" v-model="todo.text" v-todo-focus="todo == editedTodo" @blur="doneEdit(todo)" @keyup.enter="doneEdit(todo)" @keyup.esc="cancelEdit(todo)">

use :value to read value and v-on:input or v-on:change to execute a method that perform the mutation inside an explicit Vuex mutation handler

This issue is handled here: https://vuex.vuejs.org/en/forms.html


Hello I have get the same problem and solve it with clone my object using one of the following:

{ ...obj} //spread syntax 
Object.assign({}, obj)
JSON.parse(JSON.stringify(obj))

For your code I think you need to replace this part

computed: {
  todos () {
    // console.log(this)
    return this.$store.state.todos.list
  }
}

With this

computed: {
  todos () {
    // console.log(this)
    return {...this.$store.state.todos.list}
  }
}

I don't make sure if this is the best way but hope this helpful for other people that have the same issue.


There is no headache if you can use lodash

computed: {
  ...mapState({
    todo: (state) => _.cloneDeep(state.todo)
  })
}

Just in case someone's still being troubled by this, I got my code working by making a duplicate/clone of the store state.

In your case, try something like this...

computed: {
  todos () {
    return [ ...this.$store.state.todos.list ]
  }
}

It's basically a spread operator which results in making a clone of the todos.list array. With that, you're not directly changing the values of your state, just don't forget commit so your mutations will be saved in the store.


This error may come from the fact you shallow cloned an object.
Meaning that you've tried to copy an object but an object is not a primitive type (like String or Number), hence it's passed by reference and not value.
Here you think that you cloned one object into the other, while you are still referencing the older one. Since you're mutating the older one, you got this nice warning.

Here is a GIF from Vue3's documentation (still relevant in our case).
On the left, it's showing an object (mug) being not properly cloned >> passed by reference.
On the right, it's properly cloned >> passed by value. Mutating this one does not mutate the original


The proper way to manage this error is to use lodash, this is how to load it efficiently in Nuxt:

  • Install lodash-es, eg: yarn add lodash-es, this is an optimized tree-shakable lodash ES module
  • you may also need to transpile it in your nuxt.config.js with the following
build: {
  transpile: ['lodash-es'],
}
  • load it into your .vue components like this
<script>
import { cloneDeep } from 'lodash-es'

...
const properlyClonedObject = cloneDeep(myDeeplyNestedObject)
...
</script>

Few keys points:

  • lodash is recommended over JSON.parse(JSON.stringify(object)) because it does handle some edge-cases
  • we only load small functions from lodash and not the whole library thanks to this setup, so there is no penalty in terms of performance
  • lodash has a lot of well battle-tested useful functions, which is heavily lacking in JS (no core library)