Heya!
I have been working and prototyping with Vue Composition API for a while since the Vue 3 beta released on March. I would like to share some good experience I have while using it, for your reference if you're planning to use the new Vue 3, or migrate from Vue 2. Here we go!
note: the code example are based on the new Vue 3 convention
1. No more this
instance
As a Javascript Developer, we may have to deal with this
variable for quite some scenario due to the JS common behavior of inheriting the object or class instance. One of the common quirks you will face is:
"In most cases, the value of this is determined by how a function is called (runtime binding). It can't be set by assignment during execution, and it may be different each time the function is called." - MDN
You may face the similar situation while writing Vue with the object based properties, since the this
instance is very tight to the concept of Vue inheriting its object properties, and the root prototypes. This is the example of a component called my-counter
, that should increment the count value by clicking the "Add" button, or press the key +
from your keyboard.
<template>
<div>Count: {{ count }}
<button @click="incrementCount">Add</button>
</div>
</template>
<script>
export default {
name: 'my-counter',
data () {
return {
count: 0
}
},
mounted () {
// register keyboard event to listen to the `+` key press
document.addEventListener('keydown', function(e) {
if (e.keyCode === 187) { // 187 is keyCode for `+`
this.incrementCount()
}
})
},
methods: {
incrementCount () {
this.count += 1
}
}
}
</script>
It looks fine and simple. Notice that the this
in the method, it contains the .count
value from the data
we defined before. But also, this
contains more than that. It also contains the Vue root instance, the plugin installed (vuex, router, etc), $attrs
, slots, and more.
Did you see there's a bug in the code above? If yes, good eye! There is an error on pressing the +
key from your keyboard, saying that:
Uncaught TypeError: this.incrementCount is not a function
This is because the callback function of the event listener is bound to the instance of the document
, not the Vue
component. This can be easily solved by changing the function method to arrow based function
, but beginner developer may not realize it earlier, and they have to understand the inheritance concept of JS to get used to this.
Okay, sorry for the long post 🥔 to explain the basic quirk of this
, now let's jump into Composition API!
In the Composition API, it has no reliance to the this
instance. Everything is done in the setup
phase, which consist of creating the data and methods of your component. Here's the example of Composition API based on the my-counter
component above:
<template>
<div>Count: {{ count }}
<button @click="incrementCount">Add</button>
</div>
</template>
<script>
import { reactive, toRefs, onMounted } from 'vue'
export default {
name: 'my-counter',
setup () {
const data = reactive({
count: 0
})
const incrementCount = () => data.count++
onMounted(function () {
document.addEventListener('keydown', function(e) {
if (e.keyCode === 187) { // 187 is keyCode for '+'
incrementCount()
}
})
})
return {
...toRefs(data),
incrementCount
}
}
}
</script>
Let's compare the difference. Before, you rely on the object property data
to register the state count
, and methods
to register the function to increment the count
. The methods
rely on this
instance to access the count
value.
After refactored into the Composition API, the functionality are all wrapped under setup
to initiate the data, create a function to mutate the count, and also attach keyboard event listener. No more quirks on this
value, so either normal or arrow function is not a problem anymore!
2. Better code splitting management
With the Composition API example above, we can see that now we don't have to follow the Vue convention to write the component functionality to separated properties (lifecycle hooks, data, methods, computed, watch
), as everything can be composed as one function in the setup
.
It opens the chance for us to split our code if we want to organize the code better, especially when the component functionality is complicated. We can write all the functionality under the setup
, or we can also create a JS file to scope specific functionality to other file.
Let's take the example from the my-counter
component. What if we want to split the functionality to attach the keyboard event separately?
// keyboard-event.js
import { onMounted } from 'vue'
export function usePlusKey (callbackFn) {
onMounted(function () {
document.addEventListener('keydown', function(e) {
if (e.keyCode === 187) { // 187 is keyCode for '+'
callbackFn()
}
})
})
}
Now, we can import and use this function to the setup
:
import { reactive, toRefs } from 'vue'
import { usePlusKey } from './keyboard-event'
export default {
name: 'my-counter',
setup () {
const data = reactive({
count: 0
})
const incrementCount = () => data.count++
usePlusKey(incrementCount)
return {
...toRefs(data),
incrementCount
}
}
}
You may argue if it's important or not to split the keyboard listener function above, but I hope you get the idea that it's up to you to manage your code and the Composition API give you easier way to handle it. Another advantage that you see above, is that the lifecycle hook of the component can be defined separately!
If you need to handle multiple scenario on mounted, now you can split them. For example:
// my-component.vue
mounted () {
this.initPayment()
this.initTracking()
},
methods: {
initPayment () { /* init payment */ },
initTracking () { /* init tracking */ }
}
With the Composition API:
// my-component/payment.js
export function initPayment () {
onMounted(() => { /* init payment */ })
}
// my-component/tracking.js
export function initTracking () {
onMounted(() => { /* init tracking */ })
}
// my-component.vue
import { initPayment } from './payment'
import { initTracking } from './tracking'
setup () {
initPayment()
initTracking()
}
3. Function Reusability
With the example above, we can see the potential that the function is not only meant for one component only, but can also be used for others!
The reusability concept is similar to mixins. However there's a drawback of mixins, which is explained here. In short, naming collision and implicit dependencies are a "hidden bug" that can bite you when you're using it carelessly.
With the Composition API, these two concern are gone less likely to happen since the composition API function need to explicitly define the value it needs as a function parameter, and the variable name of the return value.
Let's see the example of a mixin of counter functionality:
// mixin/counter.js
const mixinCounter = {
data () {
return {
counter: 0
}
},
methods: {
increment () {
this.counter++
}
}
}
Using this mixin, we have to be considerate that it may overwrite the existing counter
data and increment
methods in the component it installed. This is what it means by "implicit dependencies".
If we convert it to the Composition API:
// compose/counter.js
import { ref } from 'vue'
export function useCounter () {
const counter = ref(0)
const increment = () => counter.value++
return {
counter,
increment
}
}
Using this function, it explicitly return counter
and increment
and let the component setup
to decide what to do with it. If by chance the name counter/increment
is already used or you need to use it multiple times, then we can still fix it by rename the variable like this:
// use default counter and increment name
const { counter, increment } = useCounter()
// since counter and increment already exist,
// rename it to countQty and incrementQty
const { counter: countQty, increment: incrementQty } = useCounter()
Cool! Perhaps one consideration here is, you need some extra time to bike shedding on deciding the new name of the variable 😅.
4. More control of the Typescript interface
Are you using typescript to type your component interface properly? If yes, great!
From the official docs, Vue has provided basic typescript support with Vue.extend
, or using vue-class-component to write the Vue component as a class, leveraging the this
instance to type the data and methods properly.
Refer back to the 1st point if we want to escape the this
quirks and still have strong typing interface, then the Composition API is a good choice.
First, setup
is a pure function that takes the input parameter to replace the needs of using this
to access the component props
and the context attrs
, slots
, and emit
.
Then, all the data and function you wrote in the setup
, is up to you to type it 😍! You can write and type your code without having to abide to the Vue way of defining things like data
, methods
, refs
, computed
and watch
.
Here's the example of a typed Vue component:
// we use Vue.extend in vue v2.x
export default Vue.extend({
data () {
return {
count: 0
}
},
computed: {
multiplyCount () {
return this.count * 2
}
},
methods: {
increment () {
this.count++
}
},
watch: {
count (val) { // `val` type is `any` :(
console.log(val)
}
}
})
In this example, we rely on the Vue.extend
to automatically type the component interface. The this.count
on the computed multiplyCount
and method increment
will have the proper typing from the data
, but the watcher count
will not be typed 😕.
Let's see how it's written in the Composition API:
// in vue 3.x, we use defineComponent
export default defineComponent({
setup () {
const count = ref(0) // typed to number
const multiplyCount = computed(() => count.value * 2 )
const increment = () => count.value++
watch(count, val => console.log(val)) // `val` is typed to number
return {
count,
multiplyCount,
increment
}
}
})
The typing here is more explicit and predictable. You can customize the typing too if you need too, means that you are in control over the interface!
Conclusion
And that's all of my insight for you to consider using the Vue Composition API!
I believe there's a lot more potential in the Composition API, so please share your feedback about your experience or what do you think about it! Any tips to improve will be appreciated too 😍
I would like to highlight as well that the Composition API is not a silver bullet and you don't have to refactor your component to the Composition API if you don't see a benefit of it, or your component is pretty simple.
Thank you and have a great day!
Top comments (3)
Now, with code quality, and possibly launching new codes faster, you convinced me to try composition API.
About maintain old codes is a different thing though. Migrating risks breaking production.
Yes good point! Shouldn't migrate the code if it risk the user experience.
Nice and clear tips! Good article!