Ref<T> and ref()
Probably the most important part in Vue 3!
In case you're new to TypeScript or OOP, <T> is the Generic Type syntax of TypeScript.
What Is Ref<T>?
Ref<T> is a type with only one public property value.
A simple interface for Ref<T> would look like this:
interface Ref<T> {
value: T
}
A Ref<T> contains only one value of any type, so you can have:
Ref<number>Ref<number[]>Ref<{ id: number, name: string }>Ref<Promise<() => void>>- ...anything you need!
What Is ref()?
ref() is a function that takes an argument of any type, and returns a Ref<T> object with that argument as its value. For example:
import { ref } from 'vue'
const name = ref('hello')
console.log(name) // { value: 'hello' }
To mutate the value of a Ref<T>, we can simply do it in the classic JavaScript way:
import { ref } from 'vue'
const name = ref('hello')
console.log(name.value) // 'hello'
name.value = 'world'
console.log(name.value) // 'world'
The same rule applies to any type of value, for example:
import { ref } from 'vue'
// array
const fruits = ref(['apple', 'banana'])
console.log(fruits.value) // ['apple', 'banana']
fruits.value[0] = 'cherry'
console.log(fruits.value) // ['cherry', 'banana']
// object
const user = ref({
name: 'hello'
age: 5,
})
console.log(user.value) // { name: 'hello', age: 5 }
user.value.name = 'world'
console.log(user.value) // { name: 'world', age: 5 }
Although the returned value of ref() seems to be a plain object like { value: 'hello' }, it's actually not! Instead, it's an instance of a class called RefImpl which has only one public property value. So from user's perspective (you and me, the developers), it's okay to just see RefImpl as Ref<T> because they expose the same property.
Also, ref() does not just blindly wrap value into Ref<T> structure, but it's not necessary to learn that now. We'll explain more in detail in ref() or reactive().
Great, we've learned enough about how Ref<T> works in <script>. Let's see how Ref<T> works in <template>!
Ref<T> in <template>
In Vue 2, we can access variables declared in <script> from <template> using 3 different syntax — double curly braces {{ }}, v-on (shorthand as @), and v-bind (shorthand as :). These 3 syntax still exist in Vue 3, but the logic is a little different. Take the following component as an example:
<template>
<!-- Will this work? -->
<div>{{ name.value }}</div>
</template>
<script setup>
import { ref } from 'vue'
const name = ref('hello')
</script>
Because name is a Ref<T>, it is very reasonable to think that <div>{{ name.value }}</div> will evaluate to <div>hello</div>. But when this component gets rendered, the output HTML will actually be <div></div>, without hello in the middle — where's our hello?
In Vue 3, when we try to access Ref<T> from <template>, sometimes (yes, SOMETIMES!) they will be automatically unwrapped. To unwrap (or unref) means to take the value out from Ref<T>. Hence, we must omit the .value behind a Ref<T> in <template> under some circumstances because Vue auto-unwraps them sometimes. So what are these "circumstances"?
The rule is simple: Vue will only auto-unwrap a Ref<T> in <template> if it is exposed as a top-level property in <script setup>. This rule also applies to v-on and v-bind.
So for the above example, if we want to see hello on the screen, we'll have to write {{ name }} instead of {{ name.value }} because name is a top-level Ref<T> in <script setup>.
<template>
<!-- This will work correctly. -->
<div>{{ name }}</div>
</template>
<script setup>
import { ref } from 'vue'
const name = ref('hello')
</script>
Great, let's see one more example of auto-unwrap:
<template>
<div>
<h1>A: {{ age.toFixed }}</h1>
<h2>B: {{ user.age.toFixed }}</h2>
</div>
</template>
<script setup>
import { ref } from 'vue'
const age = ref(5)
const user = {
age: age,
}
</script>
The output HTML of this component is:
<div>
<h1>A: function toFixed() { [native code] }</h1>
<h2>B: </h2>
</div>
Do you know why there's such difference?
This happens because... (think about it for a while before revealing the answer!)
- Both
ageanduserare exposed as top-level properties in<script setup>. - Since
ageis a top-levelRef<T>in<script setup>, it gets auto-unwrapped in<template>, which means{{ age }}in<template>will equal toage.valuein<script setup>, thus resolves to5. - In JavaScript,
toFixedis a method defined in the prototype of number;5is a number, so5.toFixedwill evaluate to that function, thus showingfunction toFixed() { [native code] }on the screen. - Although
user.ageandageare exactly the same variable in<script setup>,{{ user.age }}will not get auto-unwrapped in<template>becauseuser.ageis not a top-level property —useris! - Since
user.ageis not auto-unwrapped in<template>,{{ user.age }}in<template>will equal touser.agein<script setup>, which is aRef<T>. Ref<T>does not have a property calledtoFixed, so{{ user.age.toFixed }}resolves toundefined, causing<h2>B: {{ undefined }}</h2>to be rendered as<h2>B: </h2>.
Great, now you know how Ref<T> works in <template>! This is especially important when using composables. Without knowing this, you will end up writing so many .value in <template> that could have been avoided, which decreases the readability of your code.
ComputedRef<T> Is Also Ref<T>
ComputedRef<T> is return type of computed().
Since ComputedRef<T> extends Ref<T>, they work pretty much the same way — value is the only public property in ComputedRef<T>, and top-level ComputedRef<T> in <script setup> gets auto-unwrapped in <template>.