DEV Community

Cover image for Why you should be using Vue's new Composition API
Andrew Schmelyun
Andrew Schmelyun

Posted on • Updated on • Originally published at michaelnthiessen.com

Why you should be using Vue's new Composition API

You keep hearing about this composition API in Vue. But it's a little scary and intimidating, and why it's so much better isn't really all that clear to you.

In this article you'll see exactly why you should learn to use it by comparing the old way to the new way. The examples also start out simple and get more complex, so you can see that the composition API isn't really all that different from what you're used to.

This replaces Vue 2's current options API, but the good news is that you aren't required to use it in Vue 3 applications. You can still use the tried-and-true options API and write your components just like you would have previously in Vue 2. For those who want to adopt this new method now or just want to familiarize with the updates, here's a few examples of some common, simple components, re-written using Vue 3's composition API.

A simple counter

Pretty much the go-to "Hello world" of frontend frameworks, the counter component. Let's see what one looks like in Vue 2:

<template>
  <div class="counter">
    <span>{{ counter }}</span>
    <button @click="counter += 1">+1</button>
    <button @click="counter -= 1">-1</button>
  </div>
</template>
<script>
export default {
  data() {
    return {
      counter: 0
    }
  }
}
</script>
Enter fullscreen mode Exit fullscreen mode

We're displaying a span tag with a counter data object, which starts at zero. We then have two buttons with v-on:click attributes and inline code telling them to increase, or decrease, the counter by one. Then in the script tag, we're initializing that counter through a returned object in the data method.

Now let's take a look at what the same component looks like in Vue 3:

<template>
  <span>{{ counter }}</span>
  <button @click="counter += 1">+1</button>
  <button @click="counter -= 1">-1</button>
</template>
<script>
import { ref } from 'vue';
export default {
  setup() {
    const counter = ref(0);

    return {
      counter
    };
  }
}
</script>
Enter fullscreen mode Exit fullscreen mode

The first thing you might notice is that I've removed that wrapper div from the template. Previously in Vue, you'd get an error if you tried to render a component with more than one top-level element under the template tag. In Vue 3, this is no longer the case!

Moving down to the script section, it's a little longer than the previous component. That's kind of to be expected though, since our functionality is the bare minimum and there's slightly more setup with the composition API. Let's go over the changes line-by-line.

import { ref } from 'vue';
Enter fullscreen mode Exit fullscreen mode

The ref method is required in order to give any data point reactivity in the composition API. By default, variables returned from the setup method are not reactive.

export default {
  setup() { ... }
}
Enter fullscreen mode Exit fullscreen mode

Next, we have the new setup method. This is the entrypoint for all composition API components, and anything in the returned object from it will be exposed to the rest of our component. This includes things like computed properties, data objects, methods, and component lifecycle hooks.

setup() {
  const counter = ref(0);

  return {
    counter
  };
}
Enter fullscreen mode Exit fullscreen mode

We're first creating a counter using the previously-mentioned ref method, and passing it the initial value, zero. Then, all we have to do is return that counter, wrapped in an object.

From there, our component works just like it previously did, displaying the current value and allowing the user to adjust it based on the button presses given! Let's move on and take a look at something with a little more moving parts.

A shopping cart

Moving up in complexity, we'll create a component that uses two common attributes in Vue, computed properties and defined methods. I think a great example for that would be a basic shopping cart component, which shows items that a user has selected on something like an e-commerce website.

Here's an example of that in Vue 2 using the options API:

<template>
    <div class="cart">
        <div class="row" v-for="(item, index) in items">
            <span>{{ item.name }}</span>
            <span>{{ item.quantity }}</span>
            <span>{{ item.price * item.quantity }}</span>
            <button @click="removeItem(index)">Remove</button>
        </div>
        <div class="row">
            <h3>Total: <span>{{ cartTotal }}</span></h3>
        </div>
    </div>
</template>
<script>
export default {
    data() {
        return {
            items: [
                {
                    name: "Cool Gadget",
                    quantity: 3,
                    price: 19.99
                },
                {
                    name: "Mechanical Keyboard",
                    quantity: 1,
                    price: 129.99
                }
            ]
        }
    },
    methods: {
        removeItem(index) {
            this.items.splice(index, 1);
        }
    },
    computed: {
        cartTotal() {
            return this.items.reduce((total, item) => {
                return total += (item.price * item.quantity);
            }, 0);
        }
    }
}
</script>
Enter fullscreen mode Exit fullscreen mode

Items in the cart are listed with v-for, and a button is present after each one to remove it from the main array on click. The total cost of the cart is calculated through a computed property that uses reduce and the value is displayed at the bottom of the items. Pretty straightforward, I think!

