
Vue 3 introduced the Composition API to provide a better way to collocate code related to the same logical concern. In this article, I want to tell you why I love this new way of writing Vue components.
First, I will show you how you can build components using Vue 2, and then I will show you the same component implemented using Composition API. I'll explain some of the Composition API basics and why I prefer Composition API for building components.
For this article, I created a Stackblitz Vue 3 demo application which includes all the components that I'll showcase in this article:
The source code is also available on GitHub.
First, let's look at how we build components in Vue 2 without the Composition API.
In Vue 2 we build components using the Options API by filling (option) properties like methods, data, computed, etc. An example component could look like this:
<template>
<div>...</div>
</template>
<script>
data () {
return {
// Properties for data, filtering, sorting and paging
}
},
methods: {
// Methods for data, filtering, sorting and paging
},
computed: {
// Values for data, filtering, sorting and paging
}
</script>
As you can see, Options API has a significant drawback: The logical concerns (filtering, sorting, etc.) are not grouped but split between the different options of the Options API. Such fragmentation is what makes it challenging to understand and maintain complex Vue components.
Let's start by looking at CounterOptionsApi.vue, the Options API counter component:
<template>
<div>
<h2>Counter Options API</h2>
<p>Count: {{ count }}</p>
<p>2^Count: {{ countPow }}</p>
<button @click="increment()">Increase Count</button>
<button @click="incrementBy(5)">Increase Count by 5</button>
<button @click="decrement()">Decrease Count</button>
</div>
</template>
<script>
export default {
props: {
initialValue: {
type: Number,
default: 0,
},
},
emits: ['counter-update'],
data: function () {
return {
count: this.initialValue,
}
},
watch: {
count: function (newCount) {
this.$emit('counter-update', newCount)
},
},
computed: {
countPow: function () {
return this.count * this.count
},
},
methods: {
increment() {
this.count++
},
decrement() {
this.count--
},
incrementBy(count) {
this.count += count
},
},
mounted: function () {
console.log('Options API counter mounted')
},
}
</script>
This simple counter component includes multiple essential Vue functionalities:
count data property that uses the initialValue property as its initial value.countPow as computed property which calculates the count value to the power of two.counter-update event if the count value has changed.count value.console.log message that is written if the mounted lifecycle hook was triggered.If you are not familiar with the Vue 2 features mentioned above, you should first read the official Vue 2 documentation before you continue reading this article.
Since Vue 3 we can additionally use Composition API to build Vue components.
In my demo application I use the same template for all Vue components, so let's focus on the <script> part of the CounterCompositionApi.vue component:
<script lang="ts">
import { ref, onMounted, computed, watch } from 'vue'
export default {
props: {
initialValue: {
type: Number,
default: 0,
},
},
emits: ['counter-update'],
setup(props, context) {
const count = ref(props.initialValue)
const increment = () => {
count.value += 1
}
const decrement = () => {
count.value -= 1
}
const incrementBy = (value: number) => {
count.value += value
}
const countPow = computed(() => count.value * count.value)
watch(count, (value) => {
context.emit('counter-update', value)
})
onMounted(() => console.log('Composition API counter mounted'))
return {
count,
increment,
decrement,
incrementBy,
countPow,
}
},
}
</script>
Let's analyze this code:
The entry point for all Composition API components is the new setup method. It is executed before the component is created and once the props are resolved. The function returns an object, and all of its properties are exposed to the rest of the component.
this inside setup as it won't refer to the component instance. setup is called before data properties, computed properties, or methods are resolved, so that they won't be available within setup.But we need to be careful: The variables we return from the setup method are, by default, not reactive.
We can use the reactive method to create a reactive state from a JavaScript object. Alternatively, we can use ref to make a standalone primitive value (for example, a string, number, or boolean) reactive:
import { reactive, ref } from 'vue'
const state = reactive({
count: 0,
})
console.log(state.count) // 0
const count = ref(0)
console.log(count.value) // 0
The ref object contains only one property named value, which can access the property value.
Vue 3 also provides different new methods like computed, watch, or onMounted that we can use in our setup method to implement the same logic we used in the Options API component.
But we can further improve our Vue component code by extracting the counter logic to a standalone composition function (useCounter):
import { ref, computed, onMounted } from 'vue'
export default function useCounter(initialValue: number) {
const count = ref(initialValue)
const increment = () => {
count.value += 1
}
const decrement = () => {
count.value -= 1
}
const incrementBy = (value: number) => {
count.value += value
}
const countPow = computed(() => count.value * count.value)
onMounted(() => console.log('useCounter mounted'))
return {
count,
countPow,
increment,
decrement,
incrementBy,
}
}
This drastically reduces the code in our CounterCompositionApiv2.vue component and additionally allows us to use the counter functionality in any other component:
<script lang="ts">
import { watch } from 'vue'
import useCounter from '../composables/useCounter'
export default {
props: {
initialValue: {
type: Number,
default: 0,
},
},
emits: ['counter-update'],
setup(props, context) {
const { count, increment, countPow, decrement, incrementBy } = useCounter(props.initialValue)
watch(count, (value) => {
context.emit('counter-update', value)
})
return { count, countPow, increment, decrement, incrementBy }
},
}
</script>
In Vue 2, Mixins were mainly used to share code between components. But they have a few issues:
Composition API addresses all of these issues.
Vue 3.2 allows us to get rid of the setup method by providing the <script setup>. It's the recommended syntax if you use Composition API and SFC (Single File Component).
This syntactic sugar provides several advantages over the normal <script> syntax:
CounterCompositionApiv3.vue demonstrates our counter example using the <script setup> syntax:
<script setup lang="ts">
import { defineProps, defineEmits, watch } from 'vue'
import useCounter from '../composables/useCounter'
interface Props {
initialValue?: number
}
const props = withDefaults(defineProps<Props>(), {
initialValue: 0,
})
const emit = defineEmits(['counter-update'])
const { count, countPow, increment, decrement, incrementBy } = useCounter(props.initialValue)
watch(count, (value) => {
emit('counter-update', value)
})
</script>
If you can’t migrate to Vue 3 today, then you can still use the Composition API already. You can do this by installing the official Composition API Vue 2 Plugin.
You've seen the same counter component created in Vue 2 using Options API and created in Vue 3 using Composition API.
Let's summarize all the things I love about Composition API:
this keyword, so we can use arrow functions and use functional programming.setup method, making things more readable.The following image shows a large component where colors group its logical concerns and compares Options API versus Composition API:

You can see that Composition API groups logical concerns, resulting in better maintainable code, especially for larger and complex components.
I can understand that many developers still prefer Options API as it is easier to teach people who are new to the framework and have JavaScript knowledge. But I would recommend that you use Composition API for complex applications that require a lot of domains and functionality. Additionally, Options API does not work very well with TypeScript, which is, in my opinion, also a must-have for complex applications.
If you liked this article, follow me on Twitter to get notified about new blog posts and more content from me.
Alternatively (or additionally), you can also subscribe to my newsletter.
