Roland doda
Roland doda's Blog

Roland doda's Blog

Vue 3 Composition API ? Nah, thanks.

Vue 3 Composition API ? Nah, thanks.

Why I switched from using Composition API to Options API for creating components

Roland doda's photo
Roland doda
·Jan 8, 2022·

9 min read

No introduction text and basic explanations, let's dive right in.

This post assumes you already know what ref and reactive are. If you want to learn more about them visit the Docs

They say, ref came to save reactive

reactive has some limitations:

  1. We can't pass a nested property of a reactive variable to a function.
  2. We can't use descructuring.
const state = reactive({ count: 0 })

// the function receives a plain number and
// won't be able to track changes to state.count
callSomeFunction(state.count)

// count is a plain number that is disconnected
// from state.count.
let { count } = state
// does not affect original state
count++

And thus, Vue provides us ref to avoid the limitations of reactive. Vue docs say that by using ref we can avoid the limitations of reactive and gives us the following snippet:

const obj = {
  foo: ref(1),
  bar: ref(2)
}

// the function receives a ref
// it needs to access the value via .value but it
// will retain the reactivity connection
callSomeFunction(obj.foo)

// still reactive
const { foo, bar } = obj

So ref comes as a solution. But does it solves the limitations of reactive ? Why not use the same example we had with reactive but by using ref to have a better comparison ?

// const state = reactive({ count: 0 })
const state = ref({ count: 0 })

// callSomeFunction(state.count)
callSomeFunction(state.value)

// let { count } = state
let { count } = state.value

Hmm, that doesn't work either. So ref has the same limitations. If you pull out a property of that object you will get a non-reactive property.

ref is a smart trick by providing an object that has a value property. While you use the ref as is you are good. If you mess with .value though, you might end up with the limitations of reactive.

How to use ref correctly ? Here is my rule: Use .value of a ref variable only for mutating it. In other cases use the ref variable without accessing .value

const count = ref(0)

passToFunction(count) // ✅
watch(count, (newVal, oldVal) => {}) // ✅
count.value++ // ✅

You might wonder, how about displaying a ref variable in the template ? Shouldn't I access it via .value ?

The answer is no. Vue automatically "unwraps" the ref variable and displays the .value.

<script setup>
import { ref } from 'vue'

const count = ref(0)
</script>

<template>
  {{ count }}
  <button @click="count++">Increase counter</button>
</template>

Updating the ref variable in the template can be done without using .value, as you can see from the example above where we directly do count++.

So to wrap things up, inside script use .value only for mutation. In the template .value is automatically unwrapped, so you don't have to use .value.

Cool so now we are less confused and we know how to use ref. Right ? Not yet.

You have to know that the automatic unwrapping of ref applies only to top-level properties.

<script setup>
  const object = { foo: ref(1) }
</script>

<template>
  {{ object.foo }} <!-- does NOT get unwrapped -->
</template>

Ah, okay, noted! Lots of things to keep in mind in terms of reactivity, especially when using ref but we can live with them.


Is Vue 3 easier compared to Vue 2? 🤔

Now that we understood ref and reactive a bit better it seems that we are good to go and rock! 🤘

But wait, I know that you are still confused, right ? Maybe you have questions like:

  • Is there a best practice when to use ref or reactive or it is that if we stumble upon a limitation of reactive we use ref ?
  • Should we just always use ref ?
  • Should I use both ref and reactive ?

Good questions. Since there are both APIs on the table you can't just use ref. Some say that for primitive types use ref but for the rest, use reactive. But lots of people demonstrate using ref for basically everything. Personally ? I mostly use ref. But I don't have a definitive answer except for the usual one: 🗣️ "It depends on what you do."

I know, I know. Too much confusion. How about the last question ? Is there anything we need to know about using ref and reactive together ?

Get ready for more confusion😁

Say that we have the 2 following variables:

import { ref, reactive } from 'vue'

const count = ref(0)
const state = reactive({ count })

From the example above, here are some things that maybe don't work as you think they should work.

  • state.count can be accessed and mutated directly. No state.count.value needed.
  • If we update count variable then state.count will be updated too 😳.
  • If we update state.count then count will be updated too 😲.
  • So state.count and count.value are in sync. If we update one, the other will get updated too. But not always 🤔. If we re-assign to state.count another ref then the count will not be in sync anymore. 🤯

Oh my god! How deep the rabbit hole goes ? Calm down, let's explain it.

So what happens, is that a ref inside a reactive object is getting unwrapped so no .value is available on the refs inside that object. But there is no unwrapping happening inside a reactive Array or a native collection type like Map or Set.

