loading...

Consider Vue Composition API to improve the code quality

chenxeed profile image Albert Mulia Shintra ・7 min read

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!

Posted on by:

chenxeed profile

Albert Mulia Shintra

@chenxeed

Web Developer with the passion of solving problem and clean solution.

Discussion

markdown guide
 

And that's all of my insight for you to consider using the Vue Composition API!

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!