When using composables in Vue.js, sometimes you already have a ref that you want to use. Other times, you don’t. This article will go through a pattern that lets you use your composables either way, giving you more flexibility when writing your applications.
This is the second article in a five-part series. If you haven’t read the first one yet, I invite you to start from the beginning. This series will walk you through several best practices when writing composables. Once you’re through, you’ll have a clear understanding of crafting solid composables.
Here’s some Vue composable best practices that we’ll be covering in this article:
- How to use an options object parameter to make your composables more configurable
- Using
ref
andunref
to make our arguments more flexible 👈 we’re here - 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
But first, let’s make sure we all understand what composables are.
If you’ve already read the article that precedes this one, you can skip to the next section.
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.
Use ref and unref for more flexible parameters
Almost all composables require some type of argument as an input. Often, this is a reactive ref
. It can also be a primitive Javascript type, like a string, number, or object. But we want to write our composables to be even more flexible and reusable, right?
Instead of requiring either a ref or a primitive, we can accept either. We then convert the argument to whatever we need on the fly:
// 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 = useCount(2);
The useTitle
composable that we saw in the previous article also applies this pattern.
When you pass in a ref, it’s linked to the document title. Then the title will be set to the value of that ref
:
const title = ref('This is the title');
useTitle(title);
title.value = 'New title please';
If you pass in just a string, it will create a new ref
for you and then proceed to link it up to the document title:
const title = useTitle('This is the title');
title.value = 'New title please';
In these contrived examples, it doesn’t look like much of a difference. However, when you’re using other methods and composables, you might already have a ref
from somewhere else. Or you might not. Either way, this composable can adapt to what you need.
Now let’s see how to make this work in our composables.
Implementing flexible arguments in a composable
To make the flexible arguments pattern work, we need to use either the ref
function or the unref
function on the argument we get:
// When we need to use a ref in the composable
export default useMyComposable(input) {
const ref = ref(input);
}
// When we need to use a raw value in the composable
export default useMyComposable(input) {
const rawValue = unref(input);
}
The ref
function will create a new ref
for us. But if we pass it a ref
, it just returns that ref
to us:
// Create a new ref
const myRef = ref(0);
// Get the same ref back
assert(myRef === ref(myRef));
The unref
function works the same, but instead it either unwraps a ref
or gives us our primitive value back:
// Unwrap to get the inner value
const value = unref(myRef);
// Returns the same primitive value
assert(value === unref(value));
Let’s see how some composables from VueUse implement 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!
useTitle
We’ll come back to the useTitle
composable since we’re already familiar with it.
This composable lets us pass in either a string or a ref
of a string. It doesn’t care which we provide:
// Pass in a string
const titleRef = useTitle('Initial title');
// Pass in a ref of a string
const titleRef = ref('Initial title');
useTitle(titleRef);
In the source code, you can see that right after we destructure our options object, we create the title
ref. We use the ref
function here, which allows us to use either a ref
or a string to make the title
ref:
// ...
const title = ref(newTitle ?? document?.title ?? null)
// ...
The ??
syntax is the nullish coalescing operator — a fancy-sounding name for “if the value on the left is null or undefined, use the value on the right.” So this line first tries to use newTitle
, but if that isn’t defined, it will use document.title
, and if that isn’t defined, it will give up and use null
.
Something interesting to note for you TypeScript connoisseurs:
The newTitle
variable used here has the type MaybeRef<string>
. Here is what the type is defined as:
type MaybeRef<T> = T | Ref<T>
This type definition means that the type MaybeRef<string>
can either be a string
or a Ref<string>
, which is a ref with a string value inside.
The next composable we’ll look at also uses this type to implement this pattern.
useCssVar
The useCssVar composable allows us to grab the value of a CSS variable and use it in our app:
const backgroundColor = useCssVar('--background-color');
Unlike useTitle
though, here we need the string value so that we can look up the CSS variable in the DOM. Using the unref
function, this composable can handle both refs and strings being passed in:
// Using a string
const backgroundColor = useCssVar('--background-color');
// Using a ref
const cssVarRef = ref('--background-color');
const backgroundColor = useCssVar(cssVarRef);
Looking at the source code, we can see that it uses the unref
function to accomplish this. Actually, it uses a helper function, called unrefElement
, to ensure we’re getting a DOM element and not just a Vue instance.
Most composables in VueUse implement this pattern, if you want to explore it further. So pick one that looks interesting and dive into the code!
Wrapping things up
We just spent some time learning the second pattern in the series, where we can use arguments more flexibly by using ref
and unref
intelligently in our composables. The composable will still work whether you happen to have a ref
or just the raw Javascript value. It adapts to how you use it!
We also looked at how the VueUse library implements this pattern in the useTitle
and useCssVar
composables. The useTitle
composable uses the ref
function, and the useCssVar
uses the unref
function so that we could see both variations in action.
In the next article, we’ll look at a pattern to improve return values by making them dynamic. We’ll learn how we can return either a single value or an object, depending on what is needed:
// Returns a single value
const isDark = useDark();
// Returns an object of values
const {
counter,
pause,
resume,
} = useInterval(1000, { controls: true });
This pattern can make your composable a lot simpler to use, especially if you only need a single value most of the time.