Your First Composable

Components grow. In Lifecycle and Data, loading the menu took three refs and an onMounted, and every other component that loads something would need the same lines again. Vue's answer is a composable: a function that uses Vue's ref, computed, watch or lifecycle hooks, and returns what a component needs. Its name starts with use, so that you can tell it's one: useCounter, useLocalStorage, useFetch.

Here's a counter as a composable. It's in a file of its own, useCounter.js:

import { ref } from "vue";

export function useCounter(start) {
    const count = ref(start);

    function increase() {
        count.value++;
    }

    return { count, increase };
}

And here's a component that uses it:

<script setup>
import { useCounter } from "./useCounter.js";

const { count, increase } = useCounter(0);
</script>

<template>
    <p>Cups of coffee today: {{ count }}</p>
    <button @click="increase">+1 cup</button>
</template>

What's new:

  • A composable is in a .js file, not a .vue file: it has no template and no styles, only JavaScript. It imports from "vue" what it uses.
  • export function exports the function by its name, as export const did in the lesson Components. So the component imports it in braces, as it imports ref from "vue": import { useCounter } from "./useCounter.js";.
  • The component calls the composable at the top of <script setup>, as it calls ref(). The composable creates the ref and the function then, and returns them in an object, and destructuring takes them out: const { count, increase } = useCounter(0);.
  • From then on, count and increase work as if they had been written in the component itself: count is a ref, which {{ count }} shows, and increase is a function for @click.

Each call creates a new ref: two components that call useCounter() each have a count of their own. Calling a composable is like copying its lines into the component, except that they're written only once.

Typical mistakes:

  • import useCounter from "./useCounter.js"; without the braces: that imports the file's default export, which it doesn't have. The page stays empty, with a TypeError in the console.
  • Forgetting return: the function then returns undefined, and the destructuring ends with a TypeError that starts Cannot destructure property 'count'.

Instructions

  1. Move the counter into a composable. In useCounter.js, write useCounter(start) as shown above. In App.vue, import it, and replace the ref and the function with const { count, increase } = useCounter(0);. The page works as before.

  2. Add a function reset() to the composable that sets count back to start, the number it started with, and return it too. In App.vue, take it out with the others, and add a button Reset after +1 cup that calls it.