Let's see what a similar component with these attributes looks like in Vue 3 using the composition API:

<template>
    <div class="cart">
        <div class="row" v-for="(item, index) in items">
            <span>{{ item.name }}</span>
            <span>{{ item.quantity }}</span>
            <span>{{ item.price * item.quantity }}</span>
            <button @click="removeItem(index)">Remove</button>
        </div>
        <div class="row">
            <h3>Total: <span>{{ cartTotal }}</span></h3>
        </div>
    </div>
</template>
<script>
import { ref, computed } from 'vue';
export default {
    setup() {
        const items = ref([
            {
                name: "Cool Gadget",
                quantity: 3,
                price: 19.99
            },
            {
                name: "Mechanical Keyboard",
                quantity: 1,
                price: 129.99
            }
        ]);

        const removeItem = (index) => {
            items.value.splice(index, 1);
        };

        const cartTotal = computed(() => {
            return items.value.reduce((total, item) => {
                return total += (item.price * item.quantity);
            }, 0);
        });

        return {
            items,
            removeItem,
            cartTotal
        };
    }
}
</script>
Enter fullscreen mode Exit fullscreen mode

The biggest difference is that the computed property and method aren't in their own properties in the root Vue object, instead they're just plain methods defined and returned in the main setup() method.

For methods, we just create them as functions:

const removeItem = (index) => {
    items.value.splice(index, 1);
};
Enter fullscreen mode Exit fullscreen mode

And as long as we include them in the returned object, they're exposed to (and can be used by) the rest of the component. Computed properties are almost the exact same, with the exception of being wrapped in a computed method that's imported from the main Vue package:

const cartTotal = computed(() => {
    return items.value.reduce((total, item) => {
        return total += (item.price * item.quantity);
    }, 0);
});
Enter fullscreen mode Exit fullscreen mode

This way, we can de-couple parts of our components and separate them even further into portions of functionality that can be re-used and imported into multiple other components. We'll see how to do this in our next example.

For instance, if we wanted to, we could easily split out the cartTotal computed property or the removeItem method into their own files. Then instead of defining and using them in the main component above, we'd import them and just call the designated method.

On to the last component!

A like button

Our third and final example is even more complex than the last two, let's see what a component would look like that has to pull in data from an API endpoint and react to user input.

This is what that might look like with the options API in a Vue 2 application:

<template>
  <button @click="sendLike" :disabled="isDisabled">{{ likesAmount }}</button>
