vue watch handler called without actual changes - vue.js

In this codepen have a counter that can be incremented with a "incr" link.
I now have a computed property and a watch:
computed: {
test() {
let unused = this.counter;
return [42];
}
},
watch: {
test(val, old) {
// Should I avoid firing when nothing actually changed
// by implementiong my own poor-man's change detection?
//
// if (JSON.stringify(newVal) == JSON.stringify(oldVal))
// return;
console.log(
'test changed',
val,
old
);
}
}
A contrived example perhaps, but in reality this is a calculation where the real data is reduced (in a vuex getter) and most-often, the reduced data doesn't change even when some of the data changes.
Edited to add more detail: The data in the vuex store is normalized. We're also using vue-grid-layout that expects its layoutproperty in a certain non-normalized format. So we have a gridLayout getter that does the vuex -> vue-grid-layout tranform. Watching this gridLayout getter fires even when the resulting gridLayout doesn't actually change, but other details do, such as names and other irrelevant-to-vue-grid-layout object keys in the vuex store.
Now in the above example, when this.counter changes, the watch on test fires too, even though the newVal and oldVal are "the same". They aren't == or === mind you, but "the same" as in JSON.stringify(newVal) == JSON.stringify(oldVal).
Is there any way to have my watch fire only when there are actual changes? Actually comparing JSON.stringify() seems inefficient to me, but I'm worried about performance problems as my project grows as my watch could do expensive operations and I want to ensure I'm not missing something.

According to the Vue.js documentation computed properties are reevaluated when ever a reactive dependency is changed.
In your case the reactive dependency is this.counter
You can achieve the same result by invoking a method as opposed to a computed property.
Just change up your component architecture:
data () {
return: {
counter: 0,
obj: {},
output: null
}
},
watch: {
counter(val, old) {
// Alternatively you could remove your method and do something here if it is small
this.test(val);
},
// Deep watcher
obj: {
handler: function (val, oldVal) {
console.log(val);
console.log(oldVal);
this.test(val);
},
deep: true
},
}
},
methods: {
test() {
let unused = this.counter;
if (something changed)
this.output = [42];
}
}
}
Now in your template (or other computed properties) output is reactive
Read more: https://v2.vuejs.org/v2/guide/computed.html#Computed-Caching-vs-Methods

Related

Vue watch triggered when there is no (discernible) change to object

I have an object that I am watching in vue for the purpose of performing an action whenever a change is detected in it. Something keeps triggering it, but when I print the object to the console and compare the oldVal to newVal they seem identical.
Just looking at the objects logged to the console revealed no differences to my eye, so I thought that by stringifying them and comparing them in a text compare tool I would find differences, but there too the results were identical for code like this:
watch: {
CCompPrefs: function (newVal, oldVal) {
console.log('CC changed: ', JSON.stringify(newVal), ' | was: ', JSON.stringify(oldVal))
}
},
While not understanding why the watch was being triggered if nothing in the object had changed, I thought it was safe to do something like this:
watch: {
CCompPrefs: function (newVal, oldVal) {
if (newVal !== oldVal) {
console.log('CC CHANGED, OLD VAL DIFFERENT')
}
}
},
But the log ran, despite there being no discernible difference I could find!
So I found a working solution by doing this:
watch: {
CCompPrefs: function (newVal, oldVal) {
if (JSON.stringify(newVal) !== JSON.stringify(oldVal)) {
console.log('CC CHANGED, OLD VAL DIFFERENT')
}
}
},
But this still leaves me the nagging question of WHY this is being triggered in the first place. What could possibly be changing and why?
Supplementary info
CCompPrefs is coming via a computed element in the following way:
computed: {
CCompPrefs () {
return this.$store.state[this.$attrs.useCase].filter(x => (x.show === true && x.enabled === true))
},
}
Almost any action will seemingly trigger this watch. Like throwing up a model window.
Using Vue devtools, I can verify that there are NO mutations being applied to ANY part of the vuex store
UPDATE
Now I am wondering if this.$attrs.useCase in my computed value above might be the culprit. The modal I am opening is in a parent container, perhaps that switches the context for that value and forces an update? Looking into it now...
UPDATE2
Nope. this.$attrs.useCase does NOT change. So still confused, WHAT could be triggering this watcher?
I avoided redundant calls for unchanged data by crudely checking the object matches in my handler like this:
data: () => ({
lastDataString: '',
}),
itinerary: {
handler: function(v) {
// Avoid redundant calls
let dataString = JSON.stringify(v)
if (dataString === this.lastDataString){
return
}
this.lastDataString = dataString
// do stuff
},
deep: true,
},

