To ref or not to ref? Let’s explore through a quick, example-driven approach to understanding the core differences between ref and reactive in Vue 3.
It has been a while since Vue 3 was fully released, but one concept that still stumps newcomers to the framework is understanding the key differences between ref
and reactive
when working
with the composition API.
I don’t want to do a complete rewrite of the docs, so let’s first get out of the way the more “obvious” difference, or the key one that absolute everyone must understand before working with these two.
On one hand, reactive
can only work with object types. That is Object
, Array
, Map
and Set
.
The much more flexible ref
can work with any other type of value, including primitives like String
and Number
. So the first thing you have to ask yourself
when choosing to work with one or the other is what type of data are you going to be making reactive.
Having said that, I’m not going to talk about anything other than object type values for the remainder of the article, since anything that does not fit in that category can only be handled by using a ref
, and is
likely not a source of confusion.
So the real question is: To ref
or not to ref
?
- When in doubt
I recommend defaulting toref
. Ref is by far easier to not accidentally create a hard-to-track bug. But as you gain more understanding of wherereactive
may serve you better, you will be able to choose the best tool for the job. - When the pointer will change
If you are going to work with a reactive variable which will change pointers to new, different objects, then you want to useref
. When you make an object or arrayreactive
, you are going to commit to that pointer and not try to replace it later on with a brand-new one.
Sometimes I’ve heard people say that they’re more comfortable working with ref
exclusively because they have only worked with it and have never had to work with reactive
objects before. To an extent, this can be true—you can certainly get away with making everything a ref
. But I’ve got news for you—if you’ve worked with props
before in the composition API, you’ve already worked with reactive
objects.
setup (props) {
// props is `reactive`
}
<script setup>
const props = defineProps({})
// props is `reactive`
</script>
With this in mind, you are able to see the main advantage of working with reactive
objects: you don’t have to use the .value
property to access their properties’ values.
<script setup>
import { reactive } from 'vue'
const props = defineProps({
modelValue: { type: String, default: 'Reactive prop!' }
})
console.log(props.modelValue) // outputs: Reactive prop!
const reactiveUser = reactive({
name: 'Michael Scott'
})
console.log(reactiveObj.name) // outputs: Michael Scott
</script>
However, as I mentioned earlier, you should not assign a new object or pointer to the reactive value. Doing this will apparently “work” but will break the reactive connectivity to the original object, which is a fancy way of saying that reactivity tied to the original object will be broken or bugged out.
Let’s look at an example of how not to do it.
<script setup>
import { reactive, toRefs, isRef, isReactive, unref, computed, ref } from 'vue'
let user = reactive({
name: 'Michael Scott'
})
const firstName = computed(() => user.name.split(' ')[0])
console.log(user.name) // outputs: Michael Scott
console.log(firstName.value) // outputs: Michael
user = reactive({
name: 'Jim Halpert'
})
console.log(user.name) // outputs: Jim Halpert
console.log(firstName.value) // outputs: Michael. BUG!
</script>
Notice that the firstName
computed did not “see” the change because the reactive value inside firstName
is tied to the original user, Michael (or rather its pointer).
If you want to change objects while keeping reactivity in a singular variable, the safest way to do it is with ref
.
<script setup>
import { ref, computed } from 'vue'
const user = ref({
name: 'Michael Scott'
})
const firstName = computed(() => user.value.name.split(' ')[0])
console.log(user.value.name) // outputs: Michael Scott
console.log(firstName.value) // outputs: Michael
user.value = {
name: 'Jim Halpert'
}
console.log(user.value.name) // outputs: Jim Halpert
console.log(firstName.value) // outputs: Jim
</script>
It’s important to point out that the real value in the above example is that even though we swapped the user object completely from one point to another (Michael to Jim), our firstName
computed property will
correctly “see” and react to the change and re-calculate the value.
The last gotcha that I want to explore is the loss of reactivity when using reactive
and passing down values to composition functions and third-party libraries. The following example illustrates a common problem.
<script>
#useNumber.js
import { computed } from 'vue'
export default (number) => {
const double = computed(() => number * 2)
return { double }
}
</script>
<script setup>
#MyComponent.vue
import { ref, reactive } from 'vue'
import useNumber from 'useNumber'
const reactiveObj = reactive({
myNumber: 1
})
const { double } = useNumber(reactiveObj.myNumber)
console.log(double) // Prints 2
reactiveObj.myNumber = 2
console.log(double) // Prints 2, computed did not trigger
</script>
Can you tell exactly what went wrong?
Whenever we access a reactive
object or array by one of its properties or indexes, we get the current value of the reactive object in a non-reactive form. So in the above example, when
we passed reactiveObj.myNumber
down to useNumber
, we actually gave it the Number
2, not the reactive pointer to the myNumber
property.
There’s a few ways to solve this problem. The one I prefer (because it also works very nicely with props
) is to use toRefs
.
toRefs
allows us to create a ref
out of every single property in a reactive object. That way we can pass down the reactive value to our useNumber
composition function.
<script>
#useNumber.js
import { computed } from 'vue'
export default (number) => {
const double = computed(() => number.value * 2) // We need .value now that its a ref
return { double }
}
</script>
<script setup>
#MyComponent.vue
import { ref, reactive, toRefs } from 'vue'
import useNumber from 'useNumber'
const reactiveObj = reactive({
myNumber: 1
})
const { myNumber } = toRefs(reactiveObj) // myNumber is a ref
const { double } = useNumber(myNumber)
console.log(double) // Prints 2
reactiveObj.myNumber = 2
// We could also do myNumber.value = 2
console.log(double) // Prints 4
</script>
Hopefully with these examples you’ve gained a bit more clarity about the differences of the two, and will be able to choose the best tool for the job on your own code!