1

I'm still learning Vue and I can't understand why this error occurs.

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

Here is my code:

const DATE_FORMAT = "DD-MM-YYYY";

Vue.filter("formatDate", function(value, dateFormat) {
  dateFormat = dateFormat ? dateFormat : DATE_FORMAT;
  if (value) {
    return moment(String(value)).format(dateFormat);
  }
});

Vue.component('list-component', {
  props: ['projects'],
  template: 
`
<div>
  <ul v-for="project in projects" :key="project.id">
    <li v-for="(value, key) in project" :key="key">
      {{ key }}: {{ value }}
    </li>
    <hr />
  </ul>
</div>
`
});

const store = new Vuex.Store({
  state: {
    projects: [{
      id: 1,
      name: "super project",
      startDate: "2014-09-01T08:18:28+02:00"
    }, {
      id: 2,
      name: "extra project",
      startDate: "2017-12-20T07:28:23.133+01:00"
    }, {
      id: 3,
      name: "small project",
      startDate: "2017-12-20T07:28:23.133+01:00"
    }]
  },
  getters: {
    allProjects: state => state.projects
  },
  strict: true
});

new Vue({
  el: '#app',
  computed: {
    formattedProjects() {
      var project = store.getters.allProjects;
      project.forEach(project => {
        project.startDate = this.$options.filters.formatDate(project.startDate);
      });
      return project;
    }
  }
});
<script src="https://cdnjs.cloudflare.com/ajax/libs/moment.js/2.20.1/moment.js"></script>
<script src="https://unpkg.com/vuex@3.0.1/dist/vuex.js"></script>
<script src="https://unpkg.com/vue@2.5.13/dist/vue.js"></script>
<div id="app">
  <list-component :projects="formattedProjects"></list-component>
</div>

https://jsfiddle.net/9sn78n1h/2/

I can't use my filter in the component, because the property name can be different. In the example above it's startDate, but in other data it could be addDate, endDate etc. In the vuex store I need the startDate in the format as it is, I don't want to mutate it. I'm not sure that writing another getter in the vuex like allProjectsWithFormatedDate is a good practice, but even so, it still yells about the mutating store.

So I create a local variable project in my computed method formattedProjects() and I don't know why I've got this error after changing it's value.

Can somebody help me to understand it and show the correct way to solve this problem?

I know I can set the strict mode to false to mute this error (and I should in production), but I belive there is a better way to solve this.

kojot
  • 1,634
  • 2
  • 16
  • 32

2 Answers2

2

The issue is what the error message says. Vuex has unidirectional flow, which you must be aware about. Any changes to the state must be done only in the mutations section. So by mistake we are mutating the object on the line below.

project.startDate = this.$options.filters.formatDate(project.startDate);

this will add/update(mutate) the startDate property on a store item project. This is the root cause of the problem. I think Vuex must have a better way to handle this. If you want to handle it in your computed property itself, it can be done like this.

formattedProjects() {
  var clonedProject = []
  var project = store.getters.allProjects;
  project.forEach(project => {
    clonedProject.push({
      ...project,
      startData: this.$options.filters.formatDate(project.startDate)
    })
  });
  return clonedProject;
}

Here is a link to the fiddle

aks
  • 8,796
  • 11
  • 50
  • 78
  • Clone an array is the answer. `Push` seems to performance pretty good, other options and benchmarks can be found [here](https://stackoverflow.com/questions/3978492/javascript-fastest-way-to-duplicate-an-array-slice-vs-for-loop) – kojot Dec 28 '17 at 07:28
  • I will mostly not look for performance for the common use case and I would give priority to a more readable code. But that can be just me. We can always come back optimize the code at the right time. – aks Dec 28 '17 at 08:09
  • Using `.slice()` may be simpler. – Kalnode Jan 10 '21 at 20:43
1

Quite simply because you're modifying the state directly without using dispatch or mutations.

See this page for more information.

In your example above, you could add something like:

mutations: {
  updateProject (state, obj) {
    // Use state, get the project then set startDate.
    // You can use obj.projectId and obj.startDate
  }
}

Then to make the change in your loop:

this.$store.commit('updateProject', {
  projectId: project.id, 
  startDate: this.$options.filters.formatDate(project.startDate)
})

If you add that do your Vue instance, it'll allow you to get update the state without the warning.

Depending on the size of your state and how complex your app is, you might consider using modules instead.

In line with your comment, if you're not trying to update the store, just return the data in a new object:

formattedProjects() {
  var project = store.getters.allProjects;
  var clone = []
  project.forEach(project => {
    clone.push({
      ...project,
      startData: this.$options.filters.formatDate(project.startDate)
    })
  });
  return clone;
}

This will return a new object and not mutate the store value. There may be a shorter way to return the data but using .map caused problems when I tested it but you should be able to get the idea...

Note: Another thought would be to loop through the state on your component and just do the filtering within the component HTML - Not sure why you need to do it with a computed prop.

You're updated example:

const DATE_FORMAT = "DD-MM-YYYY";

Vue.filter("formatDate", function(value, dateFormat) {
  dateFormat = dateFormat ? dateFormat : DATE_FORMAT;
  if (value) {
    return moment(String(value)).format(dateFormat);
  }
});

Vue.component('list-component', {
  props: ['projects'],
  template: 
`
<div>
  <ul v-for="project in projects" :key="project.id">
    <li v-for="(value, key) in project" :key="key">
      {{ key }}: {{ value }}
    </li>
    <hr />
  </ul>
</div>
`
});

const store = new Vuex.Store({
  state: {
    projects: [{
      id: 1,
      name: "super project",
      startDate: "2014-09-01T08:18:28+02:00"
    }, {
      id: 2,
      name: "extra project",
      startDate: "2017-12-20T07:28:23.133+01:00"
    }, {
      id: 3,
      name: "small project",
      startDate: "2017-12-20T07:28:23.133+01:00"
    }]
  },
  getters: {
    allProjects: state => state.projects
  },
  strict: true
});

new Vue({
  el: '#app',
  computed: {
    formattedProjects() {
      var project = store.getters.allProjects;
      var clone = []
      project.forEach(project => {
        clone.push({
          ...project,
          startData: this.$options.filters.formatDate(project.startDate)
        })
      });
      return clone;
    }
  }
});
<script src="https://cdnjs.cloudflare.com/ajax/libs/moment.js/2.20.1/moment.js"></script>
<script src="https://unpkg.com/vuex@3.0.1/dist/vuex.js"></script>
<script src="https://unpkg.com/vue@2.5.13/dist/vue.js"></script>
<div id="app">
  <list-component :projects="formattedProjects"></list-component>
</div>
webnoob
  • 15,747
  • 13
  • 83
  • 165
  • Thank you for the answer, but it doesn't solve my problem. This way I'm mutating my store, that is, I'm changing the value of the `startDate`, but I need the original value for communication with the backend and for the others components. I need only to format the date in my component, like with the filters. See my updated jsfiddle https://jsfiddle.net/9sn78n1h/4/ When you comment the first tag you're going to see, that it display the different value (because the computed method `formattedProjects` is not called). – kojot Dec 27 '17 at 13:16
  • Yes it does. I marked @aks answer as correct, because it seems he posted it a little faster :) Thanks again. – kojot Dec 28 '17 at 07:31