How do I watch changes to an object? this.$set seems to capture only property update

I have this object of arrays that I'm tryin to watch every update of.
myData = {
"299":[527],
"376":[630,629]
}
I read this documentation on watching an object which instructed to use either this.$set(object, propertyName, value) or Object.assign({}, this.object, dataToBeAppended) to watch an object. I used this.$set.
export default {
...
data() {
return {
myData: {},
};
},
watch: {
myData(newVal) {
console.log(`🔴localStorage`);
},
},
methods: {
onFoldChange(propertyName) {
const newArr = [...]
this.$set(this.myData, propertyName, newArr);
},
}
}
Unlike what I expected, vue captures changes on property only. Changes in value to an existing property are not being watched. For example, if a property "299" was newly added, it will print 🔴localStorage. When the value of a property "299" is updated from [527] to something else, nothing is fired. When I print myData, I see every value updated correctly. It is just that watch isn't capturing the changes.
The documentation also described we can watch an array using this.$set(this.myData, indexOfItem, newValue) so I also tried array version of the above code, like this.
this.$set(this.myData[propertyName], index, newValueToAdd);
This time it doesn't listen at all. Not even the first entry.
Is there any better way to solve this issue? How do others watch an object? Is the complication coming from the type of values (array) ?
Currently, myData watcher observes only an object. Object contains pointers to arrays as in JS Objects & Arrays are passed by reference not by copy. That's why it can detect only changes in keys and with simple values. If you want to observe it deeper - I mean also those subarrays (or subobjects) - just use deep watch.
watch: {
myData: {
deep: true,
handler (newVal) {
console.log(`🔴localStorage`);
}
}
}
Another possible solution could be to use some Array.prototype operation to modify an array if it already exists. E.g:
methods: {
onFoldChange(propertyName) {
if (propertyName in this.myData && Array.isArray(this.myData[propertyName])) {
this.myData[properyName].push(162) // Some random value
} else {
const newArr = [...]
this.$set(this.myData, propertyName, newArr);
}
},
}

Can't get data of computed state from store - Vue

I'm learning Vue and have been struggling to get the data from a computed property. I am retrieving comments from the store and them processing through a function called chunkify() however I'm getting the following error.
Despite the comments being computed correctly.
What am I doing wrong here? Any help would be greatly appreciated.
Home.vue
export default {
name: 'Home',
computed: {
comments() {
return this.$store.state.comments
},
},
methods: {
init() {
const comments = this.chunkify(this.comments, 3);
comments[0] = this.chunkify(comments[0], 3);
comments[1] = this.chunkify(comments[1], 3);
comments[2] = this.chunkify(comments[2], 3);
console.log(comments)
},
chunkify(a, n) {
if (n < 2)
return [a];
const len = a.length;
const out = [];
let i = 0;
let size;
if (len % n === 0) {
size = Math.floor(len / n);
while (i < len) {
out.push(a.slice(i, i += size));
}
} else {
while (i < len) {
size = Math.ceil((len - i) / n--);
out.push(a.slice(i, i += size));
}
}
return out;
},
},
mounted() {
this.init()
}
}
Like I wrote in the comments, the OPs problem is that he's accessing a store property that is not available (probably waiting on an AJAX request to come in) when the component is mounted.
Instead of eagerly assuming the data is present when the component is mounted, I suggested that the store property be watched and this.init() called when the propery is loaded.
However, I think this may not be the right approach, since the watch method will be called every time the property changes, which is not semantic for the case of doing prep work on data. I can suggest two solutions that I think are more elegant.
1. Trigger an event when the data is loaded
It's easy to set up a global messaging bus in Vue (see, for example, this post).
Assuming that the property is being loaded in a Vuex action,the flow would be similar to:
{
...
actions: {
async comments() {
try {
await loadComments()
EventBus.trigger("comments:load:success")
} catch (e) {
EventBus.trigger("comments:load:error", e)
}
}
}
...
}
You can gripe a bit about reactivity and events going agains the reactive philosophy. But this may be an example of a case where events are just more semantic.
2. The reactive approach
I try to keep computation outside of my views. Instead of defining chunkify inside your component, you can instead tie that in to your store.
So, say that I have a JavaScrip module called store that exports the Vuex store. I would define chunkify as a named function in that module
function chunkify (a, n) {
...
}
(This can be defined at the bottom of the JS module, for readability, thanks to function hoisting.)
Then, in your store definition,
const store = new Vuex.Store({
state: { ... },
...
getters: {
chunkedComments (state) {
return function (chunks) {
if (state.comments)
return chunkify(state.comments, chunks);
return state.comments
}
}
}
...
})
In your component, the computed prop would now be
computed: {
comments() {
return this.$store.getters.chunkedComments(3);
},
}
Then the update cascase will flow from the getter, which will update when comments are retrieved, which will update the component's computed prop, which will update the ui.
Use getters, merge chuckify and init function inside the getter.And for computed comment function will return this.$store.getters.YOURFUNC (merge of chuckify and init function). do not add anything inside mounted.

