Composition API vs. Options API

Should you use the Composition API or stick with the Options API?

It’s a question that every Vue dev has asked themselves at least once — at least those working with Vue 3.

My answer is the same as what’s stated in the official Vue docs:

Use the Composition API over the Options API, unless you’re building a small or simple app.

There are many reasons for this, which is what we’ll get into in this article.

First, we’ll compare the Composition API and the Options API and see how they stack up against each other. There are some key factors here like modularity and reusability that will be our focus.

Then, we’ll look at some other differences to consider. Things like expressivity, TypeScript support, and how each one deals with boilerplate and handles constants. Although less important in the grand scheme of things, there are some big differences here.

Then we’ll take a short detour to see how you can actually use both the Options API and the Composition API together in the same component. There are tradeoffs for sure, but it is possible.

Lastly, we’ll take a closer look at what the Vue docs are actually saying and what it means for you as a Vue developer.

Key Differences (and why the Composition API is the best)

The main differences I want to focus on here are:

  1. Code organization
  2. Modularity
  3. Reusability

These three things are the most important when it comes to making your decision, so we’ll spend the most time on them.

1. Code Organization

An important aspect of development is how your code is organized, and how easy it is to keep it organized.

Ideally, we can have a high level of cohesion in our code. This means that code that works together lives as close to each other as possible. We often say that code is “co-located”, meaning it’s located near another piece of code.

This is where the Composition API really shines.

We’re able to co-locate our code based on the feature and functionality, not the type of code that it is. The Options API forces us to keep all of the computed props together, all the watchers together, all the methods together, and so on.

But this isn’t how we write code.

We typically modify code based on functionality. We jump between a computed prop, then to a method, then to the template and back to a lifecycle hook.

We don’t spend an hour writing all the computed props, then move on to write all of the methods, and so on.

So we want our code to be co-located based on functionality, not on the type of code that it is.

There is a caveat here though.

the advantage of the Composition API comes from the fact that it lets you organize your code however you want. This means that it’s up to you, as the developer, to keep your code neatly organized.

We all know how that can go!

Sometimes we’re lazy (or if you’re me, a lot of the time). The Options API forces me to have a minimum amount of organization. The Composition API though… I can end up with a disaster if I’m not careful.

The Options API may not be the ideal way of organizing your code, but it’s better than nothing.

But, with some professional discipline, I’m sure you can keep your code properly organized, right? We can also largely mitigate this issue by creating composables, but we’ll get to that a bit later.

2. Modularity

We don’t just want to organize our code, we also want to create distinct units of functionality.

We often spend our time breaking up large components into smaller ones, or extracting more specific functions out of bigger functions. Doing this helps us to further organize our code, and makes it possible to reuse our code as well (reusability is the next factor we’ll tackle).

The Composition API allows us to easily create modules, which we call “composables”.

Once the code is organized nicely, and related functionality has been co-located, it’s a pretty simple step to extract that functionality into a composable. These composables can even be single use, they don’t always have to be reusable!

Doing this makes the code more readable and understandable.

Let’s take a quick look at this example of a component that has some logic for a “like” button:

import { ref, computed, onMounted } from 'vue';
const likes = ref(0);
onMounted(() => {
fetch('/api/post/1')
.then((response) => response.json())
.then((data) => {
likes = data.post.likes;
});
});
const sendLike = async () => {
likes.value++;
fetch('/api/post/1/likes', {
method: 'POST'
})
.catch((error) => {
likes.value--;
});
}
const likesAmount = computed(() => {
return likes.value + ' people have liked this';
});

This code is nicely co-located (it helps that there’s no other logic in this imaginary component 😂), so let’s extract the relevant code into a useLikes composable:

// useLikes.js
import { ref, computed, onMounted } from 'vue';
export default function useLikes(postId) {
const likes = ref(0);
onMounted(() => {
fetch(`/api/posts/${postId}`)
.then((response) => response.json())
.then((data) => {
likes.value = data.post.likes;
});
});
const sendLike = async () => {
likes.value++;
fetch('/api/post/1/likes', {
method: 'POST'
})
.catch((error) => {
likes.value--;
});
}
return {
likes,
sendLike,
}
}

Now, our component is greatly simplified, but it’s still easy to see what’s going on:

import { computed } from 'vue';
import useLikes from '@/useLikes';
const {
likes,
sendLike,
} = useLikes(1);
const likesAmount = computed(() => {
return likes.value + ' people have liked this';
});

You don’t always have to extract these composables to new files, either. You can create them inline in your component if that makes more sense. If you want to learn more about them, it’s one of the 21 Vue design patterns covered in the Clean Components Toolkit.

The Options API has no good way of breaking up logic into modular bits like this.

The three main ways we’ve tried to modularize our Vue logic in the past were:

  • Plain JS functions and objects
  • Renderless components
  • Mixins

