Composable Design Patterns in Vue 3

Back in the days of Vue 2, the mixin was king. Had some code you wanted to reuse in separate components? Create a mixin, import and register it in the component and its provided props, data and methods would magically be available.

The problem with mixins was that magic. You’d often end up calling methods or referring to props that you had no way of knowing existed without visiting the mixin file and double-checking what it provided.

If you actually wanted unit testing or type checking for that component, you faced an even tougher battle. Enter: the composable.

Written By

Craig Riley


Craig is a full-stack developer who mainly deals in JavaScript and PHP. Frameworks include VueJS Laravel, Wordpress, Nuxt and Quasar. He also likes sysadmin stuff until it goes horribly wrong.

What is a composable?

A composable is a function that returns one or more reactive objects. These might be refs, reactives, computeds or any combination thereof. These functions might also return other functions that allow you to execute actions that affect the reactive objects. They can be as simple or as complex as your application needs.

Convention is that a composable starts with the word use.

Quick example: say you want a ref which has the current time, you could create a composable called useCurrentTime which might look like this:

1import { ref, computed } from "vue";
2
3export function useCurrentTime() {
4 let time = ref(Date.now());
5 const updateTime = () => (time.value = Date.now());
6 setInterval(updateTime, 100);
7
8 const formattedTime = computed(() => {
9 return new Date(time.value).toLocaleString(undefined, {
10 dateStyle: "short",
11 timeStyle: "medium"
12 })
13 });
14
15 return {
16 time,
17 formattedTime
18 }
19}

Now you can import that composable into any component and have yourself a reactive clock:

1<script setup>
2import { useCurrentTime } from "composables/useCurrentTime";
3
4const { time, formattedTime } = useCurrentTime();
5</script>
6
7<template>
8 <h1>The current time is <span v-text="formattedTime"></span></h1>
9</template>

Instance vs Singleton

You may have noticed that a new time ref and a new formattedTime computed are created every single time you call the useCurrentTime composable. So if you call it five times (you really like clocks in your app) then you’ve got five different ref objects running five different interval loops to keep them updated. Obviously this is completely unnecessary, so what can we do?

Our goal is to return the same reactive objects each time the composable is called. This is the Singleton pattern (each call returns the same single instance). To do this, all we have to do is move our reactive objects outside the function scope, like so:

1import { ref, computed } from "vue";
2
3let interval;
4let time = ref(Date.now());
5const updateTime = () => (time.value = Date.now());
6const formattedTime = computed(() => {
7 return new Date(time.value).toLocaleString(undefined, {
8 dateStyle: "short",
9 timeStyle: "medium"
10 })
11 });
12
13export function useCurrentTime() {
14 clearInterval(interval);
15 interval = setInterval(updateTime, 100);
16
17 return {
18 time,
19 formattedTime
20 }
21}

Now both time and formattedTime are declared outside the scope of useCurrentTime and so every call to useCurrentTime will get the exact same reactive objects. We also set up and interval inside the function that only starts updating the time when it’s actually been used somewhere. The call to clearInterval first is to ensure that only one interval loop is running at a time.

Isn't this a store?

Essentially, yes. Composables look very similar to Pinia stores because Pinia stores are themselves composable functions that hew to the Singleton practice.

So why would you choose a composable over a store? Firstly, you might not always want a Singleton. There are many situations that call for different instances of reactive objects. You might, for example, want to track the mouse position over a specific element. You might also want to create a computed value based on whatever string is input:

1import { computed } from "vue";
2
3export function useExclamationPoint(strRef) {
4
5 const withExclamation = computed(() => `${strRef.value}!`);
6
7 return withExclamation;
8}

You might also want to keep your composables from being too tightly coupled with your app since you might want to externalise them into a shared library at some point.

Can't someone else do it?

Surely there must be loads of these composables that lots of projects would need, you might think. You would, of course, be correct. Well done you!

VueUse is a library that houses over a hundred different composables for use in your Vue app. From reactive values for screen dimensions, via VModel proxies, to reactive Bluetooth and Websockets data streams… it truly is an exhaustive collection of battle-tested composables.

Outro

While there are often times where variables are inextricably linked to components and should be left inside the <script> tag of your Vue component, the composable pattern offers you a unique opportunity to extract a lot of your app’s logic into reusable chunks. Got an API search function that you use repeatedly? Composable. Got async data you don’t want to keep hydrating for different components? Composable. Got bagfuls of garden waste just hanging around by your shed? Compostable.

It’s a pattern that contributes to more stable apps since small chunks of logic that do one thing are much easier to unit test than gigantic, monolithic components that act like overworked air traffic controllers.

More Tutorials

Here are some more tutorials that we think might be helpful for you. Feel free to contact us if there's anything you might need

Passing through slots in Vue

If you're keeping your Vue component sizes small, there's a good chance you'll need to implement a wrapper component at some point. If you're only utilising the default slot for these, it can be as simple as putting a &lt;slot /&gt; tag inside your wrapper component. However, there's a bit more effort involved if you need to pass through named slots to your base component

Automatic Vue Component Registration with Vite

Developing a large web application is a constant struggle to not keep writing the same things over and and over again. Part of this can be the need to continually import your components within a Vue application. While there are some great tools to help automatically register all of your components globally (like unplugin-vue-components), the downside is that you can't register them using async components. This means that every single component has to be loaded before your app can start, even ones which aren't on the page being accessed. For a public-facing website like southcoastweb this was a non-starter, so we had to come up with a better way.

Using Vue's class function in your own composables

Inside your &lt;template&gt;, you may be used to stacking lots of conditionally applied classes onto elements. You can add dynamic classes (e.g. .btn--${props.type}), arrays of conditional if/else classes (e.g. props.error ? 'has-error' : 'is-valid') or even whole objects of conditionals (e.g. { 'is-disabled': props.disabled }. But what do we do if we want to shift that logic to our &lt;script&gt; block? While undocumented (and thus not a guaranteed API, so tread carefully), Vue internally uses a pretty simple function to make all this magic happen, and you can import it too.

Automating your service and repository patterns in Laravel with command generators

"Why spend 20 seconds doing something when you can spent 4 hours automating it," goes the proverb. See any professional proverbists around any more? No, because they've all been automated. Also it's fun. Whatever your programming pattern in Laravel, chances are that the php artisan make:x command is going to leave you high and dry on occasion. That's why it can be useful to have your own commands available to cover those gaps. We're going to go with the example of the "Service" pattern below.

How to Create a Simple Button Component in Figma

In this tutorial, we’ll create a simple button using Figma’s built-in component system.

Copyright 2007 - 2024 southcoastweb is a brand of DSM Design.