Composables (Custom Hooks)
Extract and reuse stateful logic across components by writing your own composable functions.
What Is a Composable?
A composable is a plain JavaScript function that uses Composition API functions (ref, computed, watch, lifecycle hooks) to encapsulate and expose a piece of reusable, stateful logic. It's Vue's answer to what React calls a "custom hook."
Writing Your First Composable
A simple counter, extracted into a reusable function so any component can get its own independent instance of this behavior.
// composables/useCounter.jsimport { ref } from 'vue';
export function useCounter(initialValue = 0) { const count = ref(initialValue);
function increment() { count.value++; }
function decrement() { count.value--; }
return { count, increment, decrement };}<script setup>import { useCounter } from '../composables/useCounter';
const { count, increment, decrement } = useCounter(10);</script>
<template> <button @click="decrement">-</button> {{ count }} <button @click="increment">+</button></template>A Data-Fetching Composable
Composables shine for repeated patterns — like the loading/error/data cycle around a fetch call, written once and reused everywhere it's needed.
// composables/useFetch.jsimport { ref, watchEffect } from 'vue';
export function useFetch(url) { const data = ref(null); const error = ref(null); const loading = ref(true);
watchEffect(async () => { loading.value = true; try { const res = await fetch(url.value ?? url); data.value = await res.json(); } catch (err) { error.value = err; } finally { loading.value = false; } });
return { data, error, loading };}<script setup>import { useFetch } from '../composables/useFetch';
const { data: posts, loading, error } = useFetch('/api/posts');</script>
<template> <p v-if="loading">Loading…</p> <p v-else-if="error">Something went wrong.</p> <ul v-else> <li v-for="post in posts" :key="post.id">{{ post.title }}</li> </ul></template>Composable Naming Convention
By strong convention, composables are named starting with use — useCounter, useFetch, useMousePosition — making it immediately clear in code that a function is a composable, not a plain utility function.
Common Beginner Mistakes
Like React hooks, composables that use lifecycle hooks should be called unconditionally at the top level of setup(), so Vue can correctly associate them with the current component instance.
Calling useCounter() inside each component gives each one its own independent state — if you actually want shared state across the whole app, that's what Pinia is for.
FAQs
Yes — composables compose naturally, just like regular functions calling other functions.
No strict requirement, but composables/ at the project root (or per-feature) is the near-universal convention.
Summary
Composables let you extract and reuse stateful logic as plain functions, avoiding duplication across components. Next, you'll see the Options API up close and how it maps onto everything you've learned so far.