Composables are, by far, the best way to organize business logic in your Vue 3 app.
They let you extract small pieces of logic into functions that you can easily reuse repeatedly. This makes your code easier to write and read.
Since this way of writing Vue code is relatively new, you might be wondering what the best practices are when writing composables. This tutorial series will serve as your guide on how to craft solid composables that you and your team can rely on.
Here’s what we’ll be covering:
- How to use an options object parameter to make your composables more configurable 👈 we’re here
- Using the
ref
andunref
to make our arguments more flexible - A simple way to make your return values more useful
- Why starting with the interface makes your composables more robust
- How to use async code without the need for
await
— making your code easier to understand
First, though, we need to make sure we’re on the same page. So let me take a bit to explain what, exactly, a composable is.
What is a Composable?
According to the Vue documentation, a composable is “a function that leverages Vue Composition API to encapsulate and reuse stateful logic”.
This means that any code that uses reactivity can be turned into a composable.
Here’s a simple example of a useMouse
composable from the Vue.js docs:
import { ref, onMounted, onUnmounted } from 'vue'
export function useMouse() {
const x = ref(0)
const y = ref(0)
function update(event) {
x.value = event.pageX
y.value = event.pageY
}
onMounted(() => window.addEventListener('mousemove', update))
onUnmounted(() => window.removeEventListener('mousemove', update))
return { x, y }
}
We define our state as refs
, then update that state whenever the mouse moves. By returning the x
and y
refs, we can use them inside of any component (or even another composable).
Here’s how we’d use this composable inside of a component:
<template>
X: {{ x }} Y: {{ y }}
</template>
<script setup>
import { useMouse } from './useMouse';
const { x, y } = useMouse();
</script>
As you can see, using the useMouse
composable allows us to easily reuse all of this logic. With very little extra code, we’re able to grab the mouse coordinates in our component.
Now that we’re on the same page, let’s look at the first pattern that will help us to write better composables.
Options object parameter
Most composables have one or maybe two required inputs. Then there’s a series of optional arguments to help configure how the composable works.
When configuring your composable, instead of passing in a long list of arguments, you can pass in an options object instead:
// Using an options object
const title = useTitle('A new title', { titleTemplate: '>> %s <<' });
// Title is now ">> A new title <<"
// Using more arguments
const title = useTitle('A new title', '>> %s <<');
// Title is now ">> A new title <<"
Passing in options as a whole object instead of arguments gives us a few benefits:
First, we don’t have to remember the correct ordering of arguments. This is less of an issue now with TypeScript and great autocomplete in our editors, but it still makes a difference. With a Javascript object, the ordering of the keys doesn’t matter.
Second, the code is more readable because we know what the option does. It says it right there. We don’t need to go hunting around the source code or rely on our IDE to let us know.
Third, it’s much easier to add new options later on. This applies both to adding new options to the composable itself, and to adding options when using the composable.
So, using an options object is better. But how do we implement that?
Implementing in a composable
Here’s how you would implement the options object pattern in a composable:
export function useMouse(options) {
const {
asArray = false,
throttle = false,
} = options;
// ...
};
Here, we can accept an argument or two if they are required, and then the last argument is the options object. So in this example, useMouse
has no required arguments, just the options object.
The next step is to destructure the options object. By destructuring, we can access all the values, and clearly provide defaults for each possible option.
Now we’ll look at how two different composables from VueUse apply this pattern. VueUse is an open source collection of composables for Vue 3, and is very well written. It’s a great resource to learn how to write great composables!
First, we’ll look at useTitle
, and then we’ll see how useRefHistory
works.
useTitle
The useTitle composable is fairly straightforward. It let’s you update the page’s title:
const title = useTitle('Initial Page Title');
// Title: "Initial Page Title"
title.value = 'New Page Title';
// Title: "New Page Title"
It also has a couple options for extra flexibility.
You can provide a titleTemplate
, and also set it to observe
the title for any changes that other scripts might make (using a MutationObserver):
const titleOptions = {
titleTemplate: '>> %s <<',
observe: true,
};
Here is how you’d use the options object:
const title = useTitle('Initial Page Title', {
titleTemplate: '>> %s <<',
observe: true,
});
// Title: ">> Initial Page Title <<"
title.value = 'New Page Title';
// Title: ">> New Page Title <<"
When you look at the source code for useTitle
you can see how this is being done:
export function useTitle(newTitle, options) {
const {
document = defaultDocument,
observe = false,
titleTemplate = '%s',
} = options;
// ...
}
The useTitle
composable has one required argument, then an options
object. After that, it implements the rest of the pattern exactly as described here.
Now let’s look at a slightly more complicated composable that also uses this options object pattern.
useRefHistory
The useRefHistory composable is a bit more interesting. It let’s you track all of the changes made to a ref
, allowing you to perform undo and redo operations fairly easily:
// Set up the count ref and track it
const count = ref(0);
const { undo } = useRefHistory(count);
// Increment the count
count.value++;
// Log out the count, undo, and log again
console.log(counter.value); // 1
undo();
console.log(counter.value); // 0
This composable can take a bunch of different options:
{
deep: false,
flush: 'pre',
capacity: -1,
clone: false,
// ...
}
If you want the complete list of these options and what they all do, the docs are the best place to go.
We can pass in the options object as the second parameter to further configure how this composable behaves, the same as in our previous example:
const state = ref({});
const { undo, redo } = useRefHistory(state, {
deep: true, // Track changes inside of objects and arrays
capacity: 15, // Limit how many steps we track
});
If we look at the source code for this composable, we see it uses the exact same object destructuring pattern that useTitle
does:
export function useRefHistory(source, options) {
const {
deep = false,
flush = 'pre',
eventFilter,
} = options;
// ...
}
However, in this example we only pull out a few values from the options object here at the start.
This is because useRefHistory
relies on the useManualRefHistory
composable internally. The rest of the options are passed as that composable’s options object later on in the composable:
// ...
const manualHistory = useManualRefHistory(
source,
{
// Pass along the options object to another composable
...options,
clone: options.clone || deep,
setSource,
},
);
// ...
This also shows something I mentioned earlier: composables can use other composables!
Bringing it all together
This article was the first installment in our series, “Writing Better Composables”.
We looked at how adding an options object as a parameter can make for much more configurable components. For example, you don’t need to worry about argument ordering or to remember what each argument does, and adding more options to an object is far easier than updating the arguments passed in.
But we didn’t just look at the pattern itself. We also saw how the VueUse composables useTitle
and useRefHistory
implement this pattern. They do it in slightly different ways, but since this is a simple pattern, there’s not much variation that you can do.
The next article in this series looks at how we can accept both refs and regular Javascript values as arguments:
// Works if we give it a ref we already have
const countRef = ref(2);
useCount(countRef);
// Also works if we give it just a number
const countRef = useRef(2);
This adds flexibility, allowing us to use our composables in more situations in our application.
You’ve reached the end of this article, so why not hop over to that one and continue learning composable best practices?