Access component instance from vue watcher

I'm working on a project, similar as a bill manager, so I want that the subtotal get recalculated every time that quantity or unit value change, I have tried and searched to accomplish this using watcher or computed properties, but I don't find the right approach, cause I need to access the whole scope of the element when another change, like this.
Model structure:
detail
quantity
unit value
subtotal (should be a computed or updated)
So I think I should be able of doing something like this:
Vue.component('item', {
template: '#item',
props: {
item: Object,
},
computed:{
total: function(){
return this.quantity*this.unit_value;
}
},
watch:{
'item.quantity':()=>{
this.subtotal = this.quantity*this.unit_value;
}
}
});
I have several components being read from a list
I merged the approach using watcher and computed in the same code to make it shorter.
The problem is that I haven't found a way to access the hole element from inside itself, anyone could pls explain the right way? thanks
You shouldn't use arrows functions there, use method declarations.
If you want to watch for a property of the item object, you'll have to watch for the item object itself, and additionally use the deep: true flag of the watcher.
Final detail, you are using several properties that are not declared in your data. Declare them, otherwise they will not be reactive, that is, the computed will not recalculate when they change.
See code:
Vue.component('item', {
template: '#item',
props: {
item: Object,
},
data() {
return {
subtotal: null, // added data properties
quantity: null,
unit_value: null
}
},
computed: {
total: function() {
return this.quantity * this.unit_value;
}
},
watch: {
item: { // watching for item now
deep: true, // using deep: true
handler() { // and NOT using arrow functions
this.subtotal = this.quantity * this.unit_value;
}
}
}
});

Vuejs deep nested computed properties

I'm not really understanding where to put function() { return {} } and where not to when it comes to deeply nesting computed properties.
By the way, this is in a component!
computed: {
styles: function() {
return {
slider: function() {
return {
height: {
cache: false,
get: function() {
return 'auto';
}
},
width: {
cache: false,
get: function() {
return $('#slideshow').width();
}
}
}
}
}
}
},
This is returning undefined. When I get rid of the function() { return {} } inside of the slider index, it returns an object when I do styles.slider.width instead of the get() return. It just shows an object with cache and get as indexes..
Thanks for any help!
The reason I'm asking is because I have multiple nested components that involve styling from the parent. Slider, tabs, carousels, etc. So I wanted to organize them like this.
I believe you mean to return a computed object, but not actually structure the computation in a nested manner?
What the others have said regarding the 'computed' hook not having syntax for nesting is correct, you will likely need to structure it differently.
This may work for you: I generate many objects in a similar fashion.
computed: {
computedStyles(){
var style = {slider:{}}
style.slider.height = 'auto'
style.slider.width = this.computedSlideshowWidth
return style
},
computedSlideshowWidth(){
return $('#slideshow').width()
}
As per 2020 and Vue 2.6.12 this is completelly possible. I believe this has been possible since v.2 but cannot confirm.
Here is the working example:
this.computed = {
// One level deep nested,
// get these at `iscomplete.experience`
// or `iscomplete.volume`
iscomplete: function() {
return {
experience: this.$data.experience !== null,
volume: this.$data.volume > 100,
// etc like this
};
},
// More levels deep nested.
// Get these at `istemp.value.v1 and `istemp.value.v2`
istemp: function() {
return {
value1: {
v1: this.$data.experience,
v2: 'constant'
}
}
}
};
As a result you will be able to access your deep nested computed in your template as e.g. follows <span v-text="iscomplete.experience"></span> that will output <span>true</span> for the first example computed above.
Note that:
Since Vue v.2 cache key is deprecated;
Vue would not execute functions assigned to a computed object nested keys;
You cannot have computed for non-Vue-reactive things which in your case is e.g. $('#slideshow').width(). This means they are not going to be re-computed on their content change in this case (which is a computed's sole purpose). Hence these should be taken away from computed key.
Other than that I find nested computeds to be quite helpful sometimes to keep things in better order.