Making Sense of Vue 3’s Composition API

Making Sense of Vue 3’s Composition API

  • 731

Making Sense of Vue 3’s Composition API .Exploring the new way to structure components in Vue applications .Vue has been around for a while now. It has been gaining immense popularity within the developer community and has managed to grab the attention of many react/angular developers

Introduction

Vue has been around for a while now. It has been gaining immense popularity within the developer community and has managed to grab the attention of many react/angular developers. Much of this was owed to the absolutely simple and intuitive component APIs that exist in Vue. With Vue 3, however, the makers have decided to change(or introduce an alternative) the way we make components in Vue. This is the composition API.

When initially released, it received mixed reactions from the community but slowly and surely, most Vue developers have come to realise how easy it is to use and how clean it would make our application code. Let’s dig into it now and see how it compares with the standard options API from previous Vue versions.

Disclaimer: This tutorial assumes prior knowledge of javascript and VueJS. If you’re not familiar with them, I would highly recommend learning them - They’re awesome!

Setting up our App

You can clone the initial code repo from here. I’ve simply created a normal signup page with a username and a password field and a submit button that could potentially make an API call to create the user. It also provides the users with the option to change the language between English and Spanish(Español). All of these features are implemented using the options API.

This is image title

For demo purposes, the API used in this case is the JSONPlaceholder API that mocks post requests.

Everything of concern happens in the App.vue file itself. Let’s take a look at it:

<template>
  <div id="app">
    <div class="form">
      <label for="username">{{dictionary.usernameLabel}}:</label>
      <input id="username" class="input" :value="username" @input="updateUsername" />
      <label for="password">{{dictionary.passwordLabel}}:</label>
      <input id="password" type="password" class="input"
      :value="password" @input="updatePassword" />
      <button class="submit" @click="submit">{{dictionary.signupLabel}}</button>
    </div>
    <div class="language-container">
      <label for="language">{{dictionary.languageLabel}}:  </label>
      <input type="radio" name="language" value="en" v-model="language"/>English
      <input type="radio" name="language" value="es" v-model="language"/>Español
    </div>
  </div>
</template>

<script>
import axios from 'axios';
import en from './constants/en.json';
import es from './constants/es.json';
export default {
  name: 'app',
  data() {
    return {
      username: '',
      password: '',
      language: 'en',
    };
  },
  methods: {
    updateUsername(event) {
      this.username = event.target.value;
    },
    updatePassword(event) {
      this.password = event.target.value;
    },
    async submit() {
      // probably add some other business logic here :)
      await axios.post('https://jsonplaceholder.typicode.com/users', {
        username: this.username,
        password: this.password,
      });
      alert('User created');
    },
  },
  computed: {
    dictionary() {
      return this.language === 'es' ? es : en;
    },
  },
};
</script>

<style>
// some custom styles here
</style>

App.vue

We’ve created two fields in the data that correspond to the two input fields in the app; username and password. We also have update methods for each of these data fields; namely, updateUsername and updatePassword.

Note: I am aware that we could have used v-modal here but the idea of this article is to show how verbose code can become when using the options API

We also have a computed property, dictionary , that updated the language strings based on the selected language.

Once the form is filled, the user can click the “Sign up” button that calls the submit method in the component’s options. This function would typically contain some business logic corresponding to the application but at the moment, let us just make a call to the JSON placeholder API and display an alert is everything goes well.

The Problem

So, at first glance, this code looks pretty good. It is something we are all familiar/comfortable with and has never really failed us.

However, if you look at the application from a “features” perspective, you will notice that we have a signup feature. Despite being part of a single feature, most of the elements, like the data fields, the update and submit methods are part of different options. As the application grows and we have a couple more features in our App component, this could get a little messy because all the code corresponding to a given feature is scattered between the different component options.

The creators of Vue have recognised this issue faced in some fairly large Vue projects and have proposed to create the composition api which solves this problem.

Note: If you’re familiar with React, the composition api, simply put, is the Vue version of React hooks

Let’s use this new API in our application:

Using the Composition API

In the new API, Vue allows us to create just one option , namely a setup method that “sets” up our component. They have also provided us with hooks into the native functionality like the data, computed, and the lifecycle method options from the old API.
To understand this better, let’s refactor our current app to use the composition API.

We need to first install the composition api package and register it in our app:

yarn add @vue/composition-api

Register this plugin in your main.js :

import Vue from 'vue';
// Import the composition API plugin
import VueCompositionApi from '@vue/composition-api';
import App from './App.vue';

// Register the composition API
Vue.use(VueCompositionApi);

Vue.config.productionTip = false;

new Vue({
  render: h => h(App),
}).$mount('#app');

main.js

Note: The composition API is going to be available by default in Vue 3. You will not have to register a plugin every time. But since Vue 3 is in alpha at . the time of writing this article and we’re using Vue 2, we need to register it.

Let’s Refactor 💪

We will begin our refactoring with the most trivial way of maintaining reactivity in Vue apps - The data attribute.

In Vue, we have two choices of maintaining this; ref and reactive. Typically, you would use refs when you need to make a single primitive value to have reactivity. reactive can be used when we have something like an entire object to maintain its reactivity. In our case, the inputs we use with be refs.

Once refactored, the App.vue file should look like this:

