ref()
or reactive()
You must learn ref()
and reactive()
before getting into this chapter.
So... which one should I use to declare reactive values, ref()
or reactive()
?
We're finally here! This is probably the most commonly asked question in Vue 3.
We'll try to answer this question by explaining how these two functions work, then decide which one should we use based on the type of argument. But all in all the conclusion is:
- For primitive values,
ref()
is recommended. - For functions, neither
ref()
norreactive()
is recommended; just uselet
,var
, orconst
— whichever is best for you. - For any other type of values, either
ref()
orreactive()
is fine.
How ref()
and reactive()
Work
In order to know how to choose between ref()
and reactive()
, it's essential to know how they work respectively.
How ref()
Works
The following pseudocode gives us a decent concept of how ref()
works in Vue 3. Although it is extremely simplified and rearranged, we're still able to get the main idea of what's going on inside ref()
:
import { reactive, Ref } from 'vue'
const ref = (arg) => {
if (arg is Ref) {
return arg
} else {
return new RefImpl(arg)
}
}
class RefImp<T> implements Ref<T> {
public value: T
constructor(arg: T) {
if (arg is primitive value) {
this.value = arg
} else {
this.value = reactive(arg)
}
track(this.value)
}
}
- As we've mentioned before,
RefImpl
is a class with only one public propertyvalue
. - If the argument is a primitive value,
RefImpl
will use it asthis.value
. - If the argument is not a primitive value,
RefImpl
will just callreactive()
and use the returned value asthis.value
; so by usingref()
, you're actually usingreactive()
as well. You just didn't realize it! - The
track(this.value)
works very differently than the source code; but the point is,RefImpl
will "track" the changes ofthis.value
when needed so that reactivity can be fulfilled.
How reactive()
Works
The following pseudocode gives us a good concept of how reactive()
works in Vue 3. It's not exactly the same as the source code, but it's close enough to let us know what's going on inside reactive()
:
const reactive = (arg) => {
if (arg is primitive value) {
if (is in development mode) {
console.warn(`value cannot be made reactive: ${String(arg)}`)
}
return arg
} else if (arg is Ref OR arg is reactive OR arg is function) {
return arg
} else {
const unwrappedValue = unwrapNestedRef(arg)
return toProxy(unwrappedValue)
}
}
- As we've mentioned before,
reactive()
only works with non-primitive values. - Even if functions are non-primitive values,
reactive()
still doesn't work with it; it immediately returns it. unwrapNestedRef()
is an imaginary function we've described inUnwrapNestedRef<T>
; it is used to unwrap the nestedRef<T>
s in an object or an array.toProxy()
is an imaginary function that is used to create reactive proxy.
Explanation
We can finally dive into the most important part — explain why we choose ref()
or reactive()
over the other based on different types of arguments.
Primitive Values
If the argument is a primitive value, then ref()
would be the best choice because reactive()
only works with non-primitive values.
Of course we can wrap the primitive value into an object to make it work (i.e. const age = reactive({ value: 5 })
), but...why? Just use ref()
and you'll get the same result!
Functions
If the argument is a function, you probably don't want it to be reactive. Functions are something that should not be rendered on the screen, and it should not be used to represent the state of a component, so making them reactive is just meaningless.
However, there are some cases where we do want to assign functions to variables. For example, event subscription/registration. It's those things we register after component is mounted, and we remove them before component unmounts.
Take the Navigation Guards of Vue Router as an example:
import { onMounted, onBeforeUnmount } from 'vue'
import { useRouter } from 'vue-router'
const router = useRouter()
let unregisterNavGuard: () => void | undefined = undefined
onMounted(() => {
unregisterNavGuard = router.beforeEach((to, from) => {
// ...
})
})
onBeforeUnmount(() => {
unregisterNavGuard?.()
})
- We register the navigation guard in
onMounted()
by usingrouter.beforeEach()
, which returns the unregister function. - The unregister function should only be called in
onBeforeUnmount()
. - After we get the unregister function in step 1, we assign it to a variable called
unregisterNavGuard
so that we can use it in step 2.
Since unregisterNavGuard
has nothing to do with the rendering of a component, we just use let
instead of ref()
or reactive()
during declaration. If for some reason we want to re-assign the value in the future, the component won't do unnecessary re-renders because it's neither a reactive proxy nor a Ref<T>
.
If you REALLY want a function to be reactive (which we cannot think of any good reason), ref()
would be a better choice because reactive()
directly returns the argument if it's a function. That means const func = reactive(() => {})
will equal to const func = () => {}
.
Any Other Type
Anything other than primitive values and functions fall into this category. For example, plain object, Array, Map, etc.
In these cases, it doesn't really matter if you use ref()
or reactive()
, because under these circumstances, reactive()
will be the one to generate the final value. The .value
after Ref<T>
is the only difference.
Since neither is better than the other, using either ref()
or reactive()
is fine. Just make sure the whole team/project is following the same rule when choosing ref()
and reactive()
for code consistency and you'll be just fine!