Ref vs. Reactive — Which is Best?
Michael Thiessen
Posted on February 22, 2023
A huge thanks to everyone who looked at early drafts of this: especially Austin Gil for challenging my arguments, Eduardo San Martin Morote, Daniel Roe, Markus Oberlehner, and Matt Maribojoc
This has been a question on the mind of every Vue dev since the Composition API was first released:
What’s the difference between ref
and reactive
, and which one is better?
My extremely short answer is this: default to using ref
wherever you can.
Now, that’s not a very satisfying answer, so let’s take some more time to go through all of the reasons why I think ref
is better than reactive
— and why you shouldn’t believe me.
Here’s what our journey will look like, in roughly three acts:
-
Act 1: The differences between
ref
andreactive
— First, we’ll go through all of the ways thatref
andreactive
are different. I’ll try to avoid giving any judgment at this point so you can see all the ways they’re different. -
Act 2: The
ref
vsreactive
debate — Next, I’ll lay out the main arguments forref
and forreactive
, giving the pros and cons of each. At this point you will be able to make your own well-informed decision. -
Act 3: Why I prefer
ref
— I end the article stating my own opinion and sharing my own strategy. I also share what others in the Vue community think about this whole debate, since one person’s opinion only counts for so much (ie. very little).
More than just a discussion of “ref vs. reactive”, I hope that as we explore this question you’ll come away with additional insights that will improve your understanding of the Composition API!
Also, this is a long article, so if you don’t have time to read it all now, definitely set this aside and come back to it — you’ll thank me later!
tl;dr — My highly opinionated and simple strategy
But first, a quick summary of my strategy for choosing.
With roughly increasing levels of complexity:
- Start with using
ref
everywhere - Group related things with
reactive
if you need to - Take related state — and the methods that operate on them — and create a composable for them (or better yet, create a store in Pinia)
- Use
reactive
where you want to “reactify” some other JS object like aMap
orSet
. - Use
shallowRef
and other more use-case-specific ref functions for any necessary edge cases.
Act 1: The differences between ref and reactive
First, I want to take some time to specifically discuss how ref
and reactive
are different, and their different uses in general.
I’ve tried to be exhaustive in this list. Of course, I’ve probably missed some things — please let me know if you know of something I haven’t included!
With that out of the way, let’s look at some differences between these two tools we’ve been given.
Dealing with .value
The most obvious distinction between ref
and reactive
is that while reactive
quietly adds some magic to an object, ref
requires you to use the value
property:
const reactiveObj = reactive({ hello: 'world' });
reactiveObj.hello = 'new world';
const refString = ref('world');
refString.value = 'new world';
When you see something.value
and you’re already familiar with how ref
works, it’s easy to understand at a glance that this is a reactive value. With a reactive object, this is not necessarily as clear.
// Is this going to update reactively?
// It's impossible to know just from looking at this line
someObject.property = 'New Value';
// Ah, this is likely a ref
someRef.value = 'New Value';
But here are some caveats:
- If you don’t already understand how
ref
works, seeing.value
means nothing to you. In fact, for someone new to the Composition API,reactive
is a much more intuitive API. - It’s possible that non-reactive objects have a
value
property. But because this clashes with theref
API I would consider this an anti-pattern, whether or not you like usingref
.
This is actually the difference that this entire debate hinges on — but we’ll get to that later.
The important thing to remember for now is that using either ref
or reactive
requires us to access a property.
Tooling and Syntax Sugar
The main disadvantage of ref
here is that we have to write out these .value
accessors all over the place. It can get quite tedious!
Fortunately, we have some extra tools that can help us mitigate this problem:
- Template unwrapping
- Watcher unwrapping
- Volar
In many places Vue does this unwrapping of the ref
for us, so we don’t even need to add .value
. In the template we simply use the name of the ref
:
<template>
<div>{{ myRef }}</div>
</template>
<script setup>
const myRef = ref('Please put this on the screen');
</script>
And when using a watcher we specify the dependencies we want to be tracked, we can use a ref
directly:
import { watch, ref } from 'vue';
const myRef = ref('This might change!');
// Vue automatically unwraps this ref for us
watch(myRef, (newValue) => console.log(newValue));
Lastly, the Volar VS Code extension will autocomplete refs for us, adding in that .value
wherever it’s needed. You can enable this in the settings under Volar: Auto Complete Refs
:
You can also enable it through the JSON settings:
"volar.autoCompleteRefs": true
It is disabled by default to keep the CPU usage down.
ref uses reactive internally
Here’s something interesting you may not have realized.
When you use an object (including Arrays, Dates, etc.) with ref
, it’s actually calling reactive
under the hood.
Anything that isn’t an object — a string, a number, a boolean value — and ref
uses its own logic.
You can see it working in these two lines:
-
Line 1: Creating a
ref
involves callingtoReactive
to get the internal value -
Line 2:
toReactive
only callsreactive
if the passed value is an object
// Ref uses reactive for non-primitive values
// These two statements are approximately the same
ref({}) ~= ref(reactive({}))
Reassigning Values
Vue developers for years have been tripped up by how reactivity works when reassigning values, especially with objects and arrays:
// You got a new array, awesome!
// ...but does it properly update your app?
myReactiveArray = [1, 2, 3];
This was a big issue with Vue 2 because of how the reactivity system worked. Vue 3 has mostly solved this, but we’re still dealing with this issue when it comes to reactive
versus ref
.
You see, reactive
values cannot be reassigned how you’d expect:
const myReactiveArray = reactive([1, 2, 3]);
watchEffect(() => console.log(myReactiveArray));
// "[1, 2, 3]"
myReactiveArray = [4, 5, 6];
// The watcher never fires
// We've replaced it with an entirely new, non-reactive object
This is because the reference to the previous object is overwritten by the reference to the new object. We don’t keep that reference around anywhere.
The proxy-based reactivity system only works when we access properties on an object.
I’m going to repeat that because it’s such an important piece of the reactivity puzzle.
Reassigning values will not trigger the reactivity system. You must modify a property on an existing object.
This also applies to refs, but this is made a little easier because of the standard .value
property that each ref
has:
const myReactiveArray = ref([1, 2, 3]);
watchEffect(() => console.log(myReactiveArray.value));
// "[1, 2, 3]"
myReactiveArray.value = [4, 5, 6];
// "[4, 5, 6]"
Both ref
and reactive
are required to access a property to keep things reactive, so no real difference there.
But, where this is the expected way of using a ref
, it’s not how you would expect to use reactive
. It’s very easy to incorrectly use reactive
in this way and lose reactivity without realizing what’s happening.
Template Refs
Reassigning values can also cause some issues when using the simplest form of template refs:
<template>
<div>
<h1 ref="heading">This is my page</h1>
</div>
</template>
In this case, we can’t use a reactive
object at all:
const heading = reactive(null);
watchEffect(() => console.log(heading));
// "null"
When the component is first instantiated, this will log out null
, because heading
has no value yet. But when the component is mounted and our h1
is created, it will not trigger. The heading
object becomes a new object, and our watcher loses track of it. The reference to the previous reactive object is overwritten.
We need to use a ref
here:
const heading = ref(null);
watchEffect(() => console.log(heading.value));
// "null"
This time, when the component is mounted it will log out the element. This is because only a ref
can be reassigned in this way.
It is possible to use reactive
in this scenario, but it requires a bit of extra syntax using function refs:
<template>
<div>
<h1 :ref="(el) => { heading.element = el }">This is my page</h1>
</div>
</template>
Then our script would be written as so, using the el
property on our reactive object:
const heading = reactive({ el: null });
watchEffect(() => console.log(heading.el));
// "null"
Alex Vipond wrote a fantastic book on using the function ref pattern to create highly reusable components in Vue (something I know quite a bit about). It’s eye-opening, and I’ve learned a ton from this book, so do yourself a favour and grab it here: Rethinking Reusability in Vue
Destructuring Values
Destructuring a value from a reactive
object will break reactivity, since the reactivity comes from the object itself and not the property you’re grabbing:
const myObj = reactive({ prop1: 'hello', prop2: 'world' });
const { prop1 } = myObj;
// prop1 is just a plain String here
You must use toRefs
to convert all of the properties of the object into refs first, and then you can destructure without issues. This is because the reactivity is inherent to the ref
that you’re grabbing:
const myObj = reactive({ prop1: 'hello', prop2: 'world' });
const { prop1 } = toRefs(myObj);
// Now prop1 is a ref, maintaining reactivity
Using toRefs
in this way lets us destructure our props when using script setup
without losing reactivity:
const { prop1, prop2 } = toRefs(defineProps({
prop1: {
type: String,
required: true,
},
prop2: {
type: String,
default: 'World',
},
}));
Composing ref and reactive
One interesting pattern is combining ref
and reactive
together.
We can take a bunch of refs and group them together inside of a reactive
object:
const lettuce = ref(true);
const burger = reactive({
// The ref becomes a property of the reactive object
lettuce,
});
// We can watch the reactive object
watchEffect(() => console.log(burger.lettuce));
// We can also watch the ref directly
watch(lettuce, () => console.log("lettuce has changed"));
setTimeout(() => {
// Updating the ref directly will trigger both watchers
// This will log: `false`, 'lettuce has changed'
lettuce.value = false;
}, 500);
We’re able to use the reactive
object as we’d expect, but we can also reactively update the underlying refs even without accessing the reactive
object we’ve created. However you access the underlying properties, they reactively update everything else that’s “hooked up” to it.
I’m not sure this pattern is better than simply putting a bunch of refs in a plain JS object, but it’s there if you need it.
Organizing State with Ref and Reactive
One of the best uses for reactive
is to manage state.
With reactive
objects we can organize our state into objects instead of having a bunch of refs floating around:
// Just a bunch a refs :/
const firstName = ref('Michael');
const lastName = ref('Thiessen');
const website = ref('michaelnthiessen.com');
const twitter = ref('@MichaelThiessen');
const michael = reactive({
firstName: 'Michael',
lastName: 'Thiessen',
website: 'michaelnthiessen.com',
twitter: '@MichaelThiessen',
});
Passing around a single object instead of lots of refs is much easier, and helps to keep our code organized.
There’s also the added benefit that it’s much more readable. When someone new comes to read this code, they know immediately that all of the values inside of a single reactive
object must be related somehow — otherwise, why would they be together?
With a bunch a refs it’s much less clear as to how things are related and how they might work together (or not).
However, an even better solution for grouping related pieces of reactive state might be to create a simple composable instead:
// Similar to defining a reactive object
const michael = usePerson({
firstName: 'Michael',
lastName: 'Thiessen',
website: 'michaelnthiessen.com',
twitter: '@MichaelThiessen',
});
// We usually return refs from composables, so we can destructure here
const { twitter } = michael;
This gives us the benefits of both worlds.
Not only can we group our state together, but it’s even more explicit that these are things that go together. And since we’re returning an object of refs from our composable (you’re doing that, right?) we can use each piece of state individually if we want.
We have the added benefit that we can co-locate methods with our composable, too. So state changes and other business logic can be centralized and easier to manage.
Of course, this may be a little more than what you need, in which case using reactive
is perfectly fine. You may also find yourself wondering, “why not just use Pinia for this?”, and you’d certainly have a valid point.
The point is this:
Using reactive
gives us another great option for organizing our state.
Wrapping Non-Reactive Libraries and Objects
In talking with Eduardo about this debate, he mentioned that the only time he uses reactive
is for wrapping collections (besides arrays):
const set = reactive(new Set());
set.add('hello');
set.add('there');
set.add('hello');
setTimeout(() => {
set.add('another one');
}, 2000);
Because Vue’s reactivity system uses proxies, this is a really easy way to take an existing object and spice it up with some reactivity.
You can, of course, apply this to any other libraries that aren’t reactive. Though you may need to watch out for edge cases here and there.
Refactoring from Options API to Composition API
It also appears that reactive
is really useful when refactoring a component to use the Composition API:
Transition from Vue2 is much easier if you go with reactive, especially if you have many options to update. You just copy and paste them and it works, yet if I have to choose - ref is the way :)
— Plamen Zdravkov (@pa4ozdravkov) January 12, 2023
I haven’t tried this myself yet, but it does make sense. We don’t have anything like ref
in the Options API, but reactive
works very similarly to reactive properties inside of the data
field.
Here, we have a simple component that updates a field in component state using the Options API:
// Options API
export default {
data() {
username: 'Michael',
access: 'superuser',
favouriteColour: 'blue',
},
methods: {
updateUsername(username) {
this.username = username;
},
}
};
The simplest way to get this working using the Composition API is to copy and paste everything over using reactive
:
// Composition API
setup() {
// Copy from data()
const state = reactive({
username: 'Michael',
access: 'superuser',
favouriteColour: 'blue',
});
// Copy from methods
updateUsername(username) {
state.username = username;
}
// Use toRefs so we can access values directly
return {
updateUsername,
...toRefs(state),
}
}
We also need to make sure we change this
→ state
when accessing reactive values, and remove it entirely if we need to access updateUsername
.
Now that it’s working, it’s much easier to continue refactoring using ref
if you want to. But the benefit of this approach is that it’s straightforward (possibly simple enough to automate with a codemod or something similar?).
They’re just different
After going through all of these examples it should be pretty clear that if we really had to, we could write perfectly fine Vue code with just ref
or just reactive
.
They’re equally capable — they’re just different.
Keep this in mind as we explore the debate between ref
and reactive
.
Keep reading Act 2 and Act 3 of this article on my blog.
Posted on February 22, 2023
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.