Introduction to Vue 3 and the Composition API

Jeff
Vue js

Introduction to Vue 3 and the Composition API

Here at Littlelines, we’re big fans of Vue. We use it for most of our projects. It makes writing the front end of our applications fun and simple. For most of our projects, we’ve used Vue 2.x. This has been the standard for the last few years. But now Vue 3 is coming (as of this post it is in beta), so we thought we should dive into the set up and take a tour of some new and exciting features.

Composition API

The most talked about new feature of version 3 of Vue is the composition API. The composition API was designed to help make Vue components more reusable and readable. In Vue 2.x, especially in large projects, components often grew large and difficult to maintain. This is, in part, because of the nature of the Options API and the limitations of reusing code (there were options like mixins, plugins, and scoped slots, but these all had certain limitations and drawbacks, as well).

Options API

To understand why the composition API is helpful, we will start with a quick recap of the Options API and an overview of some of its limitations.

The options API is basically all the methods that are available in a Vue single file component—things like data, props, computed methods, life cycle hooks (mounted, created, etc.). These functions and properties are really useful and they make Vue components predictable and uniform, but there is a negative side effect of relying on the options API for all of the logic in your component. Often, the logic of a feature is broken apart into many of these methods and spread across the component. When components are small and simple, this is no problem. But as features grow and become more dynamic and complex, they become difficult to reason about and prone to bugs.

Another challenge is sharing code between these components. The most common ways of sharing code in Vue 2.x were mixins and scoped slots. These both have their places in a Vue app, but come with certain drawbacks. Mixins require very specific implementation and naming conventions, otherwise they can become very brittle or be overridden by other mixin property names. Scoped slots, on the other hand, are decently flexible, but require a lot of configuration in the template (which can get messy and hard to understand) and can affect performance, because they create new instances of the same component.

Composition API Comparison

The composition API design pattern relies on composition functions and a new function called setup. Inside the setup function, you will define all of the data and functions for that component, and return them. This is quite a departure from the Vue 2.x way of setting up components, so let’s look at some examples to demonstrate the new practices.

For a quick example, we’ll use a hypothetical view with a list of posts.

<template>
  <div>
    <p v-if="postCountMessage">{{ postCountMessage }}</p>
    <table>
      <tr>
        <td>Title</td>
        <td>Body</td>
        <td>React</td>
      </tr>

      <tr v-for="post in posts" :key="post.id">
        <td>{{ post.title }}</td>
        <td>{{ post.body }}</td>
        <td>
          <button @click="reactToPost(post)" class="like-button" :id="`post-${post.id}`" :class="post.liked ? 'magenta' : 'grey'">
            <span class="f4 fa fa-thumb"></span>
          </button>
        </td>
      </tr>
    </table>

    <button @click="addSamplePost()">Add Sample Post</button>
  </div>
</template>

<script>

export default {
  data () {
    return {
      posts: [],
      samplePost: { title: 'New Post', body: 'Test'}
    }
  },

  computed: {
    postCountMessage() {
      const count = this.posts ? this.posts.length : 0
      return `${count} Current Posts`
    }
  },

  methods: {
    reactToPost (post) {
      // send data to API to toggle "liked" status
    },

    addSamplePost() {
      this.posts.push(this.samplePost)
    }
  }
}

</script>

Here is the way you might set up this pretty simple component in Vue 2.x. Now here’s how we might set this up using the composition API in Vue 3.

<script>
import { ref, computed } from "vue"

export default {
  setup() {
    const posts = ref([])
    const count = posts.value.length
    const samplePost = { title: "New Post", body: "Test" }

    const postCountMessage = computed(() => `${count} Current Posts`)

    function reactToPost(post) {
      // call backend API to save reaction
    }

    function addSamplePost() {
      posts.value.push(samplePost)
    }

    return {
      posts,
      postCountMessage,
      reactToPost,
      addSamplePost
    }
  }
}
</script>

You can see here that we have moved everything into the setup method. For things like data properties, we can set up reactive variables. We can also use the computed method to build complex dynamic data for the view.

One thing that might give you pause is that all the functions are defined inside the setup method. You might worry that we will always be left with one long, messy setup method for each component.

This might be the case if you exclusively used the composition API to set up a Vue app. But where the composition pattern really shines is when it’s used only with parts of an app that need to be reusable.

For this example, let’s say we want to sort this list of posts, and also sort lists in multiple other sections in the app. This might be a good opportunity to use the composition API to bundle this code together and reuse it in multiple places. To do this, we would create something called a composition function, like so:

<script>
  export default function useSortList(list, key = 'id') {

    // For this demo, we'll use the underscore package's sortBy method. You can
    // write you own with custom logic if you'd like.
    const sortList = (list, key) => _.sortBy(list, key)

    return { sortList }
  }
</script>

We will save this in a file called useSortList.js (the “use” at the beginning of the file name is a convention that is highly encouraged from the Vue community, but not technically necessary). After we have this encapsulated in a function, we can add this functionality to our original component to look like this:

<template>
  ...
  <tr>
    <td>
      Title
      <button @click="sortList(posts, 'title')">
        <i class="fas fa-sort"></i>
      </button>
    </td>
    <td>
      Body
      <button @click="sortList(posts, 'body')">
        <i class="fas fa-sort"></i>
      </button>
    </td>
    <td>React</td>
  </tr>
  ...
</template>
<script>


import { useSortList } from '@/composables/useSortList'

export default {
  setup() {
    ...

    const { sortList } = useSortList()

    return {
      posts,
      postCountMessage,
      reactToPost,
      addSamplePost,
      sortList
    }
  }
}
</script>

This pattern is all about organizing your components into functions and keeping those chunks of logic together and organized. It’s a new way of thinking about building Vue components, but it is a useful addition to out toolbelt.

The good news for users of Vue 3 is that you can use both strategies for building components. If you prefer the older way of building single file components, you can write them exactly the same, but if you’d like to experiment with building components with the composition API, that is available to you as well. You can use both and choose which strategy works best for a given scenario in your application.

Other improvements

The upgrade to Vue 3 contains some other nice benefits, as well. A few notable ones are: speed (Vue 3 is expected to be twice as fast as the previous version); size (Vue 3 has an approximately 100% smaller bundle); and greatly approved TypeScript support.

It’s great to see the continued evolution of this exciting framework. We’re looking forward to building new projects that take advantage of these exciting new features.

Have a project we can help with?
Let's Talk

Get Started Today