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
age
anduser
are exposed as top-level properties in<script setup>
. - Since
age
is a top-levelRef<T>
in<script setup>
, it gets auto-unwrapped in<template>
, which means{{ age }}
in<template>
will equal toage.value
in<script setup>
, thus resolves to5
. - In JavaScript,
toFixed
is a method defined in the prototype of number;5
is a number, so5.toFixed
will evaluate to that function, thus showingfunction toFixed() { [native code] }
on the screen. - Although
user.age
andage
are exactly the same variable in<script setup>
,{{ user.age }}
will not get auto-unwrapped in<template>
becauseuser.age
is not a top-level property —user
is! - Since
user.age
is not auto-unwrapped in<template>
,{{ user.age }}
in<template>
will equal touser.age
in<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>
.