So you’ve heard about Vue composables and want to write your own? Maybe you’ve even used composables others have written but you’re not sure how to get started creating one for yourself. That’s what this article is all about!
First, let’s quickly talk about what a Vue composable is. A Vue composable is like a utility or helper function except with one important difference: it’s stateful. That is, it includes data defined with Vue’s reactive
or ref
function.
The example used in the Vue.js docs is a useMouse
composable. The idea is that the x and y data exposed by the composable is reactive. That means whenever the data updates it triggers re-renders of the corresponding DOM elements. Here you can see what the useMouse
composable’s effect would be.
The beauty of composables, is that we can benefit from defining reactive data (just like within a component) but without having to render anything at all! This means we can abstract stateful logic outside of a component and re-use it within the context of a variety of different components (or even without any component).
You can get a good idea of what is possible by perusing the VueUse docs. It is a collection of over 200 installable and instantly usable composables that solve common problems like working with data in localStorage, toggling light/dark mode, and more.
Now that you have a good idea of what a composable is, let’s walk step by step through creating one.
The first step is to create a file for your composable to live in. The convention is to do this in a directory called composables
.
In this file, you’ll export a function with a name that describes what your composable does. For this article we’ll create a “useCycleList” composable as an example. You can see a tried and true implementation of a useCycleList composable from the VueUse library. Checkout the docs for it to get a feel for what it does.
(You can also see a very practical use-case example for this composable in this FREE Vue School video lesson: Create an Image Carousel with VueUse)
// @/src/composables/useCyleList.ts
export const useCycleList = ()=>{}
use
prefix for Vue ComposablesNotice that the composable name starts with use
. This is also another common convention to help users of your composable distinguish it from a normal non-stateful helper function.
It’s also a really good idea to write your composables using TypeScript. This makes them most intuitive to actually use (great autocompletion, error detection, etc). That’s what we’ll do during this article but if you aren’t comfortable with TS, it’s still ok to write your composables with plain JS.
Not all composables require an argument but most do. For our example, let’s take in an array. This is the list that we want to cycle through. For now, it will be an array of any
type of item.
// if not using TS you can remove the :any[]
export const useCycleList = (list: any[])=>{}
The arguments make up the interface (or API) for the composable’s INPUT.
Next, let’s define our composable’s OUTPUT API, that is, what is returned from the function.
export const useCycleList = (list: any[])=>{
return {
prev,
next,
state
}
}
Here we’re exposing 3 things. Let’s break each one down.
This will be reactive data. It’s a single item within the array. It’s the “active” item in the list that we’re cycling through.
For example, given the following use, state
would initially be Dog
(the first item in the array).
const { state } = useCycleList([
'Dog',
'Cat',
'Lizard'
])
console.log(state) // Dog
The exposed next
function allows the consumer of the composable to advance to the next item in the list. So given the following code state
would be Cat
.
const { state, next } = useCycleList([
'Dog',
'Cat',
'Lizard'
])
next()
console.log(state) // Cat
The exposed prev function allows going backwards through the list. For example:
const { state, prev } = useCycleList([
'Dog',
'Cat',
'Lizard'
])
prev()
console.log(state) // Lizard
prev()
console.log(state) // Cat
As a side note, notice that we’ve defined our compoable’s interface (it’s API) first. None of it works yet because we haven’t implemented the logic. But that’s ok. This is actually a really good workflow for writing a composable. Define how you want to use it (make it a really nice DX) BEFORE you implement the details.
This design approach applies to all kinds of code (components, stores, etc) but we can definitely apply it to composables as well.
Now, let’s create some reactive state to keep up with which item in the array is “active”. This is what makes it a composable and really what makes it useful in the context of a Vue app. What we really want to keep up with is the position of the currently active item. So let’s create a reactive ref activeIndex
.
import { ref } from "vue";
export const useCycleList = (list: any[])=>{
// 👇 Define a ref to keep track of the active index
const activeIndex = ref(0);
//...
}
Then we can make some reactive derived data (ie a “computed ref”) to determine what state
will be based on the value of activeIndex
.
import { ref, computed } from "vue";
export const useCycleList = (list: any[])=>{
const activeIndex = ref(0);
// 👇 reactive `state` is based on the activeIndex
const state = computed(()=> list[activeIndex.value]);
//...
return { state /*...*/ }
}
With that, prev and next are really easy to implement.
export const useCycleList = (list: any[])=>{
//...
// 👇 the next function
function next() {
// if the `state` is the last item, start from the beginning of the list
if (activeIndex.value === list.length - 1) {
activeIndex.value = 0;
} else {
// otherwise just increment the activeIndex by 1
activeIndex.value += 1;
}
}
// 👇 the prev function
function prev() {
// if the `state` is the first item, wrap to end end
if (activeIndex.value === 0) {
activeIndex.value = list.length - 1;
} else {
// otherwise just decrement the activeIndex by 1
activeIndex.value -= 1;
}
}
//...
}
One important thing to consider when writing a composable, is that people often work with reactive data within their components and they expect to be able to intuitively pass that reactive data into any composable.
In other words, they may want to do this:
const list = ref(['Dog', 'Cat', 'Lizard']);
const { state, prev, next } = useCycleList(list);
So let’s update the composable to accept a reactive list (a list defined with ref
or reactive
).
import { ref, computed, watch, type Ref } from 'vue';
// 👇 now we're accepting a ref that is an array of anything
export const useCycleList = (list: Ref<any[]>) => {
//...
// And then throughout the composable, you'll need to replace all uses of
// `list` with `list.value`
// for example 👇
const state = computed(() => list.value[activeIndex.value]);
// do the same for list in next, prev, etc...
// 👇 finally, since the list can change now
// let's run a little cleanup on the activeIndex
// if the list is changed out to something shorter
// than the activeIndex
watch(list, () => {
if (activeIndex.value >= reactiveList.value.length) {
activeIndex.value = 0;
}
});
// ...
};
The above is great for accepting a reactive list. But now we’ve REQUIRED the composable consumer to pass something reactive. I loved the ease of just passing a plain array when needed. No worries! We can support both with a helper function from Vue called toRef
.
import { ref, computed } from "vue";
import { type MaybeRefOrGetter, toRef, watch } from "vue";
// 👇 notice we're using the `MaybeRefOrGetter` type from Vue core
export const useCycleList = (list: MaybeRefOrGetter<any[]>) => {
// calling toRef normalizes the list to a ref (no matter how it was passed)
const reactiveList = toRef(list);
// replace all uses of list.value
// with reactiveList.value
// for example 👇
const state = computed(() => reactiveList.value[activeIndex.value]);
// do the same for list in next, prev, watch, etc...
//...
}
Now we can support both types of data for this list, plus we get support for a final third type for FREE! All of the following now work:
// As plain data
const { state, prev, next } = useCycleList(
['Dog', 'Cat', 'Lizard']
);
// As reactive data
const list = ref(['Dog', 'Cat', 'Lizard']);
const { state, prev, next } = useCycleList(list);
// As a getter
const list = ref(['Dog', 'Cat', 'Lizard']);
const { state, prev, next } = useCycleList(()=> list.value);
You can read more about toRef here in the Vue.js docs. Plus, there are a few similar functions that you might want to read up on for making flexible and robust components: toValue and isRef, among others.
Currently if we tried to set the state
from the composable, it wouldn’t work. Why? We’re defining it as a computed prop.
const state = computed(() => reactiveList.value[activeIndex.value]);
To make the API a little more flexible, I think it makes sense that writing to the state
updates the underlying item in the array. No problem! Vue can handle that too!
const state = computed({
// the get method is called when state is read
get() {
// this is the same as the return from the function before
return reactiveList.value[activeIndex.value];
},
// the set method is called when state is written to
set(value) {
// take the given value and apply it to the array item
// at the currently active index
reactiveList.value[activeIndex.value] = value;
},
});
This isn’t a 100% necessary feature to add but it’s nice to think about how all users might use your composable and make it as intuitive as possible to use.
So far, we’ve used any[]
to define the composable list
argument. This kind of makes sense because we want our composable to work for an array of anything (not just strings as in the examples).
However, most TS users know that using any
is a bit of a code smell. It’s presence means there’s room for improvement.
So let’s use a generic to say that each item in the array is some variable type.
// T is the generic here
export const useCycleList = <T>(list: MaybeRefOrGetter<T[]>) => {
How is this useful?
Now, TypeScript can infer that our state is a WritableComputedRef
of the SAME type as the items within the passed list. For our example, that’s a string.
Here’s a screenshot of what things look like in my IDE before we use the generic (that is, using any
)
Here’s what it looks like after we implement the generic.
And if we were to provide another type of data for each item in the list:
Composables are a powerful tool for creating re-usable, stateful logic in your Vue.js applications. They are easier to write than you think. While VueUse provides a vast array of pre-written composables for you, there are definitely times you’ll need to write your own. Now you know how!
Our goal is to be the number one source of Vue.js knowledge for all skill levels. We offer the knowledge of our industry leaders through awesome video courses for a ridiculously low price.
More than 200.000 users have already joined us. You are welcome too!
© All rights reserved. Made with ❤️ by BitterBrains, Inc.