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?
- Rather than using the
data
function to add local state, we are using thesetup
function. This new function replacesdata
,beforeCreate
, andcreated
, and is the place for utilizing the Composition API. - 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.