Wednesday 15 November 2023

Understanding Watchers in Vue

 One of my favorite features for frontend development and it is fun to use but tricky and deadly too if not used properly.

Watchers are functions to trigger a callback whenever the reactive state changes for the data property. We use it when certain action needs to be performed on data change for a property.

Watch for data property

<template>
<div class="about">
<div>
<input id="input1" v-model="fullName" type="text" />
</div>
</div>

</template>

<script lang="ts">
import { reactive, computed, onMounted, ref } from 'vue'

export default {
name: 'aboutViewComponent',
components: {},
props: {},
data() {
return {
fullName: 'John Cena'
}
},
watch: {
fullName: {
handler: function (newValue, oldValue) {
alert(newValue);
}
}
},
}
</script>

In the above code, when the value changes for the data property ‘fullName’ the watch trigger will be triggered and it will show the new value in alert. it is so simple but the fun starts from here.

Watch with a complex type of data property.

Let’s replace the data property ‘fullName’ with a complex type ‘author’ that has a name property and then we need to watch the change when the author’s name changes.

<template>
<div class="about">
<div>
<input id="input" v-model="author.name" type="text" />
<button @click="onSubmit">Submit</button>
</div>
</div>
</template>

<script lang="ts">
import { reactive, computed, onMounted, ref } from 'vue'

export default {
name: 'aboutViewComponent',
components: {},
props: {},
data() {
return {
author: {
name: 'John Doe',
books: [
'Vue 2 - Advanced Guide',
'Vue 3 - Basic Guide',
'Vue 4 - The Mystery'
]
}
}
},
watch: {
author: {
handler: function (newValue, oldValue) {
alert(newValue.name);
}
}
},
methods: {
onSubmit() {
this.$data.author = {
name: 'John Cena',
books: [
'Vue 2 - Advanced Guide',
'Vue 3 - Basic Guide',
'Vue 4 - The Mystery',
]
};
console.log("Submit is fired");
},
},
}
</script>

But here, the watch handler on ‘author’ doesn’t work as expected when you change the author name for text input, though the watch implicitly creates a deep watcher and the callback should trigger for all nested mutations for ‘author’.
At the same time, the watch handler on ‘author’ triggered if you click on the submit button and the difference is, on submit we are changing the whole object of the author.

Note: The watcher on getters returns a reactive object will be triggered only when the getter returns a different object.

Deep watchers

But we need to get the watcher triggers whenever any property of the author changes, and to do this we need to explicitly force the deep watcher by using the deep option:

 watch: {
author: {
deep: true,
handler: function (newValue, oldValue) {
alert(newValue.name);
}
}
},

After this modification, any changes on the author either value change for nested property or entire object change, the watch will trigger the handler as per the code and we will see the alert popup.

Problem with Deep watch

Deep watch requires traversing all nested properties in the watched object and can be expensive when used on large data structures. Use it only when necessary and beware of the performance implications.

This is the part where most of the developers make mistakes.

Problem Statement: In a scenario, if I need to make a REST API call or emit an event to notify the change to other components only when the author name changes but should ignore it if otherwise.

Solution 1: This is where newValue and oldValue help, so we just need to put a check to compare the value before acting on this handler call. i.e.

watch: {
author: {
deep: true,
handler: function (newValue, oldValue) {
if(newValue.name !== oldValue.name){
alert(newValue.name);
}
}
}
},

Solution 2: Create a computed method to return the author name and put a watch on the computed method.

 computed: {
autherName(){
return this.author.name;
},
},
watch: {
autherName:{
handler: function (newValue, oldValue) {
alert("computed:"+ newValue);
}
}
},

I would prefer Solution 2 as this avoids traversing through all nested properties.

Eager watchers

watch is lazy by default: the callback won't be called until the watched source has changed.

In a case, where we want to make the API call to fetch some initial data and then re-fetch whenever the author name changes, we can use the immediate: true option to force the watcher’s callback immediately with author initialization.

watch: {
author: {
deep: true,
immediate: true,
handler: function (newValue, oldValue) {
if(newValue.name !== oldValue.name){
alert(newValue.name);
}
}
}
},

In this case, the handler will be executed when the page loads the first time.

Watchers on Props and computed functions

You can also put a watch on props and computed methods. Putting a watch on computed fields becomes very useful when you want to watch the change on mapGetters which directly you cannot but you can combine it with the computed method.

Here is the complete code that I use as an example above:

<template>
<div class="about">
<div>
<input id="input1" v-model="fullName" type="text" />
</div>
<div>
<input id="input2" v-model="author.name" type="text" />
<button @click="onSubmit">Submit</button>
</div>
</div>
</template>

<script lang="ts">
import { reactive, computed, onMounted, ref } from 'vue'

export default {
name: 'aboutViewComponent',
components: {},
props: {
city:{
type: String
}
},
data() {
return {
author: {
name: 'John Doe',
books: [
'Vue 2 - Advanced Guide',
'Vue 3 - Basic Guide',
'Vue 4 - The Mystery'
]
},
fullName: 'John Cena'
}
},
watch: {
author: {
deep: true,
immediate: true,
handler: function (newValue, oldValue) {
alert(newValue.name);
}
},
fullName: {
handler: function (newValue, oldValue) {
alert(newValue);
}
},
city: {
handler: function (newValue, oldValue) {
alert(newValue);
}
},
publishedBooksMessage: {
immediate: true,
handler: function (newValue, oldValue) {
alert(newValue);
}
}
},
methods: {
onSubmit() {
this.$data.author.books.push("Vue 3 - The Watcher");
this.$data.author = {
name: 'John Cena',
books: [
'Vue 2 - Advanced Guide',
'Vue 3 - Basic Guide',
'Vue 4 - The Mystery',
]
};
console.log("Submit is fired");
},
},
setup(props, { attrs }) {
console.log(`the component is now setup.`)
console.log(attrs)
},
computed: {
publishedBooksMessage() {
return this.author.books.length > 0 ? 'Yes' : 'No'
}
},
mounted() {
console.log(`the component is now mounted.`)
}

}
</script>

<style>
@media (min-width: 1024px) {
.about {
min-height: 100vh;
display: flex;
align-items: center;
}
}
</style>

Hope you enjoyed the content, follow me for more like this, and please don’t forget to LIKE it. Happy programming.

No comments:

Post a Comment