I got a codesandbox that reproduces my problem: codesandbox example watcher not triggering.
I am working on a component that relies on an object with data that can be dynamically added to the object, so for example, in a seperate .js file, I am exporting the following object:
export default {
defaultSection1: {
displayName: 'Action',
},
defaultSection2: {
displayName: 'Thriller',
},
}
I import this object in my component.
I got a debouncer setup from Lodash so that when data changes, it only fires the watcher once after two seconds. The watcher IS triggered perfectly fine for the data that is already in the object (type in the input text box in my example, and the watcher is triggered). But when I dynamically add data to the object, the watcher is not triggered at all. Only when I change routes back and forth, the data is updated, but the watcher is not triggered. Why is this? What can I do so that the watcher is triggered when data is dynamically being added to an object.
methods: {
fetchAndUpdateData(){
console.log('Fetching data...')
},
addCustomSection(){
//The watcher should be triggered when this function is called
const newSectionId = Math.floor(Math.random() * 1000);
this.data[newSectionId] = {
displayName: 'Custom'
}
}
},
computed: {
dataWatcher() {
return this.data;
},
updateData() {
return debounce(this.fetchAndUpdateData, 2000);
},
},
watch: {
dataWatcher: {
handler() {
console.log('Watcher triggered')
this.updateData();
},
deep: true,
},
},
Why is the watcher not triggered, when clearly the data is being changed?
Another thing I noticed that is quite strange, is that in Vue 3.0, the watcher IS triggered with exactly the same code.
Codesandbox in Vue 2.6.11, watcher not triggering.
Codesandbox in Vue 3.0, watcher IS triggered with exactly the same code.
In vue 2 there's reactivity issue when updating an item in an array or a nested field in an object, to solve this you've to use this.$set() method :
this.$set(this.data,newSectionId, {displayName: 'Custom'})
this issue is solved in Vue 3. and you could just do :
const newSectionId = Math.floor(Math.random() * 1000);
this.data[newSectionId] = {
displayName: 'Custom'
}
Related
I am trying to move some functionality to a vue mixin from the component, to be able to use it in multiple components.
This (simplified version of the code) works:
export default {
data() {
return {
file: {},
audioPlayer: {
sourceFile: null,
},
};
},
watch: {
'audioPlayer.SourceFile': function (nextFile) {
console.log('new sourceFile');
this.$data.file = nextFile;
},
}
}
But if I move the audioPlayer data object to a mixin, the watch does no longer fire.
Is this expected behavior?
N.b. I resolved this by directly making the 'file' data property into a computed value, which works in this particular case, but the behavior is still strange.
You need a lowercase s. sourceFile not SourceFile
watch: {
'audioPlayer.sourceFile': function (nextFile) {
console.log('new sourceFile');
this.$data.file = nextFile;
},
}
I do not understand why my new code does not work. I was able to extract a minimum reproducible case. When the created() sets a data synchronously, it works well and an article radio is displayed. When I surround it with timeout, then the blog stays selected. Vue 2.6.12
The bug in this code has been fixed, but this was not the cause for my troubles because my real code is different. My problem is that the radio button is not checked when it should be after the reactive data is changed.
<Radio
v-model="type"
identifier="article"
class="pl-3"
label="article"
name="type"
/>
<Radio
v-model="type"
identifier="blog"
class="pl-3"
label="blog"
name="type"
/>
<div>Selected {{ type }}</div>
data() {
return {
type: "blog",
};
},
created() {
setTimeout(function () {
this.type = "article";
console.log(this.type);
}, 800);
},
This makes my head explode because a similar code in different component works well.
UPDATE:
my original code, that does not work, is
computed: {
blog() {
return this.$store.getters.BLOG;
},
},
watch: {
blog() {
this.type = (this.blog.info.editorial) ? 'article' : 'blog';
},
created() {
this.$store.dispatch('FETCH_BLOG', { slug: this.slug });
},
Relevant source code:
https://github.com/literakl/mezinamiridici/blob/234_editorial_team/spa/src/views/item/WriteBlog.vue
https://github.com/literakl/mezinamiridici/blob/234_editorial_team/spa/src/components/atoms/Radio.vue
https://github.com/literakl/mezinamiridici/blob/234_editorial_team/spa/src/modules/vuex/items.js
All you need is to change your function to an arrow function because it isn't point your data like this
setTimeout(() => {
this.type = "article";
console.log(this.type);
}, 800);
The problem is the selected property in Radio.vue is only set equal to value in the created() hook. When the setTimeout() occurs in the parent component, Radio.vue's v-model property is changed, which updates its value property, but its selected property is not automatically updated to match.
The solution is to replace the created() hook change with a watcher on value that updates selected:
// Radio.vue
export default {
created() {
// ⛔️ Remove this
//if (this.value) {
// this.selected = this.value
//}
},
watch: {
value: {
handler(value) {
this.selected = value
},
immediate: true,
},
},
}
demo
I assume your original code does not set the type in vue's data function, so it will not reactive when you assign this.type to a new value.
Manage state in a form is complicated, check out this library: https://github.com/vue-formily/formily and maybe it helps you easier to work with form, it will let you separate the form definition from vue component that makes it reusable, and it will manage the state for you...
Here is a small demo for your problem: https://codepen.io/hqnan/pen/YzQbxxo
I am using the mapGetters helper from VueX but i have some problem only on the first load of the page, it's not reactive...
Let me show you :
my html template triggering the change :
<input type="number" value="this.inputValue" #change="this.$store.dispatch('setInputValue', $event.target.value)">
my store receiving the value
{
state: {
appValues: {
inputValue: null
},
},
getters: {
getInputValue: (state) => {
return state.appValues.inputValue;
},
},
mutations: {
setInputValue(state, value) {
state.appValues.inputValue = value;
},
},
actions: {
setInputValue(context, payload) {
return new Promise((resolve, reject) => {
context.commit('setInputValue', payload);
resolve();
});
},
}
}
and then my component listening the store :
import {mapGetters} from 'vuex';
computed: {
...mapGetters({
inputValue: 'getInputValue',
}),
}
watch: {
inputValue: {
deep: true,
immediate: true,
handler(nVal, oVal) {
console.log("inputValue", nVal, oVal);
}
},
}
So now, when i first load the page I get this console.log "inputValue" null undefined which is totally normal because as I have nothing in my store it gaves me the default value null.
But now it's the weird part. I start changing the input value and I don't have nothing appearing in my console. Nothing is moving...
Then I reload the page and on the load I get this console.log "inputValue" 5 undefined (5 is the value I entered previously) so as you can see, when I was changing the input previously, it was well keeping the value in the store but the computed value was not updating itself...
Ans now, when I change the value of the input I have my console log like this "inputValue" 7 5 so it's working as I would like it to work from the start...
What do I do wrong? Why on the first load the computed value not reactive?
Thanks for your answers...
I think the best way to solve this issue is to store a local variable with a watcher, and then update vuex when the local is changed:
On your component:
<input type="number" v-model="value">
data() {
return {
value: ''
};
},
computed: {
...mapGetters({
inputValue: 'getInputValue'
})
}
watch: {
value(value){
this.$store.dispatch('setInputValue', value);
},
inputValue(value) {
console.log('inputValue', value);
}
},
created() {
// set the initial value to be the same as the one in vuex
this.value = this.inputValue;
}
Please take a look at this sample: https://codesandbox.io/s/vuex-store-ne3ol
Your mistake is, you are using this keyword in template. One shouldn't use this in template code.
<input
type="number"
value="inputValue"
#change="$store.dispatch('setInputValue', $event.target.value)"
>
Bonus tip: It is redundant to use a getter to return the default state
if you can just use mapState to return the state.
There are a few small mistakes in the template. It should be this:
<input type="number" :value="inputValue" #change="$store.dispatch('setInputValue', $event.target.value)">
I've removed the this. in a couple of places and put a : out the front of value. Once I make these changes everything works as expected. The this.$store was causing console errors for me using Vue 2.6.10.
I would add that you're using the change event. This is the standard DOM change event and it won't fire until the field blurs. So if you just start typing you won't see anything happen in the console. You'd be better off using input if you want it to update on every keystroke. Alternatively you could use v-model with a getter and setter (see https://vuex.vuejs.org/guide/forms.html#two-way-computed-property).
My suspicion is that when you were reloading the page that was triggering the change event because it blurred the field.
Ok, so ... I found the problem and it was not relative to my examples, I can't really explain why, but I'll try to explain how :
In my store I have the next method :
mutations: {
deleteAppValues(state) {
state.appValues = null;
}
}
I was using this one on the Logout, or when the user first comes on the pageand was not logged-in... So what was going-on?
The User first load the page, the store is initializing well, and the index inputValue is initialized with null value, so it exists...
... But as the User is not logged, I destroy the store so now the inputValue is not equals to null, it just doesn't exist...
Trying to use mapGetters on something that don't exists, the reactivity won't work, so if I dispatch a change, the store key will be created, but as the mapGetters was initialized with an inexisting key, it doesn't listen the reactivity...
After reloading the page, the key now exists in the store so the getter can be attached to it and so now everything working fine...
This is exactly the explaination of what was going wrong about my code... So to make it works fine, I just changed my destruction mutation to :
mutations: {
deleteAppValues(state) {
state.appValues = {
inputValue: null,
};
}
}
Like this, the inputValue key of the store object will always exists and so the getter won't lose his reactivity...
I tryed to make a simple concise question but that made me forgot the bad part of my code, sorry.
I am building a Vue 2 Webpack application that uses Vuex. I am trying to update a component's local state by watching a computed property which is getting data from the Vuex store. This is what the inside of the <script></script> section of my component looks like:
export default {
name: 'MyComponent',
data() {
return {
// UI
modal: {
classes: {
'modal__show-modal': false,
},
tags: [],
},
};
},
computed: {
tagList() {
return this.$store.getters.tagList;
},
},
watch: {
tagList: (updatedList) => {
this.modal.tags = updatedList;
},
},
};
As you can see, I have a computed property called tagList which fetches data from the store. I have a watcher that watches tagList so that whenever the store's data changes, I can update modal.tags to the new value.
As per Vue documentation, I can call this.propertyName and update my local component state but when I call this.modal.tags = updatedList;, I get the following error:
[Vue warn]: Error in callback for watcher "tagList": "TypeError: Cannot set property 'tags' of undefined"
Why does this error occur even though it looks no different than what is in Vue.js's documentation?
Don't use arrow functions.
Change from:
watch: {
tagList: (updatedList) => {
this.modal.tags = updatedList;
},
},
To:
watch: {
tagList(updatedList) { // changed this line
this.modal.tags = updatedList;
},
},
Vue docs mention this a few times:
Don't use arrow
functions
on an options property or callback, such as created: () => console.log(this.a) or vm.$watch('a', newValue => this.myMethod()).
Since arrow functions are bound to the parent context, this will not
be the Vue instance as you'd expect, often resulting in errors such as
Uncaught TypeError: Cannot read property of undefined
or
Uncaught TypeError: this.myMethod is not a function
It is basically a context/scope issue. When using arrow functions, the this does not refer to the Vue instance, but the enclosing context of where the component was declared (probably window).
That is because of the scope issue. You are calling this. from another context. So, within arrow functions, you don't have access to vuejs data.
I suggest you change the watch to:
tagList (updatedList) {
this.modal.tags = updatedList;
},
The props:
props: {
delay: Number,
}
The watcher:
watch: {
q: _.debounce(function() {
console.log(this.delay); // 500; always works fine, this.delay is here
}, this.delay) // never works;
},
If hardcode a delay (set 500 instead of this.delay - it works; otherwise - function not debounce).
What am I doing wrong? Thanks.
You won't be able to accomplish setting the delay there. this is not the component in that scope. You can use $watch instead inside a lifecycle hook:
created () {
this.debounceUnwatch = this.$watch('q', _.debounce(
this.someMethod,
this.delay
))
},
destroyed () {
// Removed the watcher.
this.debounceUnwatch()
},
For more information:
https://v2.vuejs.org/v2/api/#vm-watch
Edit
This doesn't work either. It just really seemed like it should have. What I think needs to be done here is you need to debounce whatever is updating q and not q itself.