</template>
<script>
export default {
  data() {
    return {
      likes: 0,
      isDisabled: false
    }
  },
  mounted() {
      fetch('/api/post/1')
          .then((response) => response.json())
          .then((data) => {
              this.likes = data.post.likes;
          });
  },
  methods: {
    sendLike() {
      this.isDisabled = true;
      this.likes++;

      fetch('/api/post/1/likes', {
        method: 'POST'
      })
        .then((response) => {
          this.isDisabled = false;
        }
        .catch((error) => {
          this.likes--;
          this.isDisabled = false;
        });
    }
  },
  computed: {
      likesAmount() {
          return this.likes + ' people have liked this';
      }
  }
}
</script>
Enter fullscreen mode Exit fullscreen mode

A little more complicated than our previous examples, but let's break it down.

We're starting off in the template with a button, that has a v-on:click bind to a sendLike method, and a bound disabled attribute to the data attribute isDisabled. Inside of that button we're showing the amount of likes with a likes data attribute.

Moving through to the script, we're initializing the data object returned with 0 likes, and isDisabled set to false. We're using the mounted() lifecycle method to call an API endpoint and set the amount of likes to a specific post's likes.

Then we define a sendLike method, which disables the button and increases the likes by 1. (We're increasing the likes before actually sending the request so that our user interaction is recorded immediately.)

Finally, we send the request to our make-believe API, and await the response. Either way, we remove the disabled attribute from the button, but if the server returns an error for some reason, we remove the initial like that was recorded and reset likes to the previous value.

Now, let's see what a similar component would look like in Vue 3 using the composition API:

<template>
  <button @click="sendLike" :disabled="isDisabled">{{ likesAmount }}</button>
</template>
<script>
import { ref, computed, onMounted } from 'vue';
export default {
  setup() {
    const likes = ref(0);
    const isDisabled = ref(false);

    onMounted(() => {
        fetch('/api/post/1')
            .then((response) => response.json())
            .then((data) => {
                likes = data.post.likes;
            });
    });

    const sendLike = async () => {
        isDisabled.value = true;
        likes.value++;

        fetch('/api/post/1/likes', {
            method: 'POST'
        })
            .then((response) => {
                isDisabled.value = false;
            })
            .catch((error) => {
                likes.value--;
                isDisabled.value = false;
            });
    }

    const likesAmount = computed(() => {
        return likes.value + ' people have liked this';
    });

    return {
      likes,
      isDisabled,
      likesAmount,
      sendLike
    };
  }
}
</script>
Enter fullscreen mode Exit fullscreen mode

Alright, there it is!

Now, a main difference between this and our counter component is the addition of a mounted lifecycle hook. Instead of being another separate method like in Vue 2's options API, this is again just written as a function in setup, wrapped in an included onMounted() method.

This is where the composition API can start to shine with composables. This like button component is getting a little long, and it includes some functionality that could be split out into a separate file and imported instead.

For example, we might want to include the retrieval and updating of likes in different components, so we can create a new JavaScript file which handles just that:

// useLikes.js
import { ref, computed, onMounted } from 'vue';

export default function useLikes(postId) {
    const likes = ref(0);
    const likesAmount = computed(() => {
        return likes + ' people have liked this'
    });

    onMounted(() => {
        fetch(`/api/posts/${postId}`)
            .then((response) => response.json())
            .then((data) => {
                likes.value = data.post.likes;
            });
    });

    return {
        likes,
        likesAmount
    }
}
Enter fullscreen mode Exit fullscreen mode

This renderless component, useLikes, initiates the placeholder likes amount, 0. It then sends a fetch request to the API endpoint of the post whose ID is passed in. After that completes, our likes are then updated to match whatever is attributed to that current post.

So, how's this used back in our main component? Like this:

<template>
  <button @click="sendLike" :disabled="isDisabled">{{ likesAmount }}</button>
</template>
<script>
import { useLikes } from '@/useLikes';
import { ref, computed, onMounted } from 'vue';
export default {
  setup() {
    const {
        likes,
        likesAmount
    } = useLikes(1);

    const isDisabled = ref(false);

    const sendLike = async () => {
        isDisabled.value = true;
        likes.value++;

        fetch('/api/post/1/likes', {
            method: 'POST'
        })
            .then((response) => {
                isDisabled.value = false;
            })
            .catch((error) => {
                likes.value--;
                isDisabled.value = false;
            });
    }

    return {
      likes,
      isDisabled,
      likesAmount,
      sendLike
    };
  }
}
</script>
Enter fullscreen mode Exit fullscreen mode

First we use an import statement to get our exported useLikes function, and then use a destructured object that consists of the likes and likesAmount ref object and method respectively. They're brought into our main component through that same useLikes function.

All that was left to do was pass in the postId attribute, which we've set as a hard-coded value to 1.

Wrapping up

Well, there you have it! You've seen three different components that were created in Vue 2, and then their counterparts replicated in Vue 3.

Whether you're a developer experienced in the framework, or one who's still learning the ropes, I hope these helped you on your journey through this newest version of Vue. Despite its different, sometimes intimidating appearance, the composition API can help you organize and refactor your frontend code in a more stable and maintainable way.

If you have any questions, comments, or want to chat more about
web development in general, don’t hesitate to reach out on Twitter or through the discussion below.

Discussion (27)

Collapse
mnussbaumer profile image
Micael Nussbaumer

I don't really agree with the arguments put forward for the new API. Setting aside that I think that, that declarative way with well understood and defined properties was one of the strengths of vue along with automatic reactivity based on data and computed properties.

In the like button you go from something that has explicitly defined and named things (mounted, data, methods and computed properties) to a soup. I need to read it all every single time because everything it exports can be just about anything.

In terms of sharing functions there wasn't anything before preventing you from writing your functions in files outside, importing them in the vue component and wrapping them in methods to be used inside the templates (import { fun_x } from "Y", methods: { fun_x(args) { return fun_x(args) }} and then using this.fun_x or fun_x from a template).

Don't know if in the internal parts of vue it helps or not the development/work and optimization of the framework, but from my point of view as a user of the framework, it seems like a step back, motivated by FOMO on React. Besides, when you extract those things into a useLikes it's even worse than mixins, for all the criticism their usage had, at least it was structured.

Collapse
hasnaindev profile image
Muhammad Hasnain • Edited

Exactly what I wanted to say! They are turning Vue into React. At least, the syntax is that of React. Vue was already really confusion with registering components in the air or using external libraries, now it's even more confusion with ref(), this.$refs and ref attribute.

You're also right about moving all the code to a setup method and turn it into soup. What about components that may hold code that are more than hundred lines long. This makes searching much more of a challenge.

Collapse
jfbrennan profile image
Jordan Brennan

this.$refs, ref="", and now ref() which has nothing to do with the first two! What were they thinking??

Collapse
jfbrennan profile image
Jordan Brennan

Nothing like a cold bowl of composition soup haha

My experience is exactly what you said. Reverted my one component and not gonna bother with it going forward. No real benefit.

Btw, you can just do this and everything works

import { fun_x } from 'Y';
methods: { 
  fun_x
}
Enter fullscreen mode Exit fullscreen mode
Collapse
mnussbaumer profile image
Micael Nussbaumer

Didn't know about that short cut!

Collapse
tqbit profile image
tq-bit

What you say is somewhat true. Then again, you can keep writing Vue as before. There'll just be another way of doing it.

I only really grasped the usability of Vue's composition API after starting to learn React Hooks (took me a while, as I highly prefer Vue). If you find the composition fucntions to be a soup - they are, but so is a huge component that uses the options API. Imagine a single file that at some point was meant to do a single thing grow to around 600 lines of code, you might consider moving some code out of it. double that amount and you end up with an unmaintainble mess.

Also, consider state. Composition API permits you to relatively simply create a data provider without using third party packages. Especially (async) actions are candidates to be reused in several components.

Wrapping up, for small projects, you're probably well set to develop Vue apps like before. Frankly speaking, I highly prefer the 'common' way, but would not want to close my eyes in front of new innovations, even if they look alien at first.

Collapse
mnussbaumer profile image
Micael Nussbaumer • Edited

To each their own, I mean it's opinions. I've written a few serious apps with React and some quite complex with vue2, I would use vue2 anytime. If you never had "complex" behaviour become a mess of useEffects, contextProviders and friends, that's ok. I use functional languages for everything else, and to be honest, describing UI's in the browser - given the html/dom model and the existing browser apis - is probably one of the few things I think an object oriented wrapper has benefits. I want hooks for the lifecycle, I want to specify reactive data, methods and data that is reactive but also reacts to any of its automatically tracked dependencies (computed properties). For me that organisation makes sense, the same way single file components make sense even if you extract things out of them to "clean up", but that's just my point of view.

Collapse
alfeg profile image
Victor Gladkikh

Those examples are very very simple. I have worked on quite a big Vue app. Some pages became very very repetitive, and there is no way to remove this other than use mixins. And mixing are evil.
On other project we started from scratch with Vue3 and composition api. It's much easier to work with. You don't need to scroll across document to find data section or computed values. All required parts can be placed together.

Collapse
mnussbaumer profile image
Micael Nussbaumer

Why is mixing evil? You could create:

my_mixin_only_for_my_component_because_i_dont_like_scrolling_and_prefer_opening_another_file.js and mix it in.

But these are all just opinions of course. Having worked with very complex ui's both in vue and react I still prefer vue2's way of organising code.

Collapse
tefoh profile image
Tefoh

With version 3.x vue team probably want to use hooks inside vue, and they come with composition api. and for some reasons it didnt work perfect. with version 3.2 they try Svelte syntax, how ever in my opinion its getting better but it wasnt a good start.

And i really excited for new $ref api for next version, its gonna be better(hopefully:))

Collapse
marzelin profile image
Marc Ziel • Edited

I know that this post isn't about async/about but I couldn't help it.
You create an async function but don't use await anywhere still working directly with promises. Either make it a normal function or use await:

const sendLike = async () => {
  isDisabled.value = true;
  likes.value++;
  try {                 // ↓ hardcoded postId, yikes
    await fetch('/api/post/1/likes', {
      method: 'POST'
    })
  } catch (e) {
    likes.value--;
  } finally {
    isDisabled.value = false;
  }
}
Enter fullscreen mode Exit fullscreen mode

Also, you should move this function to useLikes.js since it's part of the same feature set.

Collapse
pawelmiczka profile image
Paweł Miczka • Edited

I started using Composition API with TypeScript last year and damn - that was great experience. Not only because code looks "cooler" and cleaner but also with fact that you can much easier split your code into single "plugins".

With this you can move things like onMounted etc. to separated files and implement those without any problem to any component you want.

There is one great thing that was implemented in 3.2 - <script setup> which transforms your code from this:

<script>
// imports here

export default defineComponent({
   setup() {
      const name = ref('')

      return { name }
   }
})

</script>
Enter fullscreen mode Exit fullscreen mode

to this:

<script setup>
// imports here

const name = ref('')

</script>
Enter fullscreen mode Exit fullscreen mode
Collapse
pawelmiczka profile image
Paweł Miczka

You don't need to use Composition API, sure but I think you will :D

Collapse
jfbrennan profile image
Jordan Brennan

I converted a 367 loc component with child components, props, data, computed, watch, created and mounted, and many methods to the Composition API.

It added a few lines of code and a lot more characters. The code was slightly less readable and slightly less organized than before. The new setup saves the large return block and some other minor clean up, but overall it’s no better than before. Composition API is NOT an improvement, it’s merely different and seems to have only made learning Vue less straight-forward because people now have to pause and decide which of the two APIs to use and posts like this only serve to convince people new = better and that’s just not true.

Collapse
oniichan profile image
yoquiale

Agree, I liked Vue because of the straightforward templates and because it was easier to understand than React, but now the composition API complicates it and makes it a React wannabe.

Collapse
artydev profile image
artydev

Hy,
For simple apps why bother with frameworks ?
Loot at this simple counter

<html lang="de">
  <head>
  <meta charset="utf-8">
    <title>title</title>
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <script src="https://efpage.de/DML/DML_homepage/lib/DML-min.js"></script>
    <style>
      button {
        background: lightgreen;
        padding: 10px;
        margin: 5px;

      }
    </style>
  </head>
  <body> 
  <script> 

    function Counter () {
        button("inc").onclick = () =>value.innerText = Number(value.innerText)+1
        let value = idiv("0", "font-weight:bold");
        button("dec").onclick = () => value.innerText = Number(value.innerText)-1
    }

    Counter();
    br()
    Counter();

  </script>
  </body>
</html>
Enter fullscreen mode Exit fullscreen mode

You can test it here :

Counter

Collapse
arashm profile image
Arash Mousavi

Dude, the point of this post is about VueJS framework, not writing a counter.

Collapse
artydev profile image
artydev

I agree with you :-)