<template>
  <div id="app">
    <div class="form">
      <label for="username">{{dictionary.usernameLabel}}:</label>
      <input id="username" class="input" :value="username" @input="updateUsername" />
      <label for="password">{{dictionary.passwordLabel}}:</label>
      <input id="password" type="password" class="input"
      :value="password" @input="updatePassword" />
      <button class="submit" @click="submit">{{dictionary.signupLabel}}</button>
    </div>
    <div class="language-container">
      <label for="language">{{dictionary.languageLabel}}:  </label>
      <input type="radio" name="language" value="en" v-model="language"/>English
      <input type="radio" name="language" value="es" v-model="language"/>Español
    </div>
  </div>
</template>

<script>
import axios from 'axios';
import { ref, computed } from '@vue/composition-api';
import en from './constants/en.json';
import es from './constants/es.json';
export default {
  name: 'app',
  setup() {
    const username = ref('');
    const password = ref('');
    const language = ref('en');
    const updateUsername = (event) => {
      username.value = event.target.value;
    };
    const updatePassword = (event) => {
      password.value = event.target.value;
    };
    const submit = async () => {
      // probably add some other business logic here :)
      await axios.post('https://jsonplaceholder.typicode.com/users', {
        username,
        password,
      });
      alert('User created');
    };
    const dictionary = computed(() => (language.value === 'es' ? es : en));
    return {
      username,
      password,
      language,
      updateUsername,
      updatePassword,
      submit,
      dictionary,
    };
  },
};
</script>

<style>
// some custom styles here
</style>

App-composition.vue

As you can see, all our code now resides in just one setup function. We have used ref for maintaining the reactivity of the username, password and the language keys. Our methods have now just become normal functions and we have used the computed hook to evaluate the value of our dictionary based on language's value.

Few things to note here:
1. We have gotten rid of all the “mysteries” from the old options API. No this, you’re free to use arrow functions now. This is purely just functional programming now.
2. You can access only the things you “return” from your setup method in your template. This makes things more readable.
3. When you use the value of refs in the template, simply using the name suffices; but in the setup method, we need to use ref.value. I believe this is something that the core team is working on.
4. All the “parts” of a feature that were earlier scattered across the different “options” can now be colocated. Great right?!

For the people who are still unimpressed, there’s one more step we can go from here. The composition API allows us to “compose” different features and then use them in our component.

To understand this better, let’s move our login and language features into two different composition functions.

We could create a useLogin composition function for the login flow and a useLanguage for the language feature:

Note: Here, we are creating the two functions in the same file but ideally, you would create them in separate files dedicated to each composition function; That was you will be able to share the compositions between different components

<template>
  <div id="app">
    <div class="form">
      <label for="username">{{dictionary.usernameLabel}}:</label>
      <input id="username" class="input" :value="username" @input="updateUsername" />
      <label for="password">{{dictionary.passwordLabel}}:</label>
      <input id="password" type="password" class="input"
      :value="password" @input="updatePassword" />
      <button class="submit" @click="submit">{{dictionary.signupLabel}}</button>
    </div>
    <div class="language-container">
      <label for="language">{{dictionary.languageLabel}}:  </label>
      <input type="radio" name="language" value="en" v-model="language"/>English
      <input type="radio" name="language" value="es" v-model="language"/>Español
    </div>
  </div>
</template>

<script>
import axios from 'axios';
import { ref, computed } from '@vue/composition-api';
import en from './constants/en.json';
import es from './constants/es.json';
const useLogin = () => {
  const username = ref('');
  const password = ref('');
  const updateUsername = (event) => {
    username.value = event.target.value;
  };
  const updatePassword = (event) => {
    password.value = event.target.value;
  };
  const submit = async () => {
    // probably add some other business logic here :)
    await axios.post('https://jsonplaceholder.typicode.com/users', {
      username,
      password,
    });
    alert('User created');
  };
  return {
    username,
    password,
    updateUsername,
    updatePassword,
    submit,
  };
};
const useLanguage = () => {
  const language = ref('en');
  const dictionary = computed(() => (language.value === 'es' ? es : en));
  return {
    language,
    dictionary,
  };
};
export default {
  name: 'app',
  setup() {
    return {
      ...useLogin(),
      ...useLanguage(),
    };
  },
};
</script>

<style>
// some custom styles here
</style>

App-composition-modular.vue

As you can see, the setup function now is very small and readable. This way, you can extract tiny features as composition functions and compose them together in different combinations in different components!!

Summing up

Well, we’ve seen how the composition API works and seen a few ways it can be implemented. It’s about the right time to discuss the value that this has brought:

  1. We are writing code in a more functional manner. It is the style that Javascript prescribes. It’s easier to modularise and reuse such code. For example, our useLanguage function can be used in multiple components that require such translation.
  2. The previous options API organised code by functionality (data, methods, etc)and not features (like Login and Language). This felt fine while developing simple components. But as the component grew, tracking all parts of a feature became a tough job. The feature-wise separation of concerns brought with the composition API makes code more readable.
  3. Unit testing(another blog post about this soon?) these new composition functions becomes a breeze because, as mentioned earlier, they take away most of the esoteric parts of the previous API. No more this and that 😂

I hope that this article served as a good primer to the new composition API and got you excited about Vue 3.

Hit me up on twitter or leave any suggestions/views in the comments below 👇

Cheers ☕️