Ref vs. Reactive — Which is Best?

Michael Thiessen - Feb 22 '23 - - Dev Community

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';
Enter fullscreen mode Exit fullscreen mode

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';
Enter fullscreen mode Exit fullscreen mode

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>
Enter fullscreen mode Exit fullscreen mode
<script setup>
const myRef = ref('Please put this on the screen');
</script>
Enter fullscreen mode Exit fullscreen mode

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));
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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({}))
Enter fullscreen mode Exit fullscreen mode

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];
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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]"
Enter fullscreen mode Exit fullscreen mode

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>
Enter fullscreen mode Exit fullscreen mode

In this case, we can’t use a reactive object at all:

const heading = reactive(null);
watchEffect(() => console.log(heading));
// "null"
Enter fullscreen mode Exit fullscreen mode

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"
Enter fullscreen mode Exit fullscreen mode

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>
Enter fullscreen mode Exit fullscreen mode

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"
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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',
  },
}));
Enter fullscreen mode Exit fullscreen mode

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);
Enter fullscreen mode Exit fullscreen mode

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');
Enter fullscreen mode Exit fullscreen mode
const michael = reactive({
  firstName: 'Michael',
  lastName: 'Thiessen',
  website: 'michaelnthiessen.com',
  twitter: '@MichaelThiessen',
});
Enter fullscreen mode Exit fullscreen mode

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;
Enter fullscreen mode Exit fullscreen mode

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);
Enter fullscreen mode Exit fullscreen mode

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;
    },
  }
};
Enter fullscreen mode Exit fullscreen mode

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),
  }
}
Enter fullscreen mode Exit fullscreen mode

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.

Keep reading Act 2 and Act 3 of this article on my blog.

. . . . . . . . . . . . . . .