Collapse
alexmercedcoder profile image
Alex Merced

The composition API is 10x easier to use if you use the experiemental setup attribute.

‘’’
</setup><br> ‘’’</p> <p>In this situation the script tag is the setup function and looks a like cleaner, you can try this in the ‘npm init vite’ Vue template which has this out of the box</p>

Collapse
jfbrennan profile image
Jordan Brennan

10x is a major exaggeration. Composition API in my experience has only made my components less readable and they don’t save any lines of code either. It’s merely different at best.

Collapse
alexmercedcoder profile image
Alex Merced

I agree normally but with the setup attribute it’s a lot cleaner than setting up the setup function in the exported object

Collapse
kevlawton profile image
Kevin Lawton

PM / Dev Manager non-web developer here. Looking at Angular vs React vs Vue vs Flutter vs Blazor vs Svelte, I concluded that Vue is superior for my purpose of creating quick internal MVPs. However, much like the debate in the comments, the Vue2 vs Vue3 uncertainty has definitely made me question investing in Vue. I seriously applaud all you front-end web developers building real world solutions on what feels like a constantly shifting "hot mess" of WIP projects and concepts.

Collapse
twigman08 profile image
Chad Smith

When the composition API was announced I was hesitant, my argument was that most of the dev time would go towards the new composition api. It came out and I tried it, and I’d say it was ok. I’ve written large scale applications with Vue 2. Matter of fact I’m currently working on 3 large scale applications in Vue 2 right now at work. So I’m very familiar with the options API in large applications. To me it’s easier. Large Applications are usually more complex to deal with, with the options API I can go to a component and tell you very quickly where things are, and with a little code review we’ve kept components well maintained and clean still.

My point is this - you can write “spaghetti” code on any language, framework, or pattern. In the end it is up to you, the developer, to keep the codebase clean. I’ve already seen, in my honest opinion, not great code with the new composition API, so I don’t think it fixes any problems. It’s up the developers to actually write it that way to fix it. Which in my experience is possible in both ways.

So it’s not that I have an issue with the Composition API, I just don’t think it fixes the problems people say they fix. In the end cleaner code is up to the developer maintaining the project, not the framework or pattern they use, and is still relatively opinionated.

Collapse
oniichan profile image
yoquiale

Imho options API is better because before I had learnt Vue, knowing what the code would do was clearer than if you use composition API, which introduces ref and I don't know what does that do.

Collapse
pawelmiczka profile image
Paweł Miczka

but you have deal with this which in vue is kinda not the same thing that you would get inside clear JavaScript code

Collapse
oniichan profile image
yoquiale • Edited

I'm ok with dealing with this.

Collapse
wahidn profile image
WahidN

So this is like React Hooks?