Vue 3 introduced a lot of features and concepts, these concepts brought about different ways in which stateful logic and functionalities are handled and managed. One of the notable features of Vue 3 is the Composition API which introduces the ability to create composables that extract reactive state and functionality so that they’re reusable and accessible to components within an application.
In this article, we’d explore what Vue composables are, why we need them and how to implement them in our Vue applications.
What are Vue Composables
According to Wiktionary, a composable can be composed (from multiple constituent or component elements), while composing is to make something by merging parts.
Vue composables can be regarded as external functions that extract and implement stateful logic and reactive functionality with the use of the Composition API. These external functions are reusable and accessible to any component within the application.
Vue composables are similar in concept to React hooks and can also be compared to mixins in Vue options API since both mixins and composables enable us to extract stateful logic, methods, and computed properties and easily reuse them across several components. Although composables and mixins differ in a lot of ways we’d explore later in the article.
Why do we need Vue composables
It's common to reuse logic or functionalities for specific tasks when developing frontend applications, examples of this logic could be formatting amounts with regards to its currency in fintech applications or something more common to all applications like a function that acts as an error handler. The functions are extracted outside of the component so that they can be accessible to other parts of the applications. These functions most of the time encapsulates stateless logic and could also be pure functions where they take in an input and return some expected output.
Vue composables allow us to define and encapsulate stateful logic. Stateful logic is a function that uses and manages states that can change over time. An example of stateful logic could be listening to on-scroll events or getting the currently logged-in user in an application.
Vue Composables are a great utility due to the fact that they are extremely reusable and also promote a flexible code organisation structure which can be easily navigable, especially in large codebases with increasing component complexities.
Stateful logic that needs to be used within multiple components can be extracted into an external file as a composable function. Just like in single file components, the full scope of the composition API functions can be used inside composables.
Another notable feature of composables is that they can be nested, a composable can make reference or call one or more other composable functions. This helps us to further break down complex stateful logic into smaller, functional, and independent atomic units just the way we split our applications into components.
Creating a Vue Composable
Now we’re going to try creating and implementing a Vue composable in a Vue application we would create. The prerequisite for this demonstration is having Node JS installed in your application
To create our Vue project let's run the following command
npm init vue@latest
This command will install and execute create-vue, Vue’s official project scaffolding tool. You will be presented with prompts. Let's call our project name my-vue-composable
, and choose No for the other several optional features.
Once the project is created navigate into the project directory using the command below
cd my-vue-composable
After navigating to the project directory run the following commands to install the necessary dependencies and start the dev server.
npm install
npm run dev
Using the Intersection Observer API we’re going to create a custom composable function that exposes an observer that we can use to observe our target element and also allows us to call methods or perform actions when an element comes into view. The Intersection Observer API provides a way that enables us to asynchronously observe changes in the intersection of a target element with an ancestor element or with a top-level document's viewport.
We are going to implement a useElementInView composable that can be used across our application. With the composable we would be able to observe and perform actions when our element is in view.
Let's create a folder inside the src directory and name it composables, inside the folder create a file useElementInView.js.
This file would contain the logic for our composable.
There are conventions and best practices associated with the use of composables including naming conventions, composable functions should be prefixed with “use” and should be in camelCase. This is a reason why we would name our composable function useElementInView.
Just like functions a composable can accept arguments for scenarios that require dynamic data for the implementation of the logic, these arguments can be reactive even though the composable doesn’t really require the argument to be reactive. Reactive arguments can be stripped of their reactivity using the unRef utility function provided by Vue.
Another recommended convention for composables is for them to return plain objects containing reactive values. These values can be destructured in components without losing their reactivity.
In our useElementInView.js file let's import ref from Vue and create our useElementInView function which would contain the logic we need to reuse in our components.
Insert the code snippet below inside our useElementInView.js file
import { ref } from "vue";
export const useElementInView = (
actionCallback = () => {},
options = { threshold: 1.0 },
repeat = false
) => {
const isElementIntersecting = ref(false);
const observerCallback = (entries) => {
entries.forEach(({ target, isIntersecting }) => {
if (isIntersecting) {
isElementIntersecting.value = true;
actionCallback(target)
} else if(repeat) {
isElementIntersecting.value = false;
}
});
};
const observer = new IntersectionObserver(observerCallback, {
...options
});
return {
isElementIntersecting,
observer
};
};
First of all, we imported ref from Vue to help us store reactive values and we also created our useElementInView function.
Then we created an isElementIsIntersecting variable that stores a reactive value. It's instantiated with a default value of false, this variable allows us to keep track of when our target element is intersecting in the viewport or not.
Our useElementInView function accepts three parameters, a callbackAction
function, an options
object and a repeat
boolean value. The callbackAction
function is run when the target element intersects in the viewport while the options object is the options required by the IntersectionObserver API. If an options parameter is not provided our composable function defaults to the options object provided in the function. The repeat
value if true allows what action triggered when the target element intersects in the viewport to repeat every time the element intersects and when it doesn't.
We also created a function called observerCallback
which takes a parameter of entries and checks if the isIntersectingProperty of each entry is true. If true it sets our isElementIntersecting
value to true.
After we create our observer by initialising our IntersectionObserver API with our observerCallback
and our options
parameter.
Finally, we return our values and methods in an object.
Using our Composable
For us to use our composable inside our component, first we import our composable like below:
import { useElementInView } from "../composables/useElementInView.js"
We also import onMounted
, onUnMounted
, and ref
from vue
import { onMounted, onUnmounted, ref } from "vue";
Let's create a boxRef
variable that stores our target element as a reactive value using ref
const boxRef = ref({})
Lets also create a callback function called handleElementIntersect
that's triggered every time our target element intersects in the viewport
const handleElementIntersect = (target) => {
console.info('Hello Target Element is Intersecting')
}
Next, we call our composable function and destructure the values it returns and also pass in our callback function and an options object.
const { isElementIntersecting, observer } = useElementInView(handleElementIntersect, {
threshold: 0.5,
rootMargin: '20%'
})
Here's the full implementation of our composable in a boxImage
component of a mini image gallery application.
<script setup>
import { onMounted, onUnmounted, ref } from 'vue'
import { useElementInView } from '../composables/useElementInView'
const props = defineProps({
title: String,
url: String,
alt: String
})
const boxRef = ref({})
const handleElementIntersect = (target) => {
console.info('Hello Element is Intersecting')
}
const { isElementIntersecting, observer } = useElementInView(handleElementIntersect, {
threshold: 0.5,
rootMargin: '20%'
})
onMounted(() => {
observer.observe(boxRef.value)
})
onUnmounted(() => {
observer.disconnect()
})
</script>
<template>
<div class="box-image" :class="isElementIntersecting ? 'moveUp' : ''" ref="boxRef">
<img :src="url" :alt="alt" />
<div class="title">{{ title }}</div>
</div>
</template>
<style scoped>
.box-image {
position: relative;
width: 100%;
height: auto;
border: 2px solid black;
transform: translateY(15%);
transition: all ease-in 0.5s;
opacity: 0;
}
.moveUp {
transform: translateY(0%);
opacity: 1;
}
.box-image::after {
content: '';
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
background-color: rgba(0, 0, 0, 0.334);
z-index: 5;
}
.title {
position: absolute;
bottom: 1rem;
left: 1rem;
z-index: 8;
font-size: 1.1rem;
font-weight: 500;
color: white;
text-transform: lowercase;
}
</style>
Here is the link to the complete codebase for you to try it out.
Differences between composables and mixins
At the introduction of this article, we talked about how composables and mixins are similar in concept and application. Users of Vue 2 would be familiar with mixins. Just as with composables, with mixins, we can extract reactive data, methods and even computed properties and reuse them across our components.
Here are the ways in which composables differ from mixins:
Obscured or Un-clarified Data Source
When using mixins, the source of data becomes unclear especially when working with multiple mixins in a single component, or a globally registered mixin. It becomes difficult to trace which particular data or implementation is being injected by which mixin. But with composables the source of data is clear and transparent because we can explicitly destructure our data and functions from the composable. This way we are able to tell where exactly our data and functions come from.
Collisions in Naming
Multiple mixins from different locations can result in different mixins having the same named property keys resulting in namespace collisions. Therefore there’s the risk of losing data. With composables destructured variables can be renamed in a name conflict situation by the component consuming the composable, this reduces the risk of losing data.
Global State
With composables it’s possible to create a shared global state with the components that consume it. This is possible by just lifting the reactive data outside the composable function so it is not recreated with every new component instance it’s used. This is not possible with mixins because all defined data is always reset or recreated for every component instance it’s used.
Safeguarding Reactive Data
In some scenarios, we do not want to give the component consuming our reusable composable the ability to mutate certain reactive data. With mixins, it is next to impossible to safeguard its own reactive data as there’s no mechanism of safeguarding that data. With composables rather it’s possible to safeguard own reactive data, this is done by wrapping the data with a read-only function provided by Vue.
Implicit cross-mixin communication
When there’s a need for multiple mixins to interact with one another, it’s difficult as they have to rely on shared property keys, which makes them tightly coupled. This means a change to a particular mixin can affect the other mixin. With composables it is more flexible as they can be nested within one another. Basically, values returned from one composable function can be used in another or even passed into another as arguments just as in normal functions. Also one composable has the ability to call one or more composable functions just like with functions.
Exploring External Libraries for Composable Functions
Another alternative approach to using composable functions in your application is by leveraging external open-source libraries, these libraries provide custom composable functions that you can utilise in your applications accompanied by substantial documentation. Two of these libraries include:
You can check out their documentation for more extensive information on the custom composable functions they provide.
Conclusion
Composables enable us to extract reusable logic out of our components for reusability. They make our code structure more organised and flexible. This helps us to easily break up and abstract complex logic in an efficient way in our applications.