Our third article of Exploring VueJS series will continue diving into another aspect of data-binding with computed and watch properties in our newly created Vue component - the Pokemon Card. By the end of this post, you will understand what reactive dependencies are and how do they work in Vue.js, and the different use cases of computed and watch properties.
First of all, let’s do a quick recap from our last article, shall we?
Table of contents
Quick review
In our previous post, we created our very first component - PokemonCard, which looks like:
Besides, we have learned about:
- The general concepts of Vue component
- Data property as our component’s local state management
- Basic data-binding with v-on and v-bind
- Functionalities with methods and how to register our component to use within the app. If you miss it, feel free to catch up here before moving forward.
Free eBook
Directives, simple right? Wrong! On the outside they look simple, but even skilled Angular devs haven’t grasped every concept in this eBook.
- Observables and Async Pipe
- Identity Checking and Performance
- Web Components <ng-template> syntax
- <ng-container> and Observable Composition
- Advanced Rendering Patterns
- Setters and Getters for Styles and Class Bindings
And yes you can always find our code for today’s topic in the series’ Github Repo.
Are you ready? Let’s start.
A new feature request for isSelected
At this point, the component PokemonCard
has three local data
variables, which are
title: "Sample pokemon",
image: "https://res.cloudinary.com/mayashavin/image/upload/v1562279388/pokemons/br5xigetaqosvwf1r1eb.jpg",
//Flag property to keep track if component is selected
isSelected: false
And we print out true
or false
to indicate if a user selects the component. Now let’s say we have a feature request of replacing these boolean values with the following icons in the top left corner:
for selected state.
for un-selected state.
One solution is to invoking a method which will return the correct icon according to isSelected
value in the needed expression:
<!--<template>-->
<!--<p>Selected: {{ isSelected }}</p>-->
<img :src="selectedIcon()" />
//<script>
methods: {
selectedIcon() {
return this.isSelected
? "https://res.cloudinary.com/mayashavin/image/upload/w_24,ar_1:1,c_scale/v1566137286/ExploringVueJS/check-circle-solid.png"
: "https://res.cloudinary.com/mayashavin/image/upload/w_24,ar_1:1,c_scale/v1566137284/ExploringVueJS/circle-regular.png";
}
}
Simple right? Or so we think.
The problem of method
The solution is simple, but the template is no longer that simple. There are several things to consider here:
selectedIcon()
returns a new value for every invoke without any cache; thus, we will always have to rerun this function in every access to it. Take the below example for instance:
<p>First access: {{ randomTextMethod() }}</p>
<p>Second access: {{ randomTextMethod() }}</p>
In the above code, we simple display a random text generated by calling the method randomTextMethod
, which is:
methods: {
randomTextMethod: () => Math.random(100)
}
Here are the quiz for you, will the texts be the same in first and second access?
The answer is no. Each rerun of the method will generate and return a different value. And this applies to each re-rendering of the component too!
-
Missing
()
in typing is pretty easy to encounter due to the consistency with other data variable convention use in a template, and will cause a bug. Declarative is no longer at its best. -
What if we need to add additional attributes - for example
alt
text to ensure the accessibility of the icon? Certainly, thealt
description will be reflecting the correct state too. We certainly can add another method foralt
, such as:
//<script>
methods: {
selectedIcon() {
return this.isSelected
? "https://res.cloudinary.com/mayashavin/image/upload/w_24,ar_1:1,c_scale/v1566137286/ExploringVueJS/check-circle-solid.svg"
: "https://res.cloudinary.com/mayashavin/image/upload/w_24,ar_1:1,c_scale/v1566137284/ExploringVueJS/circle-regular.svg";
},
selectedIconAlt() {
return this.isSelected
? "Icon for the selected card."
: "Icon for the non-selected card."
}
}
But that will be redundant since the condition is the same in both methods. And it will get worse when there is a need to modify the comparison logic. Any change in the logic will require to update in more than one location (here it will be in selectedIcon
and selectedIconAlt
. That will lead to complexity and hidden bug for code maintaining. After all, less code is less chance for bugs to happen.
So is there any better way? Well, welcome to computed property.
Compute your data on the fly
Similar to method, a computed property is a function returns a value and locates inside computed
object. There is change in the syntax, thus we can safely move selectedIcon
code from methods
to computed
:
/* PokemonCard.vue */
//inside <script>
export default {
data: { /*...*/ },
computed: {
selectedIcon() {
return this.isSelected
? "https://res.cloudinary.com/mayashavin/image/upload/w_24,ar_1:1,c_scale/v1566137286/ExploringVueJS/check-circle-solid.svg"
: "https://res.cloudinary.com/mayashavin/image/upload/w_24,ar_1:1,c_scale/v1566137284/ExploringVueJS/circle-regular.svg";
}
}
}
But unlike method, computed property has its own getter, which allows it to be called in a similar manner with variable - without ()
:
<img :src="selectedIcon" />
The end result? It will be exactly the same. But not always.
Why? Let’s go back a bit on one of our examples above, with randomTextMethod
. We will add a new computed property called randomTextComputed
which is identical with randomTextMethod
:
methods: {
randomTextMethod: () => Math.random(100)
},
computed: {
randomTextComputed: () => Math.random(100)
}
Similarly, in <template>
tag, we will print randomTextComputed
out twice in a separate section besides the previous code.
<section>
<h3>Using methods</h3>
<p>First access: {{ randomTextMethod() }}</p>
<p>Second access: {{ randomTextMethod() }}</p>
</section>
<section>
<h3>Using computed</h3>
<p>First access: {{ randomTextComputed }}</p>
<p>Second access: {{ randomTextComputed }}</p>
</section>
And 💥, the result will be:
As we can see, while the results are different for randomTextMethod
, the return values are very consistent for randomTextComputed
regardless of how many times we access it.
Return values of computed properties, unlike methods, are cached based on their reactive dependencies.
Thus selectedIcon
will re-evaluate only when isSelected
changes. Besides, randomTextComputed
will always return the previously computed result since there is no reactive dependency,
So, what is a reactive dependency, and how does it work? Let’s find out.
Reactive dependencies at its best
What is reactive dependency?
As mentioned before, Vue.js follows the MVVP (Model-View-View-Model) approach strictly. When the model changes, the related view updates. Each of the models is a property defined in the Vue component’s data
object, and hence will be connected to the Vue reactive system automatically upon creation.
In short, a reactive dependency of a computed property is a responsive property (of data
or another computed property) which will affect or decide its return value.
Nice, isn’t it? But how does Vue compiler keep track of all the dependencies of a computed property?
How dependency-tracking works
By modifying Object.defineProperty
of its data
properties and convert them to getters/setters, respectively, the system can enable this connection easily. Let’s take our selectedIcon
as an example.
During the creation of the component and the first time when we call selectedIcon
, isSelected
value is accessed. In other words, selectedIcon
triggered the getter of isSelected
. This action will notify the system to register isSelected
as a reactive dependency of selectedIcon
.
Since then, whenever the value of isSelected
changes, the setter of isSelected
is triggered knowing that selectedIcon
relies on isSelect
. As a result, the system will re-evaluate the value of selectedIcon
based on the new information and then decide on necessary re-rendering.
In short, for every component instance, there is a watcher attached to each property’s setter and getter. This watcher will record any read/write (access/modify) attempt, notify the system on whether to re-render the component. The diagram below demonstrates how it works perfectly:
Notes on reactive dependencies
Because Object.defineProperty
is an ES5 feature, Vue.js doesn’t work with IE8 and below. Bear in mind that if support for older versions of IE is critical to your application.
Important note: The Vue system updates the DOM accordingly and asynchronously. Whenever there is a data property change, the component will not re-render immediately. Instead, the system will open a distinct watcher queue and buffer all the changes that happened within the same event loop.
Since the queue contains unique elements, a watcher will be pushed into the queue only once, regardless of how many times it is triggered. Then, only in the next ‘tick’ of the event loop, the Vue system will flush the queue and do the actual update of that component on the DOM. It is a significant move to avoid unnecessary complex computations and DOM manipulations, which are known to be costly for the application’s performance in the long run.
So far, so good? Great.
Here comes the next question: Is there any other way to monitor data changes without the need of creating a computed property? For example, if we want to observe specific data changes to fire an event or to log the information, and have no use for another computed data?
Well, here comes the watch property.
Watch your data with watch
properties
What is watch property?
First of all, Vue inherits the watch
concept from AngularJS, as a more generic way (or the only way in AngularJS) to establish a custom watcher for specific data updates on a Vue component.
The syntax is simple and straightforward, watcher for a specific data property will locate as a property inside watch
object. And each watch property is a function whose name will be identical to the data property it is observing. For instance, let’s get back to our PokemonCard
component and add a watcher for isSelected
, to log its status to the console.
export default {
data: {
/*...*/
isSelected: false,
},
/*...*/
watch: {
isSelected(val) {
console.log(val);
}
}
}
The val
paramater passed to the watcher for isSelected
is the new value of isSelected
. This watcher will be triggered right after isSelected
’s value is updated. Easy enough right?
So why and when should we choose to use watch property over computed?
watch vs. computed property
By definition, watch property, as a normal function, provides us a generic custom way of observing and reacting to data changes on a component instance. Meanwhile, computed property is a function with its getter and setter and returns a value. Computed property allows us to cache and update specific data whose value depends on other reactive data property, in a more declarative manner.
In most of the times, the best practice is to use computed property, for readability and performance optimization. Nevertheless, when there is a need to perform asynchronous API requests or complex operations, wait with intermediary states until those requests or actions completes. An excellent use case is displaying different loading statuses while waiting for extra information from the server based on the user’s interaction. With computed property? It will be impossible to achieve such a thing.
In general, unless it’s a unique case discussed above, it’s recommended to favor using computed property over method or watch property when there is data dependency.
Great. Before we conclude our session, let’s review how our component looks like at this point and make it pretty if needed, shall we?
Component with a style
After adding selectedIcon
, in the browser, our PokemonCard
will look like below:
With our JavaScript code as following:
export default {
data() {
return {
title: "Sample pokemon",
image:
"https://res.cloudinary.com/mayashavin/image/upload/v1562279388/pokemons/br5xigetaqosvwf1r1eb.jpg",
//Flag property to keep track if component is selected
isSelected: false
};
},
methods: {
toggleSelect() {
this.isSelected = !this.isSelected;
},
},
computed: {
selectedIcon() {
return this.isSelected
? "https://res.cloudinary.com/mayashavin/image/upload/w_24,ar_1:1,c_scale/v1566137286/ExploringVueJS/check-circle-solid.png"
: "https://res.cloudinary.com/mayashavin/image/upload/w_24,ar_1:1,c_scale/v1566137284/ExploringVueJS/circle-regular.png";
}
},
};
Obviously, we need a bit of styling touches, such as moving the icon to top right corner, with a surrounding margin of 5px. To achieve that, we will add a new class select-icon
to the newly added img
tag:
<img :src="selectedIcon" class="select-icon"/>
In the <style>
section:
.select-icon {
position: absolute; /*position the icon relative to its first non-static positioned ancestor*/
top: 0px; /*position the icon within 0px from the top position of its first positioned ancestor*/
right: 0px; /*position the icon within 0px from the right position of its first positioned ancestor*/
margin: 5px;
}
That’s it? Not yet. We do need to position our card container to relative
. This CSS style ensures our icon’s position will align with our card, not to any other ancestor:
.card {
position: relative;
/* same as before*/
}
.select-icon {
position: absolute;
top: 0px;
right: 0px;
margin: 5px;
}
And 🎉, our component is styled!
Conclusion
By exploring the differences between a computed property and method in detail, we learned about how Vue.js implements reactive dependency under the hook, hence continue to improve our component code. Besides, we also get to understand when and why to use watch property or computed property, for better coding habit and for optimizing the app’s performance.
You are invited to try it out the code for the pokemon app during this post here.
What’s next
Since we only cover the primary configuration options up to this point, every PokemonCard component instance has the same data information value. What about reusing and passing different information to different PokemonCard
instances? In the next post, we will uncover data-binding with props
from the external component and how to use VueDevtools to debug data changes in the app’s components.
Until then, thank you so much for reading, stay tuned, and see you in the next post 🚀!