olamileke.dev

All ArticlesGuidesSide Projects

olamileke.dev

All ArticlesGuidesSide Projects
Guides

Vue JS - A Comparison: Options API vs Composition API

#typescript

#vue

Fambegbe Olamileke. 2023/08/27, 10:04 pm

In my frontend journey, I started out using JQuery along with AJAX to perform DOM manipulation and load data without page reloads. I typically did this with full stack frameworks like Laravel and Express. Eventually though, I came up against the limits of JQuery. It was fine for sprinkling a bit of interactivity and making a few AJAX calls but in trying to do anything more I would always run into issues. At this point, I decided to test new waters and check out tools much more suited to the single page application experiences I was looking to create, I am a firm believer in using the best tool/framework/language for the job at hand. I learned Angular 2 while also dabbling along the side in Angular JS (Angular 1). I used this for about a year and while I didn't have particular complaints about Angular, I quickly realized that there were a lot more React opportunities. As a result, I pivoted to learn React and in the ensuing 3 years have written a lot of React and Next JS. Recently though, to scratch my itch to learn something new, I made the decision to learn Vue JS.


Introduction

Vue is a lightweight Javascript framework for building user interfaces. It makes use of an MVVM (Model-View-View-Model) paradigm to enable the building of rich, dynamic and reactive web interfaces. The MVVM paradigm simply refers to a two way data binding between the model (Javascript layer) and the view (HTML/Presentation layer). Essentially Vue provides a way to update the user interface rendered in the browser dynamically by responding to changes in the associated model. The model (Javascript) and view (HTML) are synced or bound together, thus making it easy to build reactive interfaces that can infinitely change or render new content without reloading the page. Vue also does quite well in cases where a full blown single page application is not needed and the goal is to introduce reactivity or simply to make specific parts of the page dynamic. Take the example of a website server side rendering HTML with Django via its native templating language. Say this website needed to have a chat widget through which customers could communicate with the site admin. Now obviously, you don't expect the page to reload every time you send or receive a new message. You expect to be able to send and receive messages real time. In a scenario like this, Vue can be used to target the section of the page containing the chat widget so as to handle the chat experience. This is just one such example of the many possible ways that Vue can be used due to its flexibility. You can either go the distance and create fully featured single page applications or use it to enhance already existing applications by introducing reactivity, the choice as ever, depends on the use case and situation at hand.


Component APIs

Vue provides two different APIs through which components can be authored. These are the Options API and Composition API. This can be likened to the way React has both class based component and hooks/functional component approaches. They both achieve the same purposes, making use of the same Vue concepts, but go about it in different ways. Vue 2.7 (Vue 3 is the current version) introduced the Composition API as a modern and better standard for creating Vue components. The major goal of this article is to compare and contrast the two different APIs, while also drawing parallels with React in the process. As I typically do when learning new tools, I built something to help me understand the Component APIs and also just Vue in general. If you want to skip to the application, here is a link to it while the code is here if anyone would like to have a look.


The Options API

This is the traditional way in which Vue components are created. It involves defining a component's configuration as an object of options. Here is an example of a component defined using the Options API


    
// Options API Example
<script>
import Task from './Task.vue'

export default {
    props: ['type'],
    inject: ['tasks'],
    components: {
        Task
    },
    methods: {
        updateTask(updatedTask) {
            // update task implementation
        },
        deleteTask(id) {
            // delete task implementation
        },
    },
    computed: {
        displayedTasks() {
            if (this.type === 'completed') return this.tasks.filter((task) => task?.is_completed)
            if (this.type === 'incomplete') return this.tasks.filter((task) => !task?.is_completed)
            return this.tasks
        }
    },
    mounted() {
        console.log('Here are the tasks available in the tasks component', this.tasks)
    },
}
</script>

<template>
    <Task v-for="task in displayedTasks" :key="task.id" v-bind="task" />
</template>
    


It leans into Vue's simplicity in that everything relating to the component is properly organized and easily understandable at a glance. Its quite clear and obvious

  • the props the component is expecting
  • the data injected from further up in the component tree,
  • the components registered to be made use of in this component
  • the different methods or functions that can be triggered in the component's template (presentation layer/HTML) via different HTML events (click, submit)
  • the component's computed properties (properties derived from reactive/stateful values)
  • the mounted method, which is called when the component mounts


