The long-awaited Vue.js 3 is scheduled for release in the 1st quarter of 2020. The announcement of a new API, the Composition API, resulted in some controversy in the Vue community. It has since simmered down, but what is the Composition API anyway?

Although Vue 3, in its entirety, hasn't been released yet, the Composition API is already out and ready to use. In this article, we'll take a look at why the Composition API was introduced, how it works, and how it compares to the options-based API. At the end, we'll build two versions of the same component: one with the Composition API and one with the options-based API!

Vue 3 Composition API vs Options API

What is the Composition API

Just to get the obvious concern out of the way, this new API will not break current Vue 2.x options-based code! We can even continue to use the Options API in Vue 3. The Composition API is just an addition to the language that was created to address the limitations of the Options API used in Vue 2. We'll touch on those limitations in the next section.

For now, let's talk about what it is.

Evan You, the creator of Vue, has described the Composition API as a reactive API coupled with the ability to register lifecycle hooks using globally imported functions. This is a little hard to digest, but the main point is that the Composition API provides a different way to manage reactivity at all points in an application, without compromising organization and readability.

At the root of it, the Composition API isn't really adding anything new to the language. Instead, think about it this way: it's exposing some of the internal functions (aka, magic) that Vue already uses, allowing us to use these functions directly in components.

"The Vue 3 Composition API makes Vue internal functions available for use in your Vue applications."

Adam Wathan has an excellent podcast called Full Stack Radio. If you're interested in learning more about the Composition API and Vue 3 in general, I'd highly recommend checking out Episode 129: Evan You — What's Coming in Vue.js 3.0, where Adam talks in-depth about the Composition API with Evan You.

Why the Change?

Before we jump into the comparison, let's look at some of the pain points in Vue 2 that the Composition API aims to solve.

Better extensibility and organization

One major concern among developers was that their Vue projects became hard to manage as they grew in size and complexity.

In Vue 2.x, components are organized using options such as data, mounted, methods, etc. Because of this, logic isn't really grouped by feature, which can make it hard to read through a large and complex file. Readers would often have to scroll back and forth to follow what's going on. Another downside of this organization is that it made logic reuse difficult, as features are split up unintuitively in a component.

The Composition API RFC includes this excellent diagram showing the flow of different concerns in the same component built with both the Options API and Composition API. Each color represents a different logical concern. As shown, using composition functions with the Composition API allows for better organization in components.

Vue 3 Composition API vs Options API code structure

Better TypeScript support

The next issue with Vue 2.x was that the sometimes confusing nature of this inside components often made it difficult to use TypeScript. The Options API relies on a lot of "magic" from the Vue compiler.

The Composition API, however, uses those internal functions directly, so this behaves as expected in JavaScript. This allows for much better TypeScript support when using the Composition API.

Options API vs. Composition API

The best way to understand the changes is to see it in action.

Let's build out a simple component that allows the user to add two numbers with the press of a button. First, we'll quickly review how that would be done with the options-based API. Then we'll rebuild that same component with the Composition API to illustrate the differences.

Vue 3 Composition API sample application

Building with Options API

Here's what we'll need to create the simple addition calculator pictured above:

  • 3 variables — 1st number to add, 2nd number to add, sum
  • 2 input boxes — 1st number to add, 2nd number to add
  • A method to add the two numbers together

And here's how it's done in Vue 2:

<template>
    <div class="add">
        <h3>Addition Calculator</h3>
        <form id="sum">
            <input type="text" class="form-control" v-model="num1">
            <input type="text" class="form-control" v-model="num2">
            <button @click="addNumbers" type="button" class="btn btn-light">
                Add me!
            </button>
        </form>
        <p><strong>Sum:</strong> {{ sum }}</p>
    </div>
</template>
<script>
export default {
    name: 'Add',
    data() {
        return {
            num1: 0,
            num2: 0,
            sum: 0
        };
    },
    methods: {
        addNumbers: function () {
            this.sum = parseInt(this.num1) + parseInt(this.num2);
        }
    }
}
</script>
<style scoped>
    .add {
        background: #d67182;
        box-shadow: 1px 3px 6px #a3a3a3;
        padding: 50px 30px;
        width: 300px;
        text-align: center;
        margin: 80px auto;
    }
    p, h3 {
        color: #FFFFFF;
    }
    #sum {
        margin-top: 25px;
    }
    .form-control {
        margin-bottom: 15px;
    }
    .btn {
        margin-bottom: 30px;
    }
</style>

Note: We can use computed here instead of addNumbers to get the value for sum and forego the button altogether. However, we're just going to put it into a separate method to demonstrate how functions and code structure works in the Composition API.

