DEV Community

Allan N Jeremy for This Dot

Posted on

How to omit `.value` in refs (Vue 3 Composition API)

How to omit .value in refs (Vue 3 Composition API)

A technical article that expounds on how we can omit using .value in VueJS ref creating APIs by converting them into reactive variables using macros.

Introduction

When Vue 3 first came around, it introduced the Composition API. The API allowed for greater code re-usability as well as 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 behaviour 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 on 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);
Enter fullscreen mode Exit fullscreen mode

This means that when the value of isReading changes, Vue knows about it and it can keep track of the changes. This means that 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>
Enter fullscreen mode Exit fullscreen mode

Using refs

That's all fine and dandy, but when you want to access or modify the value of the refs in the script, then 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
Enter fullscreen mode Exit fullscreen mode

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

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

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

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

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

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

The above code will compile to:

const __temp = getDefaultReader(),
  name = toRef(__temp, "name");
timeOnPage = toRef(__temp, "timeOnPage");
Enter fullscreen mode Exit fullscreen mode

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

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

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

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

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

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

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

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

return {
  x: x.value,
  y: y.value,
};
Enter fullscreen mode Exit fullscreen mode

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

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

compiles to:

setup(props) {
  const __props_count = toRef(props, 'count')
  passAsRef(__props_count)
}
Enter fullscreen mode Exit fullscreen mode

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

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 cleanup your code base 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(e)!


This Dot Labs is a development consultancy focused on providing staff augmentation, architectural guidance, and consulting to companies.

We help implement and teach modern web best practices with technologies such as React, Angular, Vue, Web Components, GraphQL, Node, and more.

Discussion (1)

Collapse
mreduar profile image
Eduar Bastidas • Edited on

How cool, I didn't know this. Is there anything against using $ref() instead of ref()?

Edit:
I found a problem and it is that the editor says that there is an error because it is trying to modify a constant.

const isReading = ref(true);
isReading = false; //<- Error, is constant.
Enter fullscreen mode Exit fullscreen mode