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 as useCart or useTheme.
  • 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.