Skip to content

Vue 3 Composition API: How to Omit `.value` in refs

This article was written over 18 months ago and may contain information that is out of date. Some content may be relevant but please refer to the relevant official documentation or available resources for the latest information.

NOTE: The "reactivity Transform" RFC has been concluded and the team has decided not to go ahead with this. The information in the following post are informative to learn what was being discussed, but will not be usable as this feature is not going to be added within the Vue framework.

In this article, we discuss how to omit .value in VueJS refs, creating APIs by converting them into reactive variables using macros.

Introduction

When Vue 3 first came around, it introduced the Composition API. The API permitted greater code re-usability, and provided a better way to organize Vue JS code. Along with the composition API came the concept of refs. To access the value of a ref, you needed to append .value to the name of the ref variable. To tackle this, the Vue team came up with a solution (Reactivity Transform) that would allow us to create reactive variables without creating refs.

Prerequisites

This article is primarily aimed at intermediate Vue 3 developers, and assumes three things:

The functionality discussed in this article is purely opt-in, and existing behavior is unnafected.

Tools needed

For this to work, you will need to be using vue@^3.2.25 and above. No additional dependencies are required. Vue 3.2.25+ ships an implementation under the package @vue/reactivity-transform. It is also integrated (with its APIs re-exported) in @vue/compiler-sfc, so most userland projects won't need to explicitly install it.

Reactivity in Vue 3

Reactivity refers to the ability to keep track of changes that occur in our applications. One such way to achieve reactivity in Vue 3 is by using refs.

Creating Refs

The syntax for creating a ref would be something along the lines of this:

import { ref } from "vue";

// By wrapping our default value (true) with a ref, we tell Vue to keep track of changes made to it
const isReading = ref(true);

This means that when the value of isReading changes, Vue knows about it, and it can keep track of the changes. This means that the UI is automatically updated whenever the value of isReading changes. In your template file, you would access the reactive value the same way you would access any variable. For example:

<template>
  <h1>{{ isReading ? "Shhh, I'm reading" : "Talk to me" }}</h1>
</template>

Using refs

That's all fine and dandy, but when you want to access or modify the value of the refs in the script, you need to append a .value at the end of it. This is because ref() wraps the actual variable(isReading) in an object that can keep track of any changes made to it.

import { ref } from "vue";

const isReading = ref(true);

// prints an object that represents the ref object that wraps isReading
console.log(isReading);

// This is how you would need to access the value of isReading
console.log(isReading.value); // prints true

Reactivity Transform

Removing the need for .value

The new Vue 3 syntax allows you to use refs without needing to use .value. To make this work, the Vue team implemented Reactivity Transform. This allows us to create reactive variables for every API that creates refs instead of using refs. This means we can use our variables without appending .value everywhere. Reactive variables do not need .value to be accessed while refs need you to append .value.

Previously, we used to write code like this

const isReading = ref(true);

console.log(isReading.value);

which can now be written like this

// Prepending $ to ref makes $ref() a macro that wraps around the original ref()
const isReading = $ref(true);

console.log(isReading); // no need to write

Behind the scenes, Vue will unwrap the $ref() and compile into the original .value syntax we are used to writing. The only difference is that this time you don't have to write isReading.value everywhere. This is particularly useful in areas where the ref created is used in multiple places within a script.

It is also worth noting that every reactivity API that returns refs will have a $-prefixed macro equivalent. These APIs include:

ref -> $ref computed -> $computed shallowRef -> $shallowRef customRef -> $customRef toRef -> $toRef

Do you need to import $ref ?

Since $ref and equivalents are macros, they do not need to be imported. However, if you would like to import them explicitly, you can do so from vue/macros.

import { $ref } from "vue/macros";

Convert an existing ref as reactive variable using $()

In situations where we have a function that returns a ref, the Vue compiler would not be able to know that the function will return a ref ahead of time. In such cases, we can wrap the function call with $() to explicitly convert it into a reactive variable.

function getIsReadingRef() {
  return ref(true);
}

const isReading = $(getIsReadingRef());

Destructuring objects of refs

Previously, if you tried to destructure an object that was a ref, the destructured variables would lose their reactivity.

Let's go with an example ref.

const getDefaultReader = () => ref({ name: "VueJS lover", timeOnPage: 30 });
// Vue will be able to tell when any part of `reader` changes
const reader = ref(getDefaultReader());

// Vue won't be able to tell when the values of `name` and `timeOnpage` change
const { name, timeOnPage } = ref(getDefaultReader());

With Reactivity transform, you can destructure the objects of refs and maintain reactivity. You do so by wrapping the value with a $().

// Vue will now be able to tell when the values of `name` and `timeOnpage` change
const { name, timeOnPage } = $(getDefaultReader());

The above code will compile to:

