In this article, we are going to cover in detail what watchers
are and how they can be used in Vue js 3 using the Composition API and Script Setup. I usually like to cover both APIs in the same post, but in this case, there are a few too many differences that would make the article complicated, so I have decided to split it into two separate articles. You can find the Options API on this post: How to use Watch in Vue 3 in Option API.
In this post, I am going to provide as many details as I can to make it easy to comprehend, but a basic understanding of Vue Js and its lifecycle is beneficial.
What are Watchers in Vue Js
Before we learn how to use watch
Vue Js, we should first define what this option actually is and when we should use it.
A watchers provide Vue Js users the ability to produce side effect in reaction to a change in state in one or more reactive variables.
Watch are very similar to computed properties as they are both defined as a feature that allows the user to “watch” for a property or data change. Even if it is common for new Vue developers to get confused between these two options, there is a clear distinction between them.
Computed properties return a value and do not produce any side effects. So for example a Full name could be a computed property or a sum of the available rows can be a computed property. Computed property should do nothing else than produce derived values and never trigger any other action within them.
Watchers on the other hand are purposely meant to be created to produce side effects. So for example recording some logs when the users change a selection, or triggering an API when a certain condition is met. This is a perfect example of watchers
as they do not return any value, but just trigger an action as a consequence of one or more reactive property changes.
Watchers are not extremely common and you will probably end up using them just on special occasions, but they are an extremely useful feature for a complex component that relies on side effects (logs, API calls, evaluation from dataset).
Watchers and Vue Js lifecycles
Before we move on to discuss how to use this feature is important to understand when this feature takes place and when it is triggered. Understanding its placement within the Vue lifecycle will not only be beneficial to use this, but it will also help you comprehend advanced use cases.
To fully understand the watch
option, we need to learn “what” triggers it, and “when” the triggered method takes place.
What triggers a watch to be called
As we have previously mentioned watch
is triggered by a “change in state”. What this means is that a watch, like computed, is directly related to one or more variables (data, props, computed and even Vuex getters).
When the variable looked at by the watcher changes, the method assigned will be called. Before we move on to try and understand when this actually happens with the Vue lifecycle, we are going to cover a couple of fo simple examples to clarify the above paragraph.
If you have used Vue Js at all, you are well aware that a computed property will re-evaluate as soon as anything that is part of its method block is changed.
<script setup>
import { ref, computed } from 'vue'
const username = ref('Zelig880');
const myUsername = computed(() => {
return `My username is: ${this.username}`;
})
</script>
In the above example, the computed property myUsername
will trigger as soon as the ref username
changes. So while in a computed method, any reactive reference used within its body is going to be observed, in the watch method things work differently as the “watched” variable(s) need to be declared as part of the function argument as shown below:
watch(question, async (newQuestion, oldQuestion) => {
<script setup>
import { ref, computed } from 'vue'
const username = ref('Zelig880');
watch(username, (newUsername) => {
// Do something with the updated value.
});
</script>
In the above example, a watch method would be triggered if the username refs change. I want to emphasise that watchers and computed are not the same and this example is just used to support the understanding of the feature.
When is watch
triggered
In the above section, we have learned that watchers
are actively listening to specific variables and will trigger their method as soon as any of these variables change.
In this section, we are going to analyse the Vue lifecycle and understand at what state are these functions actually triggered. Not knowing when the method is actually triggered is usually the result of dirty code and unnecessary hacks.
To ease of understanding I am going to paste part of the lifecycle diagram from the Vue documentation:
The reason why I have just passed the middle part of the lifecycle is because watchers
are triggered right here at the same time as the beforeUpdate
lifecycle hooks are called.
For the reader that has just seen this diagram for the first time, the Mounted lifecycle in the middle of the image symbolizes the component being completely loaded and rendered in the DOM, while the dotted circle around it represent the loop that happens at any time any change of a reactive property of a component (data, property, computed).
The main reason why I wanted to write this section is to emphasise two important points:
- Watchers are not called when the component is first mounted (there is a special flag to make this happen that we will cover later).
- Watchers are called “before” the component is re-rendered. So the DOM is still displaying the old values.
Let’s create a simple chronological list of how things would take place to:
- Component Instance is called
<myComponent firstName=.... />
- The component is mounted and displayed in the DOM – NOTE: The watch is NOT called!
- The property
firstName
is changed by the parent - The Component lifecycle started the update cycle
- The Watch method is triggered
- The Component is re-rendered with the new value
As we will cover later in the article, it is possible to trigger a watch effect after the DOM is re-rendered and there is no need to create any specific hack. I know I have already said above, but it is really important to understand this because the code included in the watch method should never rely on the updated DOM (so we are not supposed to check the DOM or its state).
Real-life examples
Let’s cover a couple of examples and learn more about this Vue Js feature. As mentioned at the start of this article, we are going to cover Option API examples only and we are defining them using the Single File Component (SFC):
<script setup>
import { ref, watch } from 'vue'
const selected = ref(0);
watch(selected, ( newValue, oldValue ) => {
triggerLog(newValue);
})
</script>
In the above example, we are triggering a log call as soon as the selected
data is changed. To be able to use watch
using the composition API and the script syntax, we have to first import it from vue:
import { ref, watch } from 'vue'
After the watch
is imported, we are able to call it once or multiple times. The first article accepted by this feature is the actual ref, computed or store getters that we want to observe, in our case selected
.
The second argument is the callback function that we want to trigger any time the watched variable changes. This callback accepts two arguments, The first argument includes the new value of the observed variable, while the second consists of the old value.
The above was just a simple example, but it is not time to start and introduce different options and features of this feature, starting with multiple watches and inline getters.
Watch multiple variables and inline getter
As I have already defined at the start of this article, I have decided to split up the documentation between Composition API and Options API due to some differences that would have made the article to complex to follow.
The ability to watch multiple variables at once or set an inline getter is just available within the Composition API and a workaround needs to be implemented to achieve the same in the Options API.
I have been using watchers for quite some time, and I was very excited when this features landed within Vue 3 as it was the source of verbose and unclean code.
Let’s analyse first the need to watch for multiple variables. This is a very common scenario when completing a form that should emit a side effect. Let’s reuse the above example with a few more inputs:
<script setup>
import { ref, watch } from 'vue'
const name = ref(''),
surname = ref('');
watch([ name, surname ], ( newValue ) => {
triggerLog(newValue); //newvalue is an array including both values
})
</script>
In the above example, we have used an array as the first parameter of our watch function and used it to pass multiple refs to it [ name, surname ]
. The second part looks identical to our first example but I has a hidden difference as the value of “newValue” (and “oldValue” if we would have used it), is not just the value that changed, but it is an array including all the value that we are watching.
I am going to provide a chronological example to help understand these values.
<script setup>
import { ref, watch } from 'vue'
const name = ref(''),
surname = ref('');
watch([ name, surname ], ( newValue, oldValue ) => {
triggerLog(newValue); //newvalue is an array including both values
})
</script>
// Name changes to Simone
//OUTPUT of newValue: ['Simone', '']
//OUTPUT of oldValue: ['','']
// Surname changes to Cuomo
//OUTPUT of newValue: ['Simone', 'Cuomo']
//OUTPUT of oldValue: ['Simone','']
As we can see from the above example, the value of newValue
and oldValue
includes all values that we are watching and not just the ones that we are changing. I would suggest using array restructuring to improve readability:
watch([ name, surname ], ( [ newName, newSurname] ) => {
...
})
Now it is time to introduce the second improvement, the ability to pass inline getters or computed properties as part of our observed value.
<script setup>
import { ref, watch } from 'vue'
const age = ref(0);
watch(
() => age.value > 50,
( newValue ) => {
triggerLog(newValue);
})
</script>
In the above example, we are going to trigger a log just if the value of age if greater than 50. This feature was available in the Option API , by using computed, but having the ability to declare these getters directly within the Watch function is really going to improve the development experience.
Please note that due to the fact that we accessing a ref, we have to use age.value
as explained in the Vue 3 docs.
A very important note when using inline getters, is that our watch is just going to be triggered if the returned value of our getters changes. This means that the watch callback is not going to be changed if the value of age changes multiple times unless the value fluctuates between the value of 50. So for example:
<script setup>
import { ref, watch } from 'vue'
const age = ref(0);
watch(
() => age.value > 50,
( newValue ) => {
triggerLog(newValue);
})
</script>
// Age change to 20;
// Watch NOT triggered
// Age change to 40;
// Watch NOT triggered
// Age change to 60;
// Watch triggered
Before we move on to the next features, I wanted to share that watch is able to accept a mixture of getters and refs as part of its observer variable array:
watch(
[ simpleRef, storeGetters, () => age.value > 50 ],
( newValue ) => {
triggerLog(newValue);
})
Watch reactive objects – AKA DEEP
Until now we have always looked at refs and getters, but the watch
the method is also able to support complex objects as the one declared using reactive
.
Differently from the Option API, the watch
method is able to handle complex objects out of the box and automatically apply the “deep” option if it detects an object as it observed values:
var form = reactive( { name: '', surname: '' } );
watch(
form,
(newForm) => {
}
)
It is important to realize that observing objects requires traversing the object properties and this can be very complex for large objects and should be used with caution. Watching a large object may result if a slow and resource-heavy code execution.
Immediate trigger – AKA immediate
It is now time to cover another scenario that we would likely experience during real-life development of a Vue 3 application. In this section, we are going to cover the need to call our Watcher immediately on mounted. This is usually needed when the callback function is needed to set some specific state on the application and needs to be run at all code iterations, even on the first one.
This is achievable using two different methods. The first involves the use of the Composition API directly without the use of “watch”, and the second uses a new method called “watchEffect”.
Immediate watch
Due to the syntactic sugar of the composition API, solving this problem does not actually need any specific feature as we are able to “trigger” our callback manually:
// Watchers triggered ONLY if the "src" variable changes from its frirst value
<script setup>
import { watch } from 'vue'
const imgSrc = defineProps(['src'])
watch( imgSrc, preloadImage );
</script>
// Watchers triggered on load too
<script setup>
import { watch } from 'vue'
const imgSrc = defineProps(['src'])
preloadImage( imgSrc );
watch( imgSrc, preloadImage );
</script>
Using the “watch” method out of the box would not provide the ability to trigger it immediately, but the use of the composition API makes the code to achieve this extremely simple. The only difference between the two examples above is the addition of a manual call of the method “preloadImage”. Due to the nature of the composition API, this method is going to trigger very early within the Vue lifecycle (even before the component is mounted). If the method needs to actually run after the DOM is completely rendered, we would need to wrap it in an “onMounted” callback:
<script setup>
import { watch, onMounted } from 'vue'
const imgSrc = defineProps(['src'])
onMounted( () => {
preloadImage( imgSrc );
} );
watch( imgSrc, preloadImage );
</script>
WatchEffect
In this article I am just going to cover the basic of this feature, as I personally believe it to be quite complex and I would not want to make this article too complex as it is intended for newcomers.
The watchEffect
is a different iteration of watch
that runs immediately when a component is first rendered. If you have ever used the composition API, watchEffect
would be similar to the use of the immediate
option.
As mentioned above I am purposely avoiding providing more information and code example in this post.
Run after DOM manipulation – AKA Flush
We have reached the last option available within this Vue Js feature. As we mentioned before, watch
are triggered before the component is fully re-rendered, but this can actually be changed using the “flush” configuration.
Using “flush” will make sure that our watcher is called after the component is fully re-rendered and should be used for methods that require the DOM to be fully updated with the new values.
watch(
user,
( newValue ) => {
this.$refs.test.style = ....
},
{ flush: 'post'
}
)
The use of flush
is very important when the side effect included in the watch callback requires information included in the DOM. Some developers mistakenly use “nextTick” within the watch
effect to overcome this issue, but using the flush
option is actually the preferred option.
Summary
I have used Vue JS for many years, but just recently was really made aware of all the methods available when using the watchers
feature. The above post is hopefully going to help you in using this feature correctly and avoid hacky solutions for problems that can easily be fixed with the use of a single setting.
It is time to say goodbye and as always, please make sure to leave me a comment or feedback to improve this post for future readers and subscribe to my newsletter to be notified of future posts.