It makes use of an object-oriented paradigm and its mental model may be familiar to individuals coming from object oriented programming languages such as Java. Properties and methods of the component are exposed on "this", which refers to the current instance of the component mounted at the time. It can be compared to class components in React, which were the initial way of creating React components before the React Hooks API was introduced. Also, while the example above does not show it, component variables (both reactive/stateful and non-reactive) are defined with the data method of the options object. Take for example a component that renders an array of fruits.


    
<script>
export default {
    data() {
        return {
            fruits: ["Apple", "Mango", "Coconut", "Orange"], 
        } 
    }
}
</script>
    


These fruits can then be rendered in the component's template just like with the tasks above


    
<template>
    <div class="flex gap-2 w-[640px] mx-auto mt-10">
        <p v-for="(fruit, index) in fruits" :key="index">{{ fruit }}</p>
    </div>
</template>
    


As one can see, in the template, you dont need to access the variables in the template via 'this'. Vue automatically accesses the variables from the current component instance.


Simple as it is, the Options API has several drawbacks. The tasks example above works quite well because its a simple, straight forward component without many options and external dependencies. However, the more complex a component becomes, the harder it is to manage it with the Options API. Say for the tasks example, we wanted to add more methods or add more props and define computed properties based on them. Over time, its quite clear how this can turn into an unwieldy mess. While Vue does provide mixins which are a means through which we can define re-usable logic, they have several drawbacks which I will explain below but first I'll explain what mixins are. For example, if we have a common set of methods being used in multiple components, to avoid code duplication we can extract these methods into a mixin and use in the components. Let's use the same tasks example. Say we wanted to implement this CRUD functionality with the tasks in multiple components, we can define the functionality in a mixin


    
// TaskMixin.js
export default { 
    data () {
        return { 
            tasks: []
        }
    },
    methods: {
        addTask(task) {
            this.tasks = [...this.tasks, task]
        },
        updateTask(updatedTask) {
            const index = this.tasks.findIndex(task => task.id === updatedTask.id);
            if (index >= 0)
                this.tasks[index] = updatedTask;
        },
        deleteTask(id) {
            this.tasks = this.tasks.filter(task => task.id !== id);
        }
    }
}
    


We can then use this mixin in our component like this


    
<script>
import TaskMixin from "../mixins/TaskMixin.js";
export default {
    mixins: [TaskMixin]
}
</script>
    


And this component and any other component that imports this mixin will have all the data and methods defined in the mixin.


While mixins do help with code re-usability, a notable issue with them is that they cannot accept parameters. As such, except the different components using the mixin work in exactly the same way, mixins are not much use. They cannot be customized to suit the use case of the consuming component. The data defined in mixins can also be mistakenly mutated by the consuming component leading to weird, hard to trace errors. Also, sometimes you may have a complex component which does not necessarily share functionality with other components, in which case, mixins are not of any help.


The Composition API

This was introduced in Vue 2.7 and aims to address the issues with the Options API when building larger applications. It employs a much more functional style in that components are defined with predefined, imported functions instead of with an object of options. Here is a the same component defined above with the Options API, defined here with the Composition API.


    
// Composition API Example
<script setup lang="ts">
import { inject, computed, onMounted } from 'vue';
import { TTask } from '../../types';
import Task from './Task.vue';

const props = defineProps<{ type: string }>()
const tasks: { value: TTask[] } = inject('tasks')
const displayedTasks = computed(() => {
    if (props.type === 'completed') return tasks.value.filter((task) => task?.is_completed)
    if (props.type === 'incomplete') return tasks.value.filter((task) => !task?.is_completed)
    return tasks.value
})
onMounted(() => {
    console.log("Here are the tasks available in the tasks component", tasks)
})
</script>

<template>
    <Task v-for="task in displayedTasks" :key="task.id" v-bind="task" />
</template>
    