const __temp = getDefaultReader(),
  name = toRef(__temp, "name");
timeOnPage = toRef(__temp, "timeOnPage");

Reactive props destructuring

This example is from the original Reactivity Transform RFC.

There are two pain points with the current defineProps() usage in <script setup>

  • Similar to .value, you need to always access props as props.x in order to retain reactivity. This means you cannot destructure defineProps because the resulting destructured variables are not reactive and will not update.
  • When using the type-only props declaration, there is no easy way to declare default values for the props. We introduced the withDefaults() API for this exact purpose, but it's still clunky to use.
<script setup lang="ts">
interface Props {
  msg: string;
  count?: number;
  foo?: string;
}

const {
  msg,
  // default value just works
  count = 1,
  // local aliasing also just works
  // here we are aliasing `props.foo` to `bar`
  foo: bar,
} = defineProps<Props>();

watchEffect(() => {
  // will log whenever the props change
  console.log(msg, count, bar);
});
</script>

The above will be combined to the following in runtime.

export default {
  props: {
    msg: { type: String, required: true },
    count: { type: Number, default: 1 },
    foo: String,
  },
  setup(props) {
    watchEffect(() => {
      console.log(props.msg, props.count, props.foo);
    });
  },
};

Using $$() to retain reactivity

To get around reactivity loss in certain scenarios, the $$() macro can be used.

Retaining reactivity when passing refs as function arguments

Consider a situation where you have a function that needs to accept a reactive variable as an argument.

function trackChange(isReading: Ref<boolean>) {
  watch(isReading, (isReading) => {
    console.log("isReading changed!", isReading);
  });
}

let isReading = $ref(true);

// This will not work
trackChange(isReading);

In such a case, reactivity is lost.The reason for this is that the isReading ref is actually unwrapped into isReading.value when being passed in as the argument for trackChange, while trackChange expects an actual ref. The above code compiles to this:

import { ref } from "vue";

let isReading = ref(true);

// This is what is actually happening
trackChange(isReading.value);

To get around this, we can wrap the ref in $$(), which tells the compiler not to append a .value to it.

// This will work
trackChange($$(isReading));

The above example compiles to this:

import { ref } from "vue";

let isReading = ref(true);

// This is what we want - the isReading variable should be passed as a ref
trackChange(isReading);

Retaining reactivity when returning inside function scope

Another scenario where reactivity is lost is when we are returning reactive variables from within a function.

function useMouse() {
  let x = $ref(0);
  let y = $ref(0);

  // listen to mousemove...

  // doesn't work!
  return {
    x,
    y,
  };
}

Similar to the example with passing refs as arguments, the above return statement compiles to:

return {
  x: x.value,
  y: y.value,
};

In order to maintain the reactivity of x and y, we can wrap the entire return statement with the $$() macro.

function useMouse() {
  let x = $ref(0);
  let y = $ref(0);

  // listen to mousemove...

  // This works
  return $$({
    x,
    y,
  });
}

Retaining reactivity on destructured props

$$() works on destructured props since they are reactive variables as well. The compiler will convert it with toRef for efficiency:

const { count } = defineProps<{ count: number }>();

passAsRef($$(count));

compiles to:

setup(props) {
  const __props_count = toRef(props, 'count')
  passAsRef(__props_count)
}

TypeScript & Tooling Integration

Vue will provide typings for these macros (available globally) and all types will work as expected. There are no incompatibilities with standard TypeScript semantics, so the syntax would work with all existing tooling.

This also means the macros can work in any files where valid JS/TS are allowed - not just inside Vue SFCs.

Since the macros are available globally, their types need to be explicitly referenced (e.g. in a env.d.ts file):

/// <reference types="vue/macros-global" />

When explicitly importing the macros from vue/macros, the type will work without declaring the globals.

Conclusion

By taking advantage of the macros added to Vue 3, you can drastically clean up your codebase by getting rid of .value usage. You also get to preserve reactivity within your application when destructuring reactive variables as well as props when using the Composition API and defineProps().

If you'd like to read more on the same, you can do so in the official Vue JS RFC discussion for the feature.

I do hope you find this helpful in reducing your code footprint, and making your general life easier. The next time you think of using .value for your refs, remember that you don't have to. With that, thanks for stopping by!

This Dot is a consultancy dedicated to guiding companies through their modernization and digital transformation journeys. Specializing in replatforming, modernizing, and launching new initiatives, we stand out by taking true ownership of your engineering projects.

We love helping teams with projects that have missed their deadlines or helping keep your strategic digital initiatives on course. Check out our case studies and our clients that trust us with their engineering.

Let's innovate together!

We're ready to be your trusted technical partners in your digital innovation journey.

Whether it's modernization or custom software solutions, our team of experts can guide you through best practices and how to build scalable, performant software that lasts.

Prefer email? hi@thisdot.co