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:
ref
and reactive
— First, we’ll go through all of the ways that ref
and reactive
are different. I’ll try to avoid giving any judgment at this point so you can see all the ways they’re different.ref
vs reactive
debate — Next, I’ll lay out the main arguments for ref
and for reactive
, giving the pros and cons of each. At this point you will be able to make your own well-informed decision.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!
But first, a quick summary of my strategy for choosing.
With roughly increasing levels of complexity:
ref
everywherereactive
if you need toreactive
where you want to “reactify” some other JS object like a Map
or Set
.shallowRef
and other more use-case-specific ref functions for any necessary edge cases.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.
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 linesomeObject.property = 'New Value';// Ah, this is likely a refsomeRef.value = 'New Value';
But here are some caveats:
ref
works, seeing .value
means nothing to you. In fact, for someone new to the Composition API, reactive
is a much more intuitive API.value
property. But because this clashes with the ref
API I would consider this an anti-pattern, whether or not you like using ref
.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.
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:
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 uswatch(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.
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:
ref
involves calling toReactive
to get the internal valuetoReactive
only calls reactive
if the passed value is an object
// Ref uses reactive for non-primitive values// These two statements are approximately the sameref({}) ~= ref(reactive({}))
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.
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 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',},}));
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 objectlettuce,});// We can watch the reactive objectwatchEffect(() => console.log(burger.lettuce));// We can also watch the ref directlywatch(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.
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 objectconst michael = usePerson({firstName: 'Michael',lastName: 'Thiessen',website: 'michaelnthiessen.com',twitter: '@MichaelThiessen',});// We usually return refs from composables, so we can destructure hereconst { 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.
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.
It also appears that reactive
is really useful when refactoring a component to use the Composition API:
<script>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 APIexport 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 APIsetup() {// Copy from data()const state = reactive({username: 'Michael',access: 'superuser',favouriteColour: 'blue',});// Copy from methodsupdateUsername(username) {state.username = username;}// Use toRefs so we can access values directlyreturn {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?).
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
.
Before I get into this, I do need to point at that these are both perfectly valid and useful ways to build applications.
Both ref
and reactive
are useful tools, and the wonderful thing about having lots of tools is that we all get to make our own choices about which we want to use.
The debate centres on two main points:
.value
with ref
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 linesomeObject.property = 'New Value';// Ah, this is likely a refsomeRef.value = 'New Value';
But here are some caveats:
ref
works, seeing .value
means nothing to you. In fact, for someone new to the Composition API, reactive
can be a much more intuitive API.value
property. But because this clashes with the ref
API I would consider this an anti-pattern, whether or not you like using ref
.This is where it matters for our debate.
Those who prefer ref
argue that it’s “obvious” what is reactive at a glance, while those who prefer reactive
don’t think it’s quite so obvious.
In fact, one of the reasons that devs prefer to use reactive
is precisely because this .value
syntax isn’t immediately obvious to them.
So if we can’t use this to determine a “winner”, maybe we can use consistency to help us.
One thing that we all agree on is that consistency and simplicity are best.
This theme of consistency plays out in two layers.
First, instead of agonizing over every single line of code in order to determine which tool to use, it’s much easier to stick with one tool, only switching when absolutely necessary.
We want consistency in our actions, since it’s much easier to keep reaching for the same tool over and over again.
But which tool should that be?
This is the second layer of consistency — the consistency of the tools themselves.
Why ref
is more consistent
The argument for using ref
is that it can be used everywhere, while reactive
cannot (without jumping through some hoops). If we picked reactive
as our tool, we’d still have to use ref
in a lot of places, which doesn’t help us achieve consistency.
Instead, picking ref
means that we simply use that everywhere, and almost entirely ignore reactive
. This makes our code far more consistent.
Why reactive
is more consistent
But the argument in favour of reactive
also hinges on consistency.
This argument is that the behaviour of ref
can be quite inconsistent, changing based on the context. Sometimes you need to use .value
, sometimes you don’t. This lack of consistency is confusing and makes it harder to work with.
Instead, picking reactive
means that we have a tool that works in a consistent way, regardless of the situation.
By this point you have probably guessed that I’m not going to tell you which is best, and then lay out a step-by-step proof that leaves you with no doubt.
A bullet-proof answer is what I was hoping to do when I set out to write this article. But the more I dug into this issue, the less certain I became of my own opinion.
Ultimately, you need to make your own choice here.
As we saw in Act 1, you can use ref
or reactive
in any situation. Perhaps a few years ago when Vue 3 first came out there were better arguments for or against. But since then, the tooling and framework have smoothed over most of those rough edges.
So just pick whichever one you like and don’t worry about it.
Just be consistent with how you write your code.
Finally, it’s time for me to share my own stance on the issue.
I’ve gone so far down the rabbit hole on this one, questioning everything, that I can’t really say I have any logical reasons for choosing one over the other.
But I prefer to use ref
over reactive
.
Why?
As much as I hate to admit, it just “feels” right to me. Dealing with atomic units of reactivity makes more sense to me than moving objects around.
Or maybe I’m just more familiar with refs and less comfortable using reactive.
Who knows?
I'm including this here again so you don't have to scroll to the top.
Here’s a simplified system for how I think about using ref
and reactive
in my own code. I’m sure this will change over time, and it certainly isn’t the “right” or only valid approach.
With roughly increasing levels of complexity:
ref
everywherereactive
if you need toreactive
where you want to “reactify” some other JS object like a Map
or Set
.shallowRef
and other more use-case-specific ref functions for any necessary edge cases.I’ve given you plenty of reasons for why simply defaulting to ref
is the best approach.
Perhaps you agree with me, or perhaps you find some of my arguments a little weak.
But I’m not alone in my thinking. It seems that many in the Vue community are supporting the idea of using refs where possible.
I want to say “most” of the community feels this way, but I haven’t done any real surveys, and don’t have any real data to back up a claim that strong.
However, here are some examples from around the community.
Eduardo (creator of Pinia and so much more) says this:
"Personally I only use reactive for collections besides arrays. Map, set, etc. Everything else I just use refs and I think the main downside of having to use value goes away with the ref syntax sugar."
Daniel Roe, who leads the Nuxt team, says this about ref vs. reactive:
"I think ref is a reasonable default behaviour, but I would probably always use both ref (for individual values) + reactive (for conceptual units of state)."
Markus Oberlehner also praises the idea of using a consistent strategy above all else. He likes that ref
can be used everywhere, so that’s his choice:
"ref()
can be used for every occasion, reactive()
can’t. I much prefer consistency over a minor annoyance."
"I’ve found myself using ref
everywhere in my Vue 3 projects"
Anthony from VueJSDevelopers echoes my exact suggestion as well. He says to stick with either ref
or reactive
, but he normally uses ref
:
"My personal opinion is that the best option is to just use one or the other! This is because I prefer to have one consistent pattern for reactive data in a code base, even if I occasionally miss out on the convenience that the two different methods provide."
"I normally use ref
as I find it more flexible."
Jason Yu was one of the first to write about the differences between ref
and reactive
, and the first to suggest that maybe we should just be sticking with ref
: →
"I thought reactive()
would be the api that everyone will end up using as it doesn't require the need to do .value
. Surprisingly, after building a few projects with the composition api, not once have I used reactive()
so far!"
We also have Bartosz Salwiczek suggesting that we should use ref
instead of reactive
:
"In my opinion, you should just blindly choose ref()
over reactive()
. The code would be more consistent and you will not need to think about which one to use — coding would be slightly faster."
Dan Vega shares a slightly more nuanced opinion that we’ve already discussed in this article. We should default to ref
, but then use reactive
when you need to group things together. He doesn’t have a direct quote comparing the two, but his article suggests this approach.
I also tried to find those who suggested the opposite, in order to make sure I wasn’t missing anything. Either that reactive
by default was a better approach, or something else other than ref
by default.
The only opinion I could find was that of Austin Gil. He doesn’t like that ref
can be inconsistent, and how that can make it more difficult for new developers:
"Never liked the inconsistency of when ref needs .value
and when it automatically handles it. Also not good for new folks.
I can definitely agree that ref
can be trickier to learn, as we’ve already discussed in this article.
This is your decision to make, but I definitely recommend defaulting to ref
in most cases.
Both are useful and necessary tools — if they weren’t, they wouldn’t be included in the framework.
If you’re not sure, most of the Vue community has adopted the ref
-first approach, so you won’t go wrong there.
However, you may agree with Austin and the Gil Interpretation, preferring to use reactive
— and that’s totally fine. Software development is more about choosing the tools that work for you, and less about some holy “best practices”.