Home / Blog / How to Write a Vue Composable Step-by-Step
How to Write a Vue Composable Step-by-Step

How to Write a Vue Composable Step-by-Step

Daniel Kelly
Daniel Kelly
July 3rd 2024

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!

What is Vue.js Composable?

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.

Defining a Vue Composable

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 = ()=>{}

The use prefix for Vue Composables

Notice 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.

Consider Writing Composables in TypeScript

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.

Accept Composable Arguments

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.

Return Data and Functions from a Composable

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.

state

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

next

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

prev

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

Composable API Design Workflow

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.

Define Reactive State for the Composable

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 /*...*/ }
}

Define Exposed Functions for the Composable

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;
    }
  }

  //...
}

Allow the Composable to Accept Reactive Arguments

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;
    }
  });

  // ...
};

Allow the Composable to Accept Non-Reactive AND Reactive Arguments (Plus Getters!)

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.

Improve the Composable API with a Writable Computed Prop

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.

Make the Composable TypeSafe

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)

Screenshot of state in IDE when list is defined as any

Here’s what it looks like after we implement the generic.

Screenshot of state in IDE with the generic T

And if we were to provide another type of data for each item in the list:

Screenshot of the state type as number when the passed list is made up of numbers

Conclusion

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!

Start learning Vue.js for free

Daniel Kelly
Daniel Kelly
Daniel is the lead instructor at Vue School and enjoys helping other developers reach their full potential. He has 10+ years of developer experience using technologies including Vue.js, Nuxt.js, and Laravel.

Comments

Latest Vue School Articles

v-model and defineModel: A Comprehensive Guide to Two-Way Binding in Vue.js 3

v-model and defineModel: A Comprehensive Guide to Two-Way Binding in Vue.js 3

Combine v-model directive with defineModel Composition API macro for efficient two-way binding in your Vue.js applications.
Mostafa Said
Mostafa Said
The Definitive Guide To Building Cross Platform Apps With Vue (Quasar)

The Definitive Guide To Building Cross Platform Apps With Vue (Quasar)

Quasar is a VueJs framework that can build apps for mobile, desktop and the web. This guide touches on 11 aspects of Quasar that will set your Quasar journey on fire ️‍🔥
Luke Diebold
Luke Diebold

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!

Follow us on Social

© All rights reserved. Made with ❤️ by BitterBrains, Inc.