With the Vue 2 options-based API, we have our reactive data object returned in the data() function. Then the AddNumbers() function will be registered in the methods property. To access the variables from data(), we must use this.

Now, this is nested inside the method, so it should refer to the method's object instead of the entire instance, but it doesn't. Vue is working behind the scenes here to handle this for us. In general, this is pretty straightforward and intuitive, but in more complicated cases, this behind the scenes handling can cause unexpected behavior of this.

This "magic" is also part of what has made TypeScript support tough in Vue 2. Now let's look at how this same component is built with the Vue 3 Composition API.

Building with Composition API

To keep this short, we'll just focus on building the component here. If you'd like to practice with the Composition API and rebuild this on your own, the Composition API can be added into a new Vue 2 project by running the following in your terminal:

vue create project-name
npm install @vue/composition-api 

Then in the main.js file:

import Vue from 'vue';
import VueCompositionApi from '@vue/composition-api';

Vue.use(VueCompositionApi);

For more information on this, check out the official Vue Composition API repo.

Now onto the final act 🎩! Let's rebuild the addition component using the Composition API. We're going to exclude the <style> section because nothing changes there.

<template>
    <div class="add">
        <h3>Addition Calculator</h3>
        <form id="sumComp">
            <input type="text" class="form-control" v-model="num1">
            <input type="text" class="form-control" v-model="num2">
            <button @click="addNumbers" type="button" class="btn btn-light">
            Add me!
        </button>
        </form>
        <p><strong>Sum:</strong> {{ sum }}</p>
    </div>
</template>
<script>
import { ref } from '@vue/composition-api';
export default {
    name: 'AddComposition',
    setup() {
        let num1 = ref(0);
        let num2 = ref(0);
        let sum = ref(0);
        function addNumbers() {
            sum.value = parseInt(num1.value) + parseInt(num2.value);
        }
        return {
            num1,
            num2,
            sum,
            addNumbers
        }
    }
}
</script>

First, we're importing the functions we need to use from the Composition API at the top. In this case, that's just ref.

Next, notice that everything is now inside a setup() function. Any functions or properties that need to be used in the template should go in setup() since this is how they're returned to the template.

Inside setup(), the reactive variables we're using are defined at the top as standalone variables, as opposed to a return object in the data() function.

The function addNumbers() is also hanging out on its own instead of nested in the methods property. We can now easily reuse our functions across component instances, which will significantly improve the readability of large codebases. Notice also that this is no longer needed to reference variables! Using the Composition API functions directly instead of through Vue component options removes the this magic that was occurring behind the scenes.

Finally, we're returning the functions and properties to the template.

Reactivity: ref vs reactive

One new thing here is the use of ref in the variables, e.g., let num1 = ref(0). This is how we're making our variables reactive! There are two functions that can be used to handle state and reactivity: ref and reactive.

ref()

ref, as shown in this example, takes a value and returns a reactive reference object. This object has a single value: .value that points to the value provided. Notice the use of .value on the variables in the addNumbers() method.

Refs can be created directly, as we've done here, or by using computed().

