Hi there DEV.to community!
This article will include multiple aspects of Vue 3 that are mostly used or are kind of on the dark side and not paid attention to as they are supposed to.
As I am going to describe Vue 3, I am going to use the composition API and not the old-school options API. The concept of both methods are same so you can adapt to composition API pretty quickly. I am in no place to dictate anything and every programmer is free to choose the way they want to write their program but as a personal opinion, I prefer composition API for its concise syntax and better code management. So if you are still afraid to change to composition API I suggest you give it a shot as it will be worth it.
Table of Content
Reactivity
React isn't the only one that should come to mind when we talk about reactivity. Reactivity refers to the ability of an entity (given a webpage) to react based on the data changes. You might know this concept as MVVM. MVVM is the abbreviated form of Model-View-View-Model. As the name suggests when data changes the view changes and vice-versa.
To utilize the reactivity power of Vue there are some options that we are going cover.
Ref
You can think of a ref
as a special kind of variable that you can use inside your Vue application. This description is only true when you start working with Vue for the first time as it gets a bit more complex afterwards.
Defining a ref is as simple as this:
const message = ref("Hello World")
Use it inside your template using the interpolation syntax:
<span>{{ message }}</span>
If you are asking yourself why I called it a variable but declared message
using a const
keyword you have the every right to.
As you know you cannot change the value of constant, as it is the purpose of the const
keyword. But there is subtle little thing you should know about. Although the keyword const
doesn't let the data of a variable to be changed, it doesn't care about the nested data! This is the case with ref
as well. To understand this situation better try the code below:
const o = {
a: 1,
b: 2,
c: 3
}
console.log(o.a) // 1
o.a = 4
console.log(o.a) // 4
As you can see I can change the value of o.a
since objects are only references and not a whole value by themselves. So when I change the value of a
inside the object o
the const
limitation isn't applied. Of course, if you wanted to assign a value to o
itself it will throw an error and won't let you do it. For example, the code below is wrong:
const o = {
a: 1,
b: 2,
c: 3
}
o = "hello"
This is the same case when using ref
(and other stuff you will see later here). When you invoke a ref
function it turns everything it received into an object. This is called wrapping. Try this code:
const message = ref("Hello World")
console.log(message)
You should see something like the image below:
As you can see when logging the message
variable you are not receiving Hello World
directly, instead it is inside an object and you can access your actual value using the value
key of the aforementioned object. This lets Vue watch for changes and do the MVVM thing! :)
When you access your ref inside a Vue template there is no need to access it like message.value
. Vue is smart enough to render the actual value inside the template instead of the object. But in case you want to access or modify the value of a ref inside your script you should do so using .value
:
message.value = "Adnan!"
console.log(message.value) // Adnan!
Reactive
As you've seen when using a ref
, Vue wraps your data inside an object and lets you access it via .value
. This is usually the most used case. You can wrap almost everything using a ref
and make it reactive.
In case you wonder how Vue watches for value changes and renders the view again and again, you should check out JavaScript Proxies.
If your value is an object itself, then you can use reactive
instead of ref
. The reactive
function won't wrap your value and instead will make the object itself reactive and watchable.
const o = reactive({count: 0})
If you try to print out the o
constant you will see that it is indeed your object without any major changes:
Now you may manipulate the key count
as you would normally do in JavaScript and Vue will render the changes as soon as possible.
Here is an example:
const increase = () => {
o.count++
}
If you had ref
instead of reactive
it would have looked like this:
const o = ref({count: 0})
const increase = () => {
o.value.count++
}
If you are still unsure which one to use, keep in mind that ref
is a safe option to use.
Shallow Ref
Give that you have a ref
like below:
const state = ref({
names: {
adnan: 'babakan',
arian: 'amini',
ata: 'parvin',
mahdi: 'salehi'
}
})
And printed my last name in your template as below:
<span>{{ state.names.adnan }}</span>
If you every changed my last name like this:
state.value.names.adnan = 'masruri'
Your template will be updated to show masruri
instead of babakan
. This is due to the fact that ref
makes a deeply watched object and the changes to the view (template) are triggered for nested data as well.
There is an option to prevent such behaviour if that's what you want. To do so you may use shallowRef
. A shallowRef
acts exactly like ref
does, with an exception of not watching for deep changes.
const state = shallowRef({
names: {
adnan: 'babakan',
arian: 'amini',
ata: 'parvin',
mahdi: 'salehi'
}
})
onMounted(() => {
state.value.names.adnan = 'masruri'
})
The code above will result in your template showing babakan
as it is not watched. But changing the .value
entirely will trigger changes. So the code below will result in your template getting updated as well:
const state = shallowRef({
names: {
adnan: 'babakan',
arian: 'amini',
ata: 'parvin',
mahdi: 'salehi'
}
})
onMounted(() => {
state.value = {
names: {
adnan: 'masruri',
arian: 'amini',
ata: 'parvin',
mahdi: 'salehi'
}
}
})
This is a great option for performance-related concerns.
Shallow Reactive
So far we've known that ref
wraps the data and watches it deeply and shallowRef
wraps the data and watches it shallowly. Now tell me this, if reactive
makes an object reactive, what does shallowReactive
do?
const state = shallowReactive({
names: {
adnan: 'babakan',
arian: 'amini',
ata: 'parvin',
mahdi: 'salehi',
},
})
onMounted(() => {
state.names.adnan = 'masruri'
})
As you might have guessed the template won't be updated.
Trigger Ref
Given that you are using a shallowRef
and changed a value and now want your template to be updated according to the new data as well, you may use the triggerRef
function:
const state = shallowRef({
names: {
adnan: 'babakan',
arian: 'amini',
ata: 'parvin',
mahdi: 'salehi'
}
})
onMounted(() => {
state.value.names.adnan = 'masruri'
triggerRef(state)
})
Now the template will also show masruri
. This is more like changing from an automatic gear to a manual gear if you will.
This is usable for both shallowRef
and shallowReactive
.
Read Only
The readonly
function receives a ref
or a reactive
as an argument and returns an exact copy that is only possible to be read from. This is used when you want to make sure your data is safe and is not possible to change when watching for it.
Example:
<template>
<div>
{{ infoReadOnly }}
</div>
</template>
<script setup>
const info = ref({
first: 'Adnan',
last: 'Babakan'
})
const infoReadOnly = readonly(info)
onMounted(() => {
info.value.first = 'Arian'
})
</script>
Though I've changed the value of info
, since infoReadOnly
is actually a live copy of info
my changes are reflected in infoReadOnly
as well. Yet you may not change the values using infoReadOnly
directly:
const info = ref({
first: 'Adnan',
last: 'Babakan'
})
const infoReadOnly = readonly(info)
onMounted(() => {
infoReadOnly.value.first = 'Arian'
})
This won't change the data and will warn you in the console as below:
Shallow Read Only
If we have ref
and shallowRef
, reactive
and shallowReactive
, why not have a shallowReadonly
?
A shallowReadonly
only makes the root level elements readonly
whilst you can change the nested data:
const stateShallowReadonly = shallowReadonly({
name: 'Adnan',
friends: [
{ name: 'Arian' },
{ name: 'Ata' },
{ name: 'Mahdi' }
]
})
onMounted(() => {
stateShallowReadonly.name = 'Brad'
})
The code above will warn you and won't change the value of name
since it is a direct property.
But you can freely change anything inside friends
since it is nested:
const stateShallowReadonly = shallowReadonly({
name: 'Adnan',
friends: [
{ name: 'Arian' },
{ name: 'Ata' },
{ name: 'Mahdi' }
]
})
onMounted(() => {
stateShallowReadonly.friends[0].name = 'Brad'
})
Computed
Man, I love computed in Vue! You can imagine it as a glass in which you can mix your potions and still have your potions standing there intact!
A computed is like a ref
or reactive
that can be accessed and watched but not changed. Then what's the difference between a computed and a readonly you might ask. A computed can be a mixture of stuff. For example:
const state = ref({
first: 'Adnan',
last: 'Babakan'
})
const fullName = computed(() => state.value.first + ' ' + state.value.last)
Now you have a fullName
which you may access its value inside a template with {{ fullName }}
or inside your script using fullName.value
.
The value of fullName
will always depend on the state.value.first
and state.value.last
and will change if those guys change.
A computed receives a function that returns a value and can depend on multiple reactive data.
Writable Computed
Though a computed is mostly used to read a combination of data, the possibility to make a computed writable is also there.
Instead of passing a function to computed you may pass an object including two properties called get
and set
that both are functions.
For instance:
const state = ref({
first: 'Adnan',
last: 'Babakan'
})
const fullName = computed({
get: () => state.value.first + ' ' + state.value.last,
set: (value) => {
const [first, last] = value.split(' ')
state.value.first = first
state.value.last = last
}
})
Now if you try to write the value of fullName
like below:
fullName.value = 'Ata Parvin'
It will split your string into 2 parts using a space and assign the first part to state.value.first
and the second to state.value.last
.
This is not a good way to determine which part of a name is a first name and which is a last name, but for the sake of demonstration, this is the only thing that came to my mind. :D
Watching
Watching is something that you will probably need a lot. Watching is referred to the act in which you want to run something in case a reactive data changes. In different systems there are various naming for this act, sometimes they are called hooks as well. But in Vue, we will call them watches.
Watch
The first thing you will encounter. A watch
function receives two arguments. The reactive data to watch and the function to be invoked when the data changes respectively.
Here is a simple watch:
const count = ref(0)
const increase = () => {
count.value++
}
watch(count, () => {
console.log('Count changed to ' + count.value)
})
Now whenever the value of count
is changed, you will see the log Count changed to ((count))
in your console.
The callback function also receives two arguments which are passed to it when the watch is triggered. The first argument holds the new value and the second one holds the old value. Here is an example:
const count = ref(0)
const increase = () => {
count.value++
}
watch(count, (newValue, oldValue) => {
console.log('Counter changed from ' + oldValue + ' to ' + newValue)
})
Note: Be careful when using the newValue
and oldValue
with objects as objects are passed by reference.
To be more accurate, a watch
function receives a third argument as well. This third argument is an object that holds some options which can change the behaviour of the watching action.
Immediate
An immediate watch function is triggered at the instance it's created as well as when a change happens. You can think of it as the difference between a while
loop and a do...while
loop if you know what I mean. In other words, even if there is never a change, your callback function will run at least once:
watch(count, () => {
console.log('Count changed to ' + count.value)
}, {
immediate: true,
})
The value for immediate
can be true
or false
. And the default value is false
.
Once
If you want your watcher to run only once, you may define the once
option and set its value to true
. The default value is false
.
watch(count, () => {
console.log('Count changed to ' + count.value)
}, {
once: true,
})
This will only trigger once when the value of count
changes.
Advanced Watchers
Previously we've mentioned that watchers accept a reactive data as the first argument. While this is true, this is not the whole case.
A watch
function can receive a getter function or an array of reactive objects and getter functions. This is used for when we need to watch for multiple data changes, and/or when we need to watch the result of two or more things when affecting each other. Let's have some examples.
Watching Getter Function
Take the code below as an example:
<template>
<div>
<div>
<div>Timer one: {{ timerOne }}</div>
<div>Timer two: {{ timerTwo }}</div>
</div>
<button @click="timerOne++">Accelerate timer one</button>
<button @click="timerTwo++">Accelerate timer two</button>
</div>
</template>
<script setup>
const timerOne = ref(0)
const timerTwo = ref(0)
onMounted(() => {
setInterval(() => {
timerOne.value++
timerTwo.value++
}, 1000)
})
watch(() => timerOne.value - timerTwo.value, () => {
console.log('There was a change in the gap between timerOne and timerTwo. Gap is ' + (Math.abs(timerOne.value - timerTwo.value)) + ' seconds.')
})
</script>
It's a simple code that makes 2 refs holding a number and increasing both of them 1 by 1 each second. Logically the difference of these two refs are always equal to zero unless one gets changes out of its turn. As both increase the difference stays 0 so the watched won't get triggered as it only watches for the changes to the result of timerOne.value - timerTwo.value
.
Yet there are two buttons that each adds 1 to timerOne
and timerTwo
respectively. When you click on any of those buttons the difference will be more or less than 0 thus the watch being triggered and logging the gap between these two timers.
Watching Multiple Values
Here is an example of an array of reactive data being passed to the first argument of the watch
function:
<template>
<div>
<div>
<div>Counter one: {{ counterOne }}</div>
<div>Counter two: {{ counterTwo }}</div>
</div>
<button @click="counterOne++">Increase counter one</button>
<button @click="counterTwo++">Increase counter two</button>
</div>
</template>
<script setup>
const counterOne = ref(0)
const counterTwo = ref(0)
watch([
counterOne,
counterTwo,
], () => {
console.log('One of the counters changes')
})
</script>
No matter which ref changes, the watcher will be triggered.
Watch Effect
A watchEffect
function acts almost exactly like a watch
function with a main difference. In a watchEffect
you don't need to define what to watch and any reactive data that is used inside the callback you provide your watchEffect
is watched automatically. For example:
const count = ref(0)
watchEffect(() => {
console.log(count.value)
})
In case our count
is changed the watchEffect
will trigger its callback function since count.value
is used inside it. This is good if you have complex logic to watch for.
Hope this was useful and you've enjoyed it. In case you spot any mistakes or feel like there should be an improvement, kindly let me know.
BTW! Check out my free Node.js Essentials E-book here:
Feel free to contact me if you have any questions or suggestions.
Top comments (1)
I liked this. very nice, thank you.