Consider Vue Composition API to improve the code quality
Tags: javascript, vue, typescript, codequality
Published: 2020-07-27 14:58:31

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](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/this)

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.

```vue
<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:

```vue
<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?

```js
// 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`:

```js
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:

```js
// my-component.vue
mounted () {
  this.initPayment()
  this.initTracking()
},
methods: {
  initPayment () { /* init payment */ },
  initTracking () { /* init tracking */ }
}
```

With the Composition API:

```js
// 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](https://vuejs.org/v2/guide/mixins.html). However there's a drawback of mixins, which is explained [here](https://css-tricks.com/how-the-vue-composition-api-replaces-vue-mixins/#mixins-are-considered-harmful). 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:

```js
// 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:

```js
// 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:

```js
// 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 :sweat_smile:.

# 4. More control of the Typescript interface

Are you using typescript to type your component interface properly? If yes, great!

From the [official docs](https://vuejs.org/v2/guide/typescript.html#Basic-Usage), Vue has provided basic typescript support with `Vue.extend`, or using [vue-class-component](https://github.com/vuejs/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 :heart_eyes:! 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:

```ts
// 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 :confused:.

Let's see how it's written in the Composition API:

```ts
// 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 :heart_eyes:

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!