Vue 2 - Mutating props vue-warn
I started https://laracasts.com/series/learning-vue-step-by-step series. I stopped on the lesson Vue, Laravel, and AJAX with this error:
vue.js:2574 [Vue warn]: Avoid mutating a prop directly since the value will be overwritten whenever the parent component re-renders. Instead, use a data or computed property based on the prop's value. Prop being mutated: "list" (found in component )
I have this code in main.js
Vue.component('task', {
template: '#task-template',
props: ['list'],
created() {
this.list = JSON.parse(this.list);
}
});
new Vue({
el: '.container'
})
I know that the problem is in created() when I overwrite the list prop, but I am a newbie in Vue, so I totally don't know how to fix it. Anyone have an idea how (and please explain why) to fix it?
Solution 1:
This has to do with the fact that mutating a prop locally is considered an anti-pattern in Vue 2
What you should do now, in case you want to mutate a prop locally, is to declare a field in your data
that uses the props
value as its initial value and then mutate the copy:
Vue.component('task', {
template: '#task-template',
props: ['list'],
data: function () {
return {
mutableList: JSON.parse(this.list);
}
}
});
You can read more about this on Vue.js official guide
Note 1: Please note that you should not use the same name for your prop
and data
, i.e.:
data: function () { return { list: JSON.parse(this.list) } } // WRONG!!
Note 2: Since I feel there is some confusion regarding props
and reactivity, I suggest you to have a look on this thread
Solution 2:
The Vue pattern is props
down and events
up. It sounds simple, but is easy to forget when writing a custom component.
As of Vue 2.2.0 you can use v-model (with computed properties). I have found this combination creates a simple, clean, and consistent interface between components:
- Any
props
passed to your component remains reactive (i.e., it's not cloned nor does it require awatch
function to update a local copy when changes are detected). - Changes are automatically emitted to the parent.
- Can be used with multiple levels of components.
A computed property permits the setter and getter to be separately defined. This allows the Task
component to be rewritten as follows:
Vue.component('Task', {
template: '#task-template',
props: ['list'],
model: {
prop: 'list',
event: 'listchange'
},
computed: {
listLocal: {
get: function() {
return this.list
},
set: function(value) {
this.$emit('listchange', value)
}
}
}
})
The model property defines which prop
is associated with v-model
, and which event will be emitted on changes. You can then call this component from the parent as follows:
<Task v-model="parentList"></Task>
The listLocal
computed property provides a simple getter and setter interface within the component (think of it like being a private variable). Within #task-template
you can render listLocal
and it will remain reactive (i.e., if parentList
changes it will update the Task
component). You can also mutate listLocal
by calling the setter (e.g., this.listLocal = newList
) and it will emit the change to the parent.
What's great about this pattern is that you can pass listLocal
to a child component of Task
(using v-model
), and changes from the child component will propagate to the top level component.
For example, say we have a separate EditTask
component for doing some type of modification to the task data. By using the same v-model
and computed properties pattern we can pass listLocal
to the component (using v-model
):
<script type="text/x-template" id="task-template">
<div>
<EditTask v-model="listLocal"></EditTask>
</div>
</script>
If EditTask
emits a change it will appropriately call set()
on listLocal
and thereby propagate the event to the top level. Similarly, the EditTask
component could also call other child components (such as form elements) using v-model
.
Solution 3:
Vue just warns you: you change the prop in the component, but when parent component re-renders, "list" will be overwritten and you lose all your changes. So it is dangerous to do so.
Use computed property instead like this:
Vue.component('task', {
template: '#task-template',
props: ['list'],
computed: {
listJson: function(){
return JSON.parse(this.list);
}
}
});
Solution 4:
If you're using Lodash, you can clone the prop before returning it. This pattern is helpful if you modify that prop on both the parent and child.
Let's say we have prop list on component grid.
In Parent Component
<grid :list.sync="list"></grid>
In Child Component
props: ['list'],
methods:{
doSomethingOnClick(entry){
let modifiedList = _.clone(this.list)
modifiedList = _.uniq(modifiedList) // Removes duplicates
this.$emit('update:list', modifiedList)
}
}
Solution 5:
Props down, events up. That's Vue's Pattern. The point is that if you try to mutate props passing from a parent. It won't work and it just gets overwritten repeatedly by the parent component. Child component can only emit an event to notify parent component to do sth. If you don't like these restrict, you can use VUEX(actually this pattern will suck in complex components structure, you should use VUEX!)