Vue.js 3 Composables (Custom Hooks) Tutorial

In this Vue tutorial we learn how to create code files with the Composition API that we can use across multiple components.

We cover how to create and use composables with and without parameters.

Lesson Project

If you want to follow along with the examples, you will need to create an app generated by the Vue CLI as well as the following extra components.

  • src/components/ClickCounter.vue
  • src/components/HoverCounter.vue

The project should look similar to the following.

Example: project
project-name/
├── src/
|   ├── components/
|   |   ├── ClickCounter.vue
|   |   └── HoverCounter.vue
|   └── App.vue

We’ll set up the components throughout the lesson.

What is a Composable?

A composable is a Mixin that works with the Composition API. They allow us to use sections of code across multiple components.

Composables are also known as Hooks or Composition Functions because that’s all they really are, functions. These functions are regular Javascript (or TypeScript) files that may contain any Vue composition method, like ref, computed etc.

As an example, let’s say our ClickCounter component tracks a counter based on the number of clicks on a button.

Example: src/components/ClickCounter.vue
<template>
  <button @click="increment">Clicks: {{ count }}</button>
</template>

<script>
import { ref } from 'vue'

export default {
  setup() {

    const count = ref(0)

    function increment() {
      count.value++
    }

    return {  count, increment }
  }
}
</script>

And our HoverCounter component tracks a counter based on the number of times an element was hovered over.

Example: src/components/HoverCounter.vue
<template>
  <div @mouseover="increment">Hovers: {{ count }}</div>
</template>

<script>
import { ref } from 'vue'

export default {
  setup() {

    const count = ref(0)

    function increment() {
      count.value++
    }

    return {  count, increment }
  }
}
</script>

<style scoped>
div{background:gainsboro}
</style>

If we add them to the root App component and take a look in the browser, they both work as expected.

Example: src/App.vue
<template>
  <click-counter />
  <hr>
  <hover-counter />
</template>

<script>
import ClickCounter from './components/ClickCounter'
import HoverCounter from './components/HoverCounter'

export default {
  components: {
    ClickCounter,
    HoverCounter
  }
}
</script>

The problem is that we’re duplicating code. Both components have the exact same script block.

A composable allows us to define a method with the tracking code once in an external file, then import and use it in both components.

How to create a composable

As mentioned earlier, a composable is just a Javascript (or TypeScript) file with a method. We also export the method to be able to import and use it in our components.

Example: composable
export default function composableName() {}

Composables can use any composition method, but we have to import whatever we use. This remains true even if we already import the same methods in a component that uses the composable.

Example: import composition methods
import { ref, computed } from 'vue'

export default function composable_name() {

  // import ref
  const a = ref('value')

  // import computed
  const b = computed(() => {
    return a.value
  })
}

A convention in Vue is to prefix the function name with “use”. For example, if we had a composable called “counter”, we want to name it “useCounter”.

Vue plugins like the Router and Vuex also use this convention in the Composition API with their useRouter and useStore methods.

Example: naming convention
export default function useCounter() {}

We can store our composables wherever we like. A popular convention is to use a folder in the /src/ directory called “composables” or “hooks”.

Example: project
project/
├── src/
|   ├── components/
|   |   ├── component.vue
|   ├── composables/
|   |   ├── composable.js
|   └── App.vue

As an example, let’s create /src/composables/counter.js . Inside the new file, we’ll create and export a useCounter function with the same logic we had in the two components.

Example: src/composables/counter.js
import { ref } from 'vue'

export default function useCounter() {

  const count = ref(0)

  function increment() {
    count.value++
  }

  return { count, increment }
}

That’s all we need to do to create a composable with simple functionality.

How to use a composable in a component

To use a composable in a component, we follow a simple 3-step process.

  1. Import the composable
  2. Store the returned value(s) from the composable, if any.
  3. Return the stored values from the setup method, if needed.
Example: use composable
<script>
import composable from 'path-to-composable'

export default {
  setup() {
    // store return value (if any)
    const returnedValue = composable()

    // return stored value (if needed)
    return { returnedValue }
  }
}
</script>

As an example, let’s import and use our counter composable in the ClickCounter component.

Because we returned an object with the count ref and increment method from the composable, we can store the value as an object. Then, we access the ref and method through the object with dot notation.

Example: src/components/ClickCounter.vue
<template>
  <button @click="counter.increment">Clicks: {{ counter.count }}</button>
</template>

<script>
import useCounter from '../composables/counter'

export default {
  setup() {

    // store as object
    const counter = useCounter()

    // return object to use
    // in template with dot
    // notation
    return { counter }
  }
}
</script>

If we run the example and click on the button in the browser, the counter will still work as expected.

Another option is to destructure our object . This will create separate constants for each thing we return from the composable.

As an example, let’s use the composable in our HoverCounter component, but destructure the return values.

We have to take care to destructure them in the same order they were returned from the composable. In our case, the composable returned the count ref first, then the increment method.

Example: src/components/HoverCounter.vue
<template>
  <div @mouseover="increment">Hovers: {{ count }}</div>
</template>

<script>
import useCounter from '../composables/counter'

export default {
  setup() {

    // destructure into separate constants
    const { count, increment } = useCounter()

    // return separate constants
    return { count, increment }
  }
}
</script>

<style scoped>
div{background:gainsboro}
</style>

If we run the example and hover over the gray box in the browser, the counter will still work as expected.

Composable parameters

Because a composable is just a regular function, we can accept parameters for it. This can be useful when we need to pass data like props to the composable.

As an example, let’s add an incrementBy parameter to our composable that allows us to specify a custom number we want to increment the counter with.

Example: src/composables/counter.js
import { ref } from 'vue'

// add incrementBy param with default value
export default function useCounter(incrementBy=1) {

  const count = ref(0)

  function increment() {
    // use param to increment
    count.value += incrementBy
  }

  return { count, increment }
}

In the HoverCounter component where we invoke the composable and store its returned values, we’ll add 3 as an argument.

Example: src/components/HoverCounter.vue
<template>
  <div @mouseover="increment">Hovers: {{ count }}</div>
</template>

<script>
import useCounter from '../composables/counter'

export default {
  setup() {
    // increment counter by 3              --v
    const  { count, increment } = useCounter(3)
    return { count, increment }
  }
}
</script>

<style scoped>
div{background:gainsboro}
</style>

If we run the example and hover over the gray box in the browser, the counter will increment by 3.

In the case of props, we just pass the prop’s value as an argument.

As an example, let’s bind an incBy prop to the ClickCounter component in the root App’s template.

Example: src/App.vue
<template>
  <!-- bind 'incBy' prop -->
  <click-counter :incBy="10" />
  <hr>
  <hover-counter />
</template>

<script>
import ClickCounter from './components/ClickCounter'
import HoverCounter from './components/HoverCounter'

export default {
  components: {
    ClickCounter,
    HoverCounter
  }
}
</script>

In the ClickCounter CODE component, we pass the prop as an argument to the composable.

note We still have to tell Vue which props to expect by specifying them in the props option array. We also use the props parameter in the setup method to be able to access the props we receive.

Example: src/components/ClickCounter.vue
<template>
  <button @click="counter.increment">Clicks: {{ counter.count }}</button>
</template>

<script>
import useCounter from '../composables/counter'

export default {
  // tell Vue to expect 'incBy' prop
  props: ['incBy'],
  // receive props object
  setup(props) {

    // pass prop as argument      --v
    const counter = useCounter(props.incBy)

    return { counter }
  }
}
</script>

If we run the example in the browser and click on the button, the counter will increment by 10.