Ref vs. Reactive — Which is Best?

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 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.
  • Act 2: The 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.
  • 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:

  1. Start with using ref everywhere
  2. Group related things with reactive if you need to
  3. Take related state — and the methods that operate on them — and create a composable for them (or better yet, create a store in Pinia)
  4. Use reactive where you want to “reactify” some other JS object like a Map or Set.
  5. 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:

  1. 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.
  2. It’s possible that non-reactive objects have a 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.

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:

  1. Template unwrapping
  2. Watcher unwrapping
  3. 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:

Enabling .value auto-complete in Volar

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:

  1. Line 1: Creating a ref involves calling toReactive to get the internal value
  2. Line 2: toReactive only calls reactive 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:

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 thisstate 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.

Act 2: The ref vs reactive debate

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:

  1. The use of .value with ref
  2. Consistency

1. Using .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 line
someObject.property = 'New Value';

// Ah, this is likely a ref
someRef.value = 'New Value';

But here are some caveats:

  1. 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 can be a much more intuitive API.
  2. It’s possible that non-reactive objects have a 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.

2. Consistency

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.

So, which is best?

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.

Act 3: Why I prefer ref

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?

My highly opinionated and simple strategy

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:

  1. Start with using ref everywhere
  2. Group related things with reactive if you need to
  3. Take related state — and the methods that operate on them — and create a composable for them (or better yet, create a store in Pinia)
  4. Use reactive where you want to “reactify” some other JS object like a Map or Set.
  5. Use shallowRef and other more use-case-specific ref functions for any necessary edge cases.

What the Vue community has to say

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."

Matt from LearnVue says:

"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.

So, should you use ref or reactive?

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”.

🎉 Get 30% off Vue Tips Collection!
Get it now!