Writing maintainable code is of paramount importance when building a scalable Vue.js application. The traditional Options API is good but often leads to messy code. Here is why Vue.js 3 introduced the Composition API as a powerful tool for achieving both scalability and maintainability.
In this guide with insights and experience from the Intelligent Information Management Group of Konica Minolta’s Solutions Development Centre, you will learn what the Composition API is and understand why it is ideal for building scalable applications, and dig into best practices to unleash its full potential.
What Is the Composition API in Vue?
Composition API is a new way of structuring Vue.js components introduced with Vue 3, in 2022.
In detail, it is a set of APIs that allows you to build Vue components by defining and importing functions instead of declaring options.
“Composition API” is an umbrella term that contains:
- Reactivity API: To create reactive state, computed state, and watchers. For instance, you can use the reactive() function to trigger a re-render of the component when some data changes.
- Lifecycle Hooks: Individual functions that you can use inside the setup() function to programmatically hook into the component lifecycle. For example, you can use the onMounted() hook to perform specific actions when the component is mounted on the page.
- Dependency Injection: To share data between parent and child components without the need for complex prop drilling, via the provide() and inject() functions.
Its goal is to address some challenges and limitations that developers face with complex component logic in the Options API, the traditional way of writing components in Vue 2.
Consider the Vue component below, developed with the Options API:
As shown by the colors, the code is not uniformly organized. The sort logic (in yellow) is split into three different sections. This makes the component less readable and more difficult to maintain.
Now, let’s take a look at the equivalent component built with the Composition API:
As you can see, related logic is now encapsulated together. Thus, this API promotes a function-based approach to enable reusability and achieve more maintainable code.
If you are wondering what the setup keyword after script is, Vue 3 introduced the <script setup> syntax as a compile-time instruction for using Composition API inside Single-File Components (SFCs).
Why Use the Vue 3’s Composition API?
There are five good reasons to embrace this new API.
Flexible Adoption With Gradual Migration
The Composition API and the Options API can coexist. This means that you can build new components or rewrite some of them using the Composition API while keeping the rest of the project in the Options API. You do not need a full project rewrite to transition to the Composition API, but you can do it slowly and over time.
Better Code Organization
With the Options API, complex logic is often split across different options blocks, making it challenging to achieve separation of concerns. Instead, the Composition API is all about organizing code more logically and cohesively as you can see in the image below:
Grouping related code together helps you achieve more maintainable and readable code.
Increased Reusability
In Vue.js 2, mixins were the recommended approach to distribute reusable functionalities across components. However, they suffer from namespace collisions and lead to hard-to-understand coupled code. That is why the Composition API has introduced composable functions as a mechanism to facilitate reusability while overcoming all the drawbacks of mixins. In Vue 3, a composable is a function that encapsulates and can reuse stateful logic.
Improved Type Inference in TypeScript
The Composition API is designed to integrate seamlessly with TypeScript, providing better type inference and reducing the need for manual type hints. Since it relies on simple variables and functions, TypeScript can infer types more effectively, making it easier for developers to write robust and type-safe code.
Smaller Bundles
Components written with the Composition API and <script setup> generate smaller production bundles compared to their Options API equivalents. The reason is that the templates inside <script setup> are compiled as inline functions within the same scope, allowing direct access to variables without the need for an instance proxy, which results in better minification.
How to Use the Composition API to Improve Your Vue.js Project
The inherent simplicity of the Options API discourages some developers from trying the Composition API. The main reason is that the latter does not provide a guided approach to coding. Its more flexible nature can disorient users, especially at the beginning.
All you need to get the most out of it are some great Composition API best practices. Let’s see them all!
1. Encapsulate Logic in Composables for Reusability
Composables are key to creating business logic or utility modules that help you reduce code duplication. Encapsulate related logic into composable functions to create reusable units that can be shared across multiple components.
For example, you can create a useEventListener() composable to handle events:
// event-listener.js
import { onMounted, onUnmounted } from 'vue'
export function useEventListener(target, event, callback) {
// register the event lister when the component gets mounted
// and unregister it when the component no longer exists
onMounted(() => target.addEventListener(event, callback))
onUnmounted(() => target.removeEventListener(event, callback))
}
Code language: JavaScript (javascript)
Keep in mind that the naming convention for composable functions involves a camel case name that starts with “use.”
Then, you can that function in any component:
<template>
<div>Mouse position is ({{ x }}, {{ y }})</div>
</template>
<script setup>
import { useEventListener } from './event-listener.js'
// define some reactive state to keep track of
// the data returned by the composable
const x = ref(0)
const y = ref(0)
// the event listener callback
function update(event) {
x.value = event.pageX
y.value = event.pageY
}
// register the event listener
useEventListener(window, "mousemove", update)
return { x, y }
</script>
Code language: HTML, XML (xml)
All you have to do is import the composable and then call it to perform the desired action.
2. Take Advantage of the Reactive State for Dynamic Updates
The recommended way to use the reactive state is through the ref() function, which takes the argument and returns it wrapped within an object with a value property. Whenever a ref is modified, Vue triggers a re-render of only the components that rely on it. Take advantage of this feature to build a dynamic and performance-oriented user experience.
When it comes to storing non-primitive values, you can also use the reactive() API. Unlike ref(), this works only with objects. See it in action in the example below:
<template>
<button @click="increment">Increment</button>
<p>Count: {{ state.count }}</p>
</template>
<script setup>
import { reactive } from 'vue'
const state = reactive({
count: 0,
})
const increment = () => {
state.count++
}
</script>
Code language: HTML, XML (xml)
This code defines a Vue.js component with a button. When clicked, the count variable gets incremented and the component is re-rendered to display the current count value.
3. Derive Data with Computed Properties for Efficient Calculations
Computed properties enable you to perform automatic calculations based on other data. You can define them with computed(), which takes a getter function and returns its value in a read-only reactive ref object. In other words, computed properties are functions that automatically update when their dependent reactive data changes. Harness this functionality for caching values automatically generated when their reactive dependencies change.
Use computed properties as in the following examples:
<template>
<div>
<button @click="increment">Increment width</button>
<label>Width:</label>
<input v-model="state.width" type="number" />
<label>Height:</label>
<input v-model="state.height" type="number" />
<p>Area: {{ area }}</p>
</div>
</template>
<script setup>
import { reactive, computed } from 'vue'
const state = reactive({
width: 10,
height: 5,
})
const increment = () => {
state.width++
}
// create a property that automatically updates
// when the reactive state changes
const area = computed(() => state.width * state.height)
return { state, area }
</script>
Code language: HTML, XML (xml)
The first time the component renders, area will store 50 (10 * 5). After clicking “Increment,” it will contain 55 (11 * 5), and so on. As you can notice, area is always up-to-date.
4. Master the Lifecycle Hooks to Gain Control Over Components
Lifecycle hooks like onMounted(), onUpdated(), and onBeforeUnmount() allow you to perform actions at specific stages of a component’s life on a page. Being aware of all the hooks available and using them effectively is crucial to write great components. To learn more about how they work, check out the official lifecycle hook diagram.
5. Embrace Dependency Injection for Loose Coupling
Dependency injection promotes modularity and maintainability in your components. Utilize the provide() and inject() functions to share data between components in the same tree without having to pass them down via props. This avoids boilerplate code and makes your components more flexible.
Understand how this mechanism works in this example:
<template>
<!-- ... -->
</template>
<script setup>
import { provide } from 'vue'
// provide a message to all its ancestors
provide('message', 'I am your father!')
</script>
Code language: HTML, XML (xml)
In a child component of the root component above, you can then read the message:
<template>
<div>
<h1>{{ parentMessage }}</h1>
</div>
</template>
<script setup>
import { inject } from 'vue'
// access the message from the father
const parentMessage = inject('message')
return { parentMessage }
</script>
Code language: HTML, XML (xml)
This will print “I am your father!”
To update the state in injector components, pass to provide() an object with a ref value and a function that is responsible for mutating its value:
import { provide, ref } from 'vue'
const city = ref('New York')
// the "setter" function for the reactive state
function updateLocation(city) {
location.value = city
}
provide('city', {
city,
updateCity
})
Code language: JavaScript (javascript)
Conclusion
In this article, you learned why adopting the Composition API in Vue.js opens up a world of possibilities. By encapsulating logic in composable functions, leveraging reactive state, and mastering lifecycle hooks, you can gain better control over your components and improve code quality.
Thanks to the best practices shown here, you now know how to adopt the Composition API to create flexible, efficient, and modern Vue.js applications. Building easy-to-maintain applications that can scale effortlessly has never been easier!