Skip to content

Vue 3 Composition API - "ref" and "reactive"

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.

Introduction

With the release of Vue 3, developers now have access to the Composition API, a new way to write Vue components. This API allows features to be grouped together logically, rather than having to organize your single-file components by function. Using the Composition API can lead to more readable code, and gives the developer more flexibility when developing their applications.

The Composition API provides two different ways to store data locally in the component - “ref” and “reactive”. These two methods serve a similar role as the “data” function in the traditional Options API that is commonly used in Vue applications today. In this article, we will explore both of these new methods, and when to use them in your own application.

If you want to read more about the Composition API, you can read this article by Bilal Haidar for an overview.

You can view the code examples below in a working app on CodeSandbox by clicking this link.

Options API - data

If you have used Vue in the past, you have probably seen how different parts of your components are divided by function. Each single-file component has access to a number of attributes: data, computed, methods, lifecycle hooks, and so on. This is referred to as the "Options API". Below is an example application implementing local state with the Options API:

<template>
  <button @click="count++">count is: {{ count }}</button>
</template>

<script lang="ts">
import { defineComponent } from "vue";

export default defineComponent({
  name: "App",
  data() {
    return {
      count: 0
    };
  }
});
</script>

Composition API - ref

Now let's look at the same example as above, using the Composition API. First, we'll take a look at ref. From the Vue 3 documentation:

ref takes an inner value and returns a reactive and mutable ref object. The ref object has a single property .value that points to the inner value.

Below is our example code using ref:

<template>
  <button @click="count++">count is: {{ count }}</button>
</template>

<script lang="ts">
import { ref, defineComponent } from "vue";

export default defineComponent({
  name: "App",
  setup: () => {
    const count = ref(0);

    return { count };
  },
});
</script>

Let's take a closer look. What changed between these two examples?

  1. Rather than using the data function to add local state, we are using the setup function. This new function replaces data, beforeCreate, and created, and is the place for utilizing the Composition API.
  2. Like with data, setup returns an object. The contents of that object is any variable that needs to be accessible from the template. Since we want the count to be available in the template, we include it in the return object.

Before we continue to reactive, we should make one more change. Let's move the click event into its own method, rather than performing the action in the template.

<template>
  <button @click="increaseCount">count is: {{ count }}</button>
</template>

<script lang="ts">
import { ref, defineComponent } from "vue";

export default defineComponent({
  name: "App",
  setup: () => {
    const count = ref(0);

    const increaseCount = () => {
      count.value++;
    }

    return { count, increaseCount };
  },
});
</script>

Unlike in the Options API, methods are simply functions. We don't need any special syntax or Composition API methods to make them work. However, just like the count variable above, we do need to return the method from the setup function in order for it to be available in the template.

Notice that in the template, we used count++ to increase the value, but in the setup function, we use count.value. This is because in the setup function, count has a type of Ref<number>, where in the template the internal value is directly available.

Composition API - reactive

Now let's try out the reactive method. From the Vue 3 docs:

reactive returns a reactive copy of the object. The reactive conversion is "deep"—it affects all nested properties. In the ES2015 Proxy based implementation, the returned proxy is not equal to the original object. It is recommended to work exclusively with the reactive proxy, and avoid relying on the original object.

Here is our code example using reactive instead of ref:

<template>
  <button @click="increaseCount">count is: {{ state.count }}</button>
</template>

<script lang="ts">
import { reactive, defineComponent } from "vue";

export default defineComponent({
  name: "App",
  setup: () => {
    const state = reactive({
      count: 0
    });

    const increaseCount = () => {
      state.count++;
    }

    return { state, increaseCount };
  },
});
</script>

Within the setup function, we can use the return value of reactive very similarly to how we use the data function in the Options API. Because the object is deeply reactive, we can also make changes to it directly. So instead of count.value++, we can simply increment the value with state.value++.

When to use ref and reactive

In general, ref is useful for primitives (string, number, boolean, etc), and reactive is useful for objects and arrays. However, there are some key points to consider:

First, if you pass an object into a ref function call, it will return an object that has been passed through reactive. The below code works perfectly fine:

<template>
  <button @click="increaseCount">count is: {{ state.count }}</button>
</template>

<script lang="ts">
import { ref, defineComponent } from "vue";

export default defineComponent({
  name: "App",
  setup: () => {
    const state = ref({
      count: 0
    });

    const increaseCount = () => {
      state.value.count++;
    }

    return { state, increaseCount };
  },
});
</script>

Second, while the object returned from reactive is deeply reactive (setting any value to the object will trigger a reaction in Vue), you can still accidentally make the values non-reactive. If you try to destructure or spread the values of the object, for example, they will no longer be reactive. The below code does not work as you might expect:

<template>
  <button @click="increaseCount">count is: {{ count }}</button>
</template>

<script lang="ts">
import { reactive, defineComponent } from "vue";

export default defineComponent({
  name: "App",
  setup: () => {
    const state = reactive({
      count: 0
    });

    const increaseCount = () => {
      state.count++;
    }

    return { ...state, increaseCount };
  },
});
</script>

If you want to do something like this, Vue 3 has you covered. There are a number of functions to convert between Refs and their values. In this case, we will use the toRefs function. From the docs:

Converts a reactive object to a plain object where each property of the resulting object is a ref pointing to the corresponding property of the original object.

If we update our above code example to spread the result of toRefs, then everything works as we'd expect:

<template>
  <button @click="increaseCount">count is: {{ count }}</button>
</template>

<script lang="ts">
import { toRefs, reactive, defineComponent } from "vue";

export default defineComponent({
  name: "App",
  setup: () => {
    const state = reactive({
      count: 0
    });

    const increaseCount = () => {
      state.count++;
    }

    return { ...toRefs(state), increaseCount };
  },
});
</script>

There are other ways to combine these two functions, both with setting a value in a reactive object to its own variable with toRef or adding a ref to an object directly. ref and reactive are two parts of the solution, and you will find yourself reaching for each of them as needed.

Conclusion

The Vue 3 Composition API provides a lot of benefit to developers. By allowing features to be grouped together, rather than functions, developers can focus on what they are building, and less on fitting their code into a predetermined structure.

By leveraging the ref and reactive functions to maintain local state, developers have new tools to write more maintainable and readable code. These methods work well together, rather than one or the other, each solving a different problem.

Remember that the Composition API is optional! The existing Options API is not going anywhere, and will continue to work as expected. I would encourage you to try out the Composition API, and see how these new methods can improve your own workflow and applications.

To view the above examples in a working application, click here to view them in CodeSandbox.

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