Vue.js Composables
A composable is a plain JavaScript function that packages reactive logic for reuse. Many components need the same behavior: a counter, a mouse position tracker, a dark mode switch, or a data loader. Copying that code into each component creates clutter and bugs. A composable holds the code once, and every component borrows it.
Think of a kitchen tool such as a blender. One blender serves every recipe in the house. Nobody builds a new blender for each dish. A composable is that blender for your component logic.
Build Your First Composable
Create a file named useCounter.js. The use prefix is a naming habit that signals "this is a composable." The function creates reactive state, defines actions, and returns what the component needs.
// useCounter.js
import { ref } from "vue";
export function useCounter(start = 0) {
const count = ref(start);
function increase() { count.value++; }
function decrease() { count.value--; }
function reset() { count.value = start; }
return { count, increase, decrease, reset };
}Any component can import the function and call it inside <script setup>.
<script setup>
import { useCounter } from "./useCounter.js";
const { count, increase, decrease, reset } = useCounter(10);
</script>
<template>
<p>Count: {{ count }}</p>
<button @click="increase">+</button>
<button @click="decrease">-</button>
<button @click="reset">Reset</button>
</template>Diagram: Shared Logic, Separate State
useCounter.js (written once)
|
+--> ComponentA calls useCounter(10) --> its own count = 10
|
+--> ComponentB calls useCounter(0) --> its own count = 0
Each call creates new state. Components do not affect each other.A Composable with Cleanup
Some logic attaches listeners to the browser. A composable can use lifecycle hooks to start and stop that work together with the component.
// useMouse.js
import { ref, onMounted, onUnmounted } from "vue";
export function useMouse() {
const x = ref(0);
const y = ref(0);
function update(event) {
x.value = event.pageX;
y.value = event.pageY;
}
onMounted(() => window.addEventListener("mousemove", update));
onUnmounted(() => window.removeEventListener("mousemove", update));
return { x, y };
}The listener starts when a component mounts and stops when the component leaves the page. Forgotten listeners slow down an application, and this pattern prevents them.
Diagram: Lifecycle Inside a Composable
Component mounts --> onMounted --> listener added Mouse moves --> x and y update --> template redraws Component unmounts --> onUnmounted --> listener removed
A Composable for Loading Data
Loading data follows the same three states in every project: waiting, success, and failure. One composable can cover all three.
// useFetch.js
import { ref } from "vue";
export function useFetch(url) {
const data = ref(null);
const error = ref(null);
const loading = ref(true);
fetch(url)
.then((res) => res.json())
.then((json) => { data.value = json; })
.catch((err) => { error.value = err; })
.finally(() => { loading.value = false; });
return { data, error, loading };
}A component shows a spinner while loading is true, an error message when error has a value, and the result when data arrives.
Rules for Good Composables
- Start the name with
use, such asuseCartoruseTheme. - Return refs so components keep reactivity when they destructure the result.
- Call composables at the top of
setup, not inside loops or conditions. - Keep each composable focused on one job.
- Clean up timers, listeners, and requests when the component unmounts.
Composables Compared with Older Patterns
Mixins from Vue 2 merged properties into a component from the outside, which made it hard to see where a value came from. A composable returns named values that a component receives explicitly. Name clashes become easy to spot, and tools understand the code clearly.
Diagram: Mixin and Composable
Mixin: component <-- hidden merge -- mixin (origin unclear)
Composable: const { count } = useCounter() (origin visible)Summary
Composables move reactive logic into reusable functions. Each call creates fresh state, lifecycle hooks handle setup and cleanup, and the returned refs plug straight into any template. A clear use name, one job per function, and top-level calls keep composables predictable. Counters, mouse trackers, and data loaders become short, shareable files.