How about the sync part ? Why does that happen? And why does it stop only when assigning another ref ?

Honestly ? I don't know. That's how it works.

I know, I know. too many things to keep in mind.

Not to mention that instead of ref, reactive we also have shallowRef, shallowReactive, readonly, shallowReadonly, unref, toRefs and many other APIs which deserve their own post.

Wasn't supposed Vue 3 to be easier ? Wasn't easier when we had just data and computed for reactive properties ?


Why not using good not-old Options API ?

It's been more than 4 months using Composition API for defining components, and I have to admit that using Options API is definitely more intuitive, predictable and easier to use.

I know that almost everyone out there says you should go with Composition API because it's better and calls options API an "old way", but that's not true. If you feel that Options API is better for you, stick to it.

After a lot of practice, I am confident when I use Composition API, but when I used Options API in a Vue 2 project again, I thought to myself: "What a wonderful world" 🎶.

For the last month, I switched to using Options API again, and I love that reactivity just works, I don't have to worry about all those APIs and I am more productive. Also debugging the app with Options API just makes sense. No "destructuring" or in general "reactivity loss" problems again.

How about Reusability ? Should we use mixins ? For extracting code, I use Composition API. Also, I believe, (haven't tried it yet) that Composition API makes more sense if you use Typescript.

Options API 🤝 Composition API

For creating components, I use Options API but when it comes to reusing code, I write composables with Composition API and I just use them:

<script setup>
import { useMouse } from './mouse';

const { x, y } = useMouse();
</script>

<script>
export default {
  data: () => ({
    count: 0,
  }),

  methods: {
    increase() {
      this.count++;
    },
  },
};
</script>

<template>
  Count: {{ count }}
  <button @click="increase">Increase</button>
  Mouse position is at: {{ x }}, {{ y }}
</template>

See it live on Stackblitz

The above approach works fine but has some limitations. So far I have discovered that shallowRef and shallowReactive are not working properly (reactivity works on nested properties when it should not). In Vue 3 docs, there isn't any mention of how to use those APIs with Options API but I have a feeling that they are going to include in the docs something soon.

I haven't heavily used Options API with Vue 3, but so far it feels way better to just have the reactivity magic work without specifically defining it with ref, reactive etc.

Personal thoughts

I think Vue 3 is more focused on the performance and not too much on the Development experience where Vue 2 shines✨

They have abstracted everything on their own API. Too much abstraction is harmful IMO. For example, Vite is opinionated and compared to Webpack which has a lot of abstraction, has better DX.

Now if I want to use router, route, pinia stores, vue-i18n, vee-validate etc I have to import and use those every single time on each component whereas on Options API they can simply be accessed via global properties.

<script>
import { ref } from 'vue'
import { useRouter } from 'vue-router'
import { useI18n } from 'vue-i18n'
import { useCounterStore } from './store'
import { useMouse } from "./lib/mouse";

export default {
  setup() {
    const router = useRouter()
    const { t } = useI18n()
    const { count } = useCounterStore()
    const { x, y } = useMouse()
    const name = ref('Roland')

    //...
  }
}
</script>

I've also seen people stating that having everything in a single function is more readable. And they show simple examples where setup looks cleaner than Options API. Personally, with what I've seen in real-world code, setup methods:

  • are often huge, lots of LOC
  • it's difficult to read the code
  • In order to make it clean devs usually add empty lines and comments (eg lifecycle hooks, watch etc)
  • it is not clean and intuitive and makes it hard to search for specific sections (eg where is a specific method)
  • enforce extraction. When a setup method becomes relatively big, since it has the above downsides, you feel the push to split it into composables. I've experienced that too many composables make it really difficult to debug and follow the flow of the code and also developers don't initially extract everything. Personally, I like building the code for features first and then I do refactoring/cleanup code.

So instead of showing simple examples, let me show you a real-world code of a setup method:

image.png

I couldn't fit the whole code in the image

I believe, writing that setup method in an "Options API" way would be more readable, flexible and manageable.


Feel brave and share your opinion even if it's against a popular one. I've seen devs just following trends because it seems that everybody uses them and it's a trend now. We should be more open and respect different thoughts but also share our own without being afraid.

What do you think about Options API and Composition API ? Have you tried both in Vue 3 ? Vue 3 is faster and has more features but do you think is easier compared to Vue 2 ?

Thank you for reading, I appreciate any feedback, feel free to comment out. With respect, Roland.

 
Share this