Reputation: 2303
In my root Vue instance, I have an array of objects with some data, which I use to render a set of components. These components have a watcher on the object of data provided to them, which is supposed to make an asynchronous call every time the object is updated.
The problem is that when I update a property of one of the objects in my array, the watcher is not called. It shouldn't fall into any of Vue's caveats because a) I'm not adding a new property, just updating an existing one and b) I'm not mutating the array itself in any way. So why is this happening? And how do I fix it?
My main Vue instance:
let content = new Vue({
el: '#content',
data: {
testData: [
{ name: 'test1', params: {
testParam: 1
} },
{ name: 'test2', params: {
testParam: 1
} },
{ name: 'test3', params: {
testParam: 1
} }
]
}
});
The code which I use to render my components:
<div id="content">
<div v-for="item in testData">
<test-component v-bind="item"></test-component>
</div>
</div>
And my component:
Vue.component('test-component', {
props: {
name: {
type: String,
required: true
},
params: {
type: Object,
required: true
}
},
data: function() {
return { asyncResult: 0 };
},
watch: {
params: function(newParams, oldParams) {
// I use a custom function to compare objects, but that's not the issue since it isn't even being called.
console.log(newParams);
if(!this.compareObjs(newParams, oldParams)) {
// My async call, which mutates asyncResult
}
}
},
template: `
<span>{{ asyncResult }}</span>
`
});
My goal is to mutate the properties of the params
property of a given object and trigger the watcher to rerender the corresponding component, but when I try to mutate it directly it doesn't work.
Example (and the way I'd like my component to work):
content.testData[2].params.testParam = 5;
Unfortunately, it doesn't. Using Vue.set
doesn't work either:
Vue.set(content.testData[2].params, 'testParam', 5);
The only thing I found which does work is to assign a new object entirely (which is not something I'd like to do every time I have to mutate a property):
content.testData[2].params = Object.assign({}, content.testData[2].params, { testParam: 5 });
I also tried using a deep watcher, as suggested in a similar question, but it didn't work in my case. When I use the deep watcher the function is called, but both newParams
and oldParams
are always the same object, no matter which value I set to my property.
Is there a solution to this that will allow me to mutate the array items just by setting a property? That would be the most desirable outcome.
Upvotes: 0
Views: 3460
Reputation: 29092
First things first.
Using Vue.set
isn't going to help. Vue.set
is used to set the values of properties that Vue's reactivity system can't track. That includes updating arrays by index or adding new properties to an object but neither of those apply here. You're updating an existing property of a reactive object, so using Vue.set
won't do anything more than setting it using =
.
Next...
Vue does not take copies of your objects when passing them as props. If you pass an object as a prop then the child component will get a reference to the same object as the parent. A deep
watcher will trigger if you update a property within that object but it's still the same object. The old and new values passed to the watcher will be the same object. This is noted in the documentation:
https://v2.vuejs.org/v2/api/#vm-watch
Note: when mutating (rather than replacing) an Object or an Array, the old value will be the same as new value because they reference the same Object/Array. Vue doesn’t keep a copy of the pre-mutate value.
As you've noticed, one solution is to use a totally new object when performing the update. Ultimately, if you want to compare the old and new objects then you have no choice but to make a copy of the object somewhere. Taking a copy when mutating is a perfectly valid choice, but it's not the only option.
Another option would be to use a computed property to create the copy:
new Vue({
el: '#app',
data () {
return {
params: {
name: 'Lisa',
id: 5,
age: 27
}
}
},
computed: {
watchableParams () {
return {...this.params}
}
},
watch: {
watchableParams (newParams, oldParams) {
console.log(newParams, oldParams)
}
}
})
<script src="https://unpkg.com/[email protected]/dist/vue.js"></script>
<div id="app">
<input v-model="params.name">
<input v-model="params.id">
<input v-model="params.age">
</div>
A few notes on this:
JSON.stringify
/JSON.parse
might be an option.watch
doesn't need to be deep
. The computed property will create dependencies on the properties it uses and if any of them changes it will be recomputed, creating a new object each time. We just need to watch that object.The key advantage of this approach is where the copying is handled. The code doing the mutating doesn't need to worry about it, the copying is performed by the same component that needs the copy.
Upvotes: 2
Reputation: 919
As you said, you will need to use deep
property in watch
.
Using Vue.set
you should remounting the entire object inside your array, like:
const newObj = {
name: 'test1',
params: {
testParam: 1,
},
};
Vue.set(yourArray, newObj, yourIndex);
Note you are setting some value inside your array and in this case the array contains objects.
Upvotes: 0