Telerik blogs

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?

  1. When in doubt
    I recommend defaulting to ref. Ref is by far easier to not accidentally create a hard-to-track bug. But as you gain more understanding of where reactive may serve you better, you will be able to choose the best tool for the job.
  2. 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 use ref. When you make an object or array reactive, 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.value) // Prints 2

reactiveObj.myNumber = 2
console.log(double.value) // 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.value) // Prints 2

reactiveObj.myNumber = 2
// We could also do myNumber.value = 2

console.log(double.value) // 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!


Vue
About the Author

Marina Mosti

Marina Mosti is a frontend web developer with over 18 years of experience in the field. She enjoys mentoring other women on JavaScript and her favorite framework, Vue, as well as writing articles and tutorials for the community. In her spare time, she enjoys playing bass, drums and video games.

Related Posts

Comments

Comments are disabled in preview mode.