They each have their flaws which make them far less than ideal, so let’s take a closer look.

Plain Javascript functions and objects are certainly useful to modularize logic in Vue projects. The main drawback here is that you aren’t able to access any of Vue’s reactivity, which hugely limits what you can actually do with it.

Composables are basically JS functions, but with the added benefit of having full access to Vue’s reactivity system (if it doesn’t use Vue’s reactivity, it’s just a utility method).

Renderless components were extremely popular for a time, because they were the best way to modularize logic in Vue 2 — before we had the Composition API. But what you’re doing is creating an entire component just to carry some logic around, so there’s lots of unnecessary overhead.

If I remember correctly, it was Adam Wathan (of TailwindCSS) who first popularized renderless components in 2018.

There’s also the huge issue of accessing needed variables from inside of the slot’s scope:

<template>
<RenderlessComponent v-slot="slotProps">
<ChildComponent :props="slotProps" />
</RenderlessComponent>
</template>

If you have logic in your component that needs to use a value from slotProps, you’re out of luck. Either you have to push that logic into the ChildComponent, or you have to do this hacky thing with calling a method from the template and passing slotProps to it:

<template>
<RenderlessComponent v-slot="slotProps">
<ChildComponent :props="myMethod(slotProps)" />
</RenderlessComponent>
</template>
const myMethod = slotProps => {
// Now we can access slotProps, but only inside this method :/
}

Renderless components do still have some uses, but seriously, don’t resort to doing all sorts of template gymnastics — even if it’s fun to figure out.

Finally, we get to mixins.

As far back as 2016 the web dev community started to shift away from mixins for some very good reasons.

The main reason is that mixins create implicit dependencies that are extremely difficult to track down. Methods, computed props, and other things get added to a component in a way that’s not clear or easy to discover.

When you go to change a component that uses a mixin (or to change a mixin that’s used in a component), you really have no good idea of what’s going to happen or what you might break.

However, with composables, you know exactly which components they’re being used in, and how they’re being used in those components. Every dependency is explicit, so you know where all of the logic is coming from.

With auto-imports in Nuxt 3 this is still true. While the imports are not explicit, the usage of the composables themselves is, and that’s all we need.

This issue with mixins is actually a well-known problem called the fragile base-class problem. Too bad we didn’t see that one coming, huh?

3. Reusability

Finally, we arrive at reusability. Once we’ve written and tested some great logic, we want to be able to reuse it over and over again, to get the most out of it.

We’ve seen that with the Composition API we can use composables to create highly reusable pieces of logic. These composables can hook into all sorts of Vue’s reactivity system, lifecycles, and basically every process of Vue.

But because they aren’t bound to the component instance with this, we can share them throughout our projects, and even between projects.

VueUse is a great example of this.

It’s a library containing dozens and dozens of extremely useful and well-written composables that have been battle-tested to make sure they are rock solid. If you haven’t yet checked it out, here are some examples:

  • useTitle makes it simple to update the page’s title
const title = useTitle('The Initial Title');
// Change the title reactively
title.value = 'This is a new title';
  • useAsyncState allows you to run async code in the background without blocking your main code, and will reactively update once the async code returns
const { state: number, isLoading } = useAsyncState(fetchData());
// When the data has been fetched this value will be automatically
// recomputed, because `number` is updated reactively
const computedValue = computed(() => number + 4);
  • With useStorage you can reactively access localStorage, sessionStorage, or any other storage provider that you want
// Read from storage with a fallback value
const name = useStorage('my-name', 'Default Name');
// Any changes are reactively synced with localStorage
name.value = 'Michael Thiessen';

If you’re trying to do something similar with the Options API though, you’re out of luck.

We already covered why renderless components and mixins aren’t great options, but then you aren’t really left with anything else. You can use plain JS files, but again, you’re missing out on all of the reactivity.

And honestly, we’re talking about making Vue components and logic reusable, so you can’t get terribly far without that reactivity.

These three main factors — code organization, modularity, and reusability — are all interrelated. You need modularity for reusability, and modularity requires good organization.

While the Options API does give you some of this, it’s the Composition API that really shines in this area, and this is why it’s the recommended approach.

More Differences

While organization, modularity, and reusability are the most important factors to consider, there are several other differences I’d like to point out.

This is what we’ll cover in this section:

  1. Expressivity
  2. The problem with this
  3. TypeScript support
  4. Boilerplate
  5. Handling constants (and external resources)

1. Expressivity

The Composition API is much more granular than the Options API is, and lets you have more control over exactly how the reactivity works.

In fact, in Vue 3 the Options API is built using the Composition API. It’s an abstraction over top to help simplify some things in key areas, but this simplification necessarily means that you lose some control and flexibility.

Sometimes this is good, but other times you want that extra flexibility, and that’s where the Composition API truly shines.

2. this is an issue

The Options API always needs access to this.