The computed() getter function will return an immutable (can't be changed) ref object thats value can be accessed with .value(). It can also accept an object with a setter function to make it mutable if necessary.

Earlier, we mentioned that it would be better to find the sum of the numbers with computed(), but we kept it in the method for demonstration's sake.

Here's how it would be done using computed():

let sum = computed(() => parseInt(num1.value) + parseInt(num2.value));

Make sure to add computed to the import at the top as well:

import { computed } from '@vue/composition-api';

Note: Because computed() creates a ref, it can be used with primitive values as well. Normally the primitive value would get lost because primitives (essentially non-object values, such as a number) are passed by value, not reference. ref, however, will wrap the value and create an object. The value of the object can be referenced with ref.value. And then, when the computed() value changes, ref.value will change as well.

reactive()

The next way to handle reactivity is by using reactive. This is equivalent to Vue.observable() from Vue 2.x. It accepts an object and returns a proxy to that object that will be tracked. In this example, we're using ref for reactivity, but we could easily use reactive instead, like so:

let state = reactive({ 
    num1: 0,
    num2: 0,
    sum: 0
})

If done this way, then addNumbers() would change to:

function addNumbers() {
    sum = parseInt(num1) + parseInt(num2);
}

This was a very simple example of a component built with the Composition API, but hopefully it has demonstrated how the new API can lead to better code organization and less "magic".

While waiting for Vue 3 to come out, why not experiment with adding authentication to your Vue 2 application? Follow the example below to add authentication to a Vue.js + Node.js application or check out this beginner tutorial to build an events showcase app using the Vue Options API. Remember, the Options API will still be useable in Vue 3, so there's no harm at all in starting with Vue 2 now.

Aside: Authenticate a Vue App and Node API with Auth0

We can protect our applications and APIs so that only authenticated users can access them. Let's explore how to do this with a Vue application and a Node API using Auth0. You can clone this sample app and API from the vue-auth0-aside repo on GitHub.

Auth0 login screen

Features

The sample Vue application and API has the following features:

  • Vue application generated with Vue CLI and served at http://localhost:4200
  • Authentication with auth0.js using the Auth0 login page
  • Node server protected API route http://localhost:3001/api/meetups/private returns JSON data for authenticated GET requests
  • Vue app fetches data from API once user is authenticated with Auth0
  • Authentication service uses a subject to propagate authentication status events to the entire app.
  • Access token and token expiration are stored in local storage and removed upon logout.

Sign Up for Auth0

You'll need an Auth0 account to manage authentication. You can sign up for a free account here. Next, set up an Auth0 application and API so Auth0 can interface with a Vue.js app and Node API.

Set Up an Auth0 Application

  1. Go to your Auth0 Dashboard and click the "create a new application" button.
  2. Name your new app and select "Single Page Web Applications".
  3. In the Settings for your new Auth0 application, add http://localhost:8080/callback to the Allowed Callback URLs.
  4. Scroll down to the bottom of the Settings section and click "Show Advanced Settings". Choose the OAuth tab and verify that the JsonWebToken Signature Algorithm is set to RS256.
  5. If you'd like, you can set up some social connections. You can then enable them for your app in the Application options under the Connections tab. The example shown in the screenshot above utilizes username/password database, Facebook, Google, and Twitter. For production, make sure you set up your own social keys and do not leave social connections set to use Auth0 dev keys.

Set Up an API

  1. Go to APIs in your Auth0 dashboard and click on the "Create API" button. Enter a name for the API. Set the Identifier to a URL. In this example, this is http://meetupapi.com/. The Signing Algorithm should be RS256.
  2. You can consult the Node.js example under the Quick Start tab in your new API's settings. We'll implement our Node API in this fashion, using Express, express-jwt, and jwks-rsa.

We're now ready to implement Auth0 authentication on both our Vue client and Node backend API.

Dependencies and Setup

The Vue app utilizes the Vue.js CLI. Make sure you have the CLI installed globally:

$ npm install -g vue-cli

Once you've cloned the project, install the Node dependencies for both the Vue app and the Node server by running the following commands in the root of your project folder:

$ npm install
$ cd server
$ npm install

The Node API is located in the /server folder at the root of our sample application.

Find the config.js.example file and remove the .example extension from the filename. Then open the file:

// server/config.js
// (formerly config.js.example)
module.exports = {
  CLIENT_DOMAIN: '[CLIENT_DOMAIN]', // e.g. 'you.auth0.com'
  AUTH0_AUDIENCE: 'http://meetupapi.com'
};

Change the CLIENT_DOMAIN variable to your Auth0 application domain and set the AUTH0_AUDIENCE to your audience (in this example, this is http://meetupapi.com). The /api/examples/private route will be protected with express-jwt and jwks-rsa.

Note: To learn more about RS256 and JSON Web Key Set, read Navigating RS256 and JWKS.

Our app and API are now set up. They can be served by running npm run dev from the root folder and node server.js from the /server folder.

With the Node API and the Vue.js app running, let's take a look at how authentication is implemented.

Authentication Service

Authentication logic on the front end is handled with an Auth authentication service: src/auth/Auth.js file.

// src/auth/Auth.js
/* eslint-disable */
import auth0 from 'auth0-js';
import router from '../router';

export default class Auth {

  auth0 = new auth0.WebAuth({
    domain: AUTH0_DOMAIN, // e.g., you.auth0.com
    clientID: AUTH0_CLIENT_ID, // e.g., i473732832832cfgajHYEUqiqwq
    redirectUri: CALLBACK_URL, // e.g., http://localhost:8080/callback
    audience: AUTH0_API_AUDIENCE, // e.g., https://meetupapi.com
    responseType: 'token',
    scope: 'openid'
  });

  constructor() {
    this.login = this.login.bind(this);
    this.handleAuthentication = this.handleAuthentication.bind(this);
    this.logout = this.logout.bind(this);
  }

  handleAuthentication() {
    this.auth0.parseHash((err, authResult) => {
      if (authResult && authResult.accessToken) {
        this.setSession(authResult);
        router.replace('/');
      } else if (err) {
        router.replace('/');
      }
    })
  }

  setSession(authResult) {
    // Set the time that the access token will expire at
    const expiresAt = JSON.stringify(authResult.expiresIn * 1000 + new Date().getTime());
    localStorage.setItem('access_token', authResult.accessToken);
    localStorage.setItem('expires_at', expiresAt);
  }

  requireAuth(to, from, next) {
    if (! (new Auth).isAuthenticated()) {
      next({
        path: '/',
        query: { redirect: to.fullPath }
      });
    } else {
      next();
    }
  } 


  login() {
    this.auth0.authorize();
  }

  logout() {
    // Clear access token and expiration from local storage
    localStorage.removeItem('access_token');
    localStorage.removeItem('expires_at');
    // navigate to the landing page route
    router.go('/');
  }

  isAuthenticated() {
    // Check whether the current time is past the
    // access token's expiry time
    const expiresAt = JSON.parse(localStorage.getItem('expires_at'));
    return new Date().getTime() < expiresAt;
  }
}

Replace the constants, AUTH0_DOMAIN, AUTH0_CLIENT_ID, AUTH0_API_AUDIENCE with values from your Auth0 dashboard. Replace CALLBACK_URL with http://localhost:8080/callback.

The login() method authorizes the authentication request with Auth0. An Auth0 login page will be shown to the user and they can then log in.

Note: If it's the user's first visit to our app and our callback is on localhost, they'll also be presented with a consent screen where they can grant access to our API. A first party application on a non-localhost domain would be highly trusted, so the consent dialog would not be presented in this case. You can modify this by editing your Auth0 Dashboard API Settings. Look for the "Allow Skipping User Consent" toggle.

We'll receive accessToken and expiresIn in the hash from Auth0 when returning to our app. The handleAuthentication() method uses Auth0's parseHash() method callback to set the session (setSession()) by saving the tokens, and token expiration to local storage. The isAuthenticated method informs the components in the app about the user's authentication status via checking the access token's expiry time.

Finally, we have a logout() method that clears data from local storage.

Callback Component

The callback component is where the app is redirected after authentication. This component simply shows a loading message until the login process is completed. It executes the handleAuthentication() method to parse the hash and extract authentication information.


  // src/components/Callback.vue
  <template>
    <div>
        <h3>Loading....</h3>
    </div>
  </template>
  <script>

  import Auth from '../auth/Auth.js';

  const auth = new Auth();

  export default {
    name: '',
    mounted() {
      this.$nextTick(() => {
        auth.handleAuthentication();
      });
    },
  };
  </script>

Making Authenticated API Requests

In order to make authenticated HTTP requests, we need to add an Authorization header with the access token in our meetup-api.js file.

  // utils/meetup-api.js
  /* eslint-disable */
  import axios from 'axios';
  import Auth from '../src/auth/Auth.js';

  const auth = new Auth();
  const BASE_URL = 'http://localhost:3333';

  export function getPublicMeetups() {
    const url = `${BASE_URL}/api/meetups/public`;
    return axios.get(url).then(response => response.data).catch(err =>  err || 'Unable to retrieve data');
  }

  export function getPrivateMeetups() {
    const url = `${BASE_URL}/api/meetups/private`;
    return axios.get(url, { headers: { Authorization: `Bearer ${localStorage.getItem('access_token')}` }}).then(response => response.data).catch(err => err || 'Unable to retrieve data');
  }

Final Touches: Route Guard and Private Meetups Page

A Private meetup page component can show information about private meetups. However, we only want this component to be accessible if the user is logged in.

The route guard is implemented on specific routes of our choosing in the router/index.js file like so:

// src/router/index.js
import Vue from 'vue';
import Router from 'vue-router';
import PublicMeetups from '@/components/PublicMeetups';
import PrivateMeetups from '@/components/PrivateMeetups';
import Callback from '@/components/Callback';
import Auth from '../auth/Auth';

const auth = new Auth();

Vue.use(Router);

export default new Router({
  mode: 'history',
  routes: [
    {
      path: '/',
      name: 'PublicMeetups',
      component: PublicMeetups,
    },
    {
      path: '/private-meetups',
      name: 'PrivateMeetups',
      beforeEnter: auth.requireAuth,
      component: PrivateMeetups,
    },
    {
      path: '/callback',
      component: Callback,
    },
  ],
});

More Resources

That's it! We have an authenticated Node API and Vue.js application with login, logout, and protected routes. To learn more, check out the following resources:

Summary

We've now gone over what the Composition API is, why it was necessary, and how you can build a component using it. The Composition API is already published and ready to use, so make sure you go check it out on GitHub! Let me know below if you'll be trying out the Composition API or sticking to the options-based API. Thanks for reading!