It aims to provide a developer experience much closer to writing normal Javascript or Typescript. Variables (reactive/stateful or non reactive) and methods are defined with the usual 'const' syntax. Going through and understanding the component above should come naturally to an individual with Javascript experience who has no Vue experience. Unlike the Options API, which is uniquely a Vue concept. It also does not enforce organizational constraints in how and where definitions in the component should be placed. This was a defining feature of the Options API, it told you where exactly the props, the component's methods and data should be. Instead, the Composition API is much more flexible and these definitions can be defined in any order and manner. As a result, related definitions can easily be grouped together for components that are responsible for multiple operations.


Also, reactive variables in the Composition API can be defined with the 'ref' function. The fruits component example from above can be re-written as


    
<script setup lang="ts">
import { ref } from "vue";

const fruits = ref(["Apple", "Mango", "Coconut", "Orange"])
</script>

<template>
    <div class="flex gap-2 w-[640px] mx-auto mt-10">
        <p v-for="(fruit, index) in fruits" :key="index">{{ fruit }}</p>
    </div>
</template>
    


One important quirk though with reactive variables defined with 'ref' is that to access its underlying value and to set or change the value after the initial assignment, we need to make use of the value property on the variable. For example, a function to add a new fruit in the above component will be defined as


    
<script setup lang="ts">
const addFruit = (fruit: string) => {
    fruits.value = [...fruits.value, fruit]
}
</script>
    


Also, just like with the Options API, when accessing the reactive variables in the component's template, you don't need to specify the value property, ('fruits.value'). This is because Vue automatically unpacks reactive variables in templates.

Apart from 'ref', reactive variables can also be defined with the 'reactive' function. However, for reasons I won't get into, 'reactive' can only be used with objects and not with scalar variables, arrays or other data types.


Finally, the Composition API provides a very useful paradigm called composables for re-using logic across multiple components. This is similar to the concept of mixins in the Options API but because of the flexibility offered by the Composition API is much more powerful. Take the example of this component fetching remote data as taken from the official Vue documentation.


    
<script setup lang="ts">
import { ref } from "vue";

const data = ref(null);
const error = ref(null);

fetch('...')
.then((res) => res.json())
.then((json) => (data.value = json))
.catch((err) => (error.value = err))
</script>

<template>
    <div v-if="error">Oops! Error encountered: {{ error.message }}</div>
    <div v-else-if="data">
        Data loaded: 
        <pre>{{ data }}</pre>
    </div>
    <div v-else>Loading...</div>
</template>
    


In most cases, you are fetching remote data in multiple components in your application. Ideally, you don't want to have to be duplicating this logic in every component where a remote api call is made. You would want to figure out a way to extract this into a standalone utility or function that can then be called in the component. This is where composables come in. The above data fetching logic can be extracted into a useFetch composable.


    
// fetch.ts
import { ref } from "vue";

export function useFetch(url: string) {
    const data = ref(null);
    const error = ref(null);
    
    fetch(url)
    .then((res) => res.json())
    .then((json) => (data.value = json))
    .catch((err) => (error.value = err))

    return { data, error }
}
    


We can then refactor the component to


    
<script setup lang="ts">
import { useFetch } from "fetch.ts"

const { data, error } = useFetch("...")
</script>
    


This is one such example demonstrating what composables can do. You can define reactive variables, define computed properties and watchers on these reactive variables, access the different lifecycle hooks, basically anything you would typically do in a component. This makes them extremely powerful and flexible because literally any component operation can be extracted out into a composable. They are very similar to React hooks in the sense of being able to perform component operations and access other hooks/composables without being components themselves.


Conclusion

The Options and Composition APIs are both two different ways of authoring Vue components. While the Composition API may be newer and a better standard, with much greater flexibility, the Options API is not going anywhere. In smaller to medium sized applications, the Options API would work just fine. However, with larger applications, its advised to use the Composition API because it solves much of the flexiblity issues that the Options API has. It was a nice experience for me learning about Vue and digging into its internals. Here is a link to the application I built to understand Vue and its respective APIs while its code is here if anyone wants to have a look.


Share this article

More from Guides

olamileke.dev © 2024