If you’ve used it for any length of time, you’ll likely have run into some issues there, as well.

For example, you need to make sure to use regular functions, or you lose access to this:

methods: {
someMethod: () => {
// Oops, "this is undefined"
console.log(this.someComputedProp);
},
someMethod() {
// Ah, much better
console.log(this.someComputedProp);
}
}

But then other times, you’ll need to use the arrow function and not the regular function in order for this to be preserved inside of the function context:

methods: {
someMethod() {
const sortedArray = this.array.sort((a, b) => {
// Need to use an arrow function so we can use `this`
return b[this.sortProp] - a[this.sortProp];
});
// ...
}
}

All of these headaches go away with the Composition API — just write your functions however you want to, and it works.

There’s no need to think about or worry about how to access this.

3. TypeScript Support

Vue 3 is written in TypeScript, so support has gotten a lot better than it was with Vue 2. However, using the Composition API still gives you better control over types than the Options API does.

Using TS with the Options API requires you to wrap your component in the defineComponent method, and then jump through some extra hoops to get things typed properly.

With the Composition API, we get incredible support for TypeScript that is constantly improving with each new release. For example, generic components (examples taken from that post):

<script setup lang="ts" generic="T">
defineProps<{
items: T[]
selected: T
}>()
</script>

The value in the generic field works exactly like <...> in normal TypeScript, so we have full flexibility here:

<script setup lang="ts" generic="T extends string | number, U extends Item">
import type { Item } from './types'
defineProps<{
id: T
list: U[]
}>()
</script>

If you’re writing your app in TypeScript, then the Composition API is definitely the way to go.

4. Boilerplate

I’m sure that by now you’ve seen the comparison of boilerplate code between using the Composition API and the Options API.

This difference becomes even more dramatic when using <script setup>, as there is even less we need to write:

<script setup>
import { ref } from 'vue';
const number = ref(0);
</script>
<script>
export default {
data() {
return {
number: 0,
};
}
};
</script>

This is a trivial example, but we reduce our code by many lines. The savings on larger components can be even higher.

Depending on the component you’re writing, this boilerplate can become a significant part of the file. But the main problem is that all of this extra code is noise, and you have to filter through it when trying to read and understand the code.

Less boilerplate makes the code easier to understand and easier to work with!

5. Constants and External Resources

If you have an icon or SVG file that you need to render in your template, what do you do?

With the Options API you have this awkward moment where you have to decide if it should be put in data as a reactive value, or as a computed prop:

import Logo from './logo.svg';
export default {
data() {
return {
logo: Logo,
};
};
}

But an SVG logo isn’t really “data”, and it certainly doesn’t need to be reactive. So maybe you get it into the template scope through a computed prop:

import Logo from './logo.svg';
export default {
computed: {
// Use an arrow function because we don't access `this`
logo: () => Logo,
};
}

While both of these work, neither really makes any sense.

There is a third way, which you can use if you really want to avoid making it reactive:

import Logo from './logo.svg';
export default {
data() {
// Add to the component instance without making it reactive
this.logo = Logo;
return {
// ...
};
};
}

By adding the value to the component instance directly, you can access it within the template, but it won’t be made reactive.

Here’s a much better solution using <script setup> which requires the Composition API:

import Logo from './logo.svg';

Yeah, that’s literally all we need to do, since anything within scope of <script setup> is also within scope in our template.

Which method makes more sense to you?

Where the Options API is Better

I’d be lying if I didn’t say there was one thing that the Options API was better at:

Simplicity.

Yes, the Composition API is more powerful, more flexible, and better in so many different ways.

But it’s more complicated to use. It can be more difficult to understand.

I think this is it’s greatest drawback, and the main reason — perhaps the only reason — that so many devs do not like it. Many of us who started with Vue 2 have become very productive using the Options API over the years, and the Composition API was (and maybe still is) something new and difficult.

You don’t have to deal with the inconsistencies of using .value with ref in different places, you don’t have to think about how to organize your code — you just write stuff and things happen.

However, in my opinion, these extra difficulties are small in comparison to the extra value that the Composition API brings.

It just takes some time to get used to it.

Why the Composition API is Better than the Options API

I am in complete agreement with the official position of Vue in that the Composition API is a better choice.

The main reasons for this:

  • Better code organization
  • Better modularity
  • Better reusability by using composables

Some other, less important places where the Composition API shines are:

  • More expressivity
  • No dealing with the complexities of this and scopes
  • Better TypeScript support
  • Less boilerplate to deal with
  • Much easier to deal with constants and external resources

And while these differences do show themselves on small projects, they become even more dramatic and important as the application grows in size and complexity. This is why the documentation recommends the Composition API specifically for production applications.

However, the Composition API has one major drawback: it’s complexity.

But I — and most of the Vue community — believe that this small increase in complexity is completely worth all the benefits it brings.

It just takes some time and effort to get used to.