Vue Composables

Extract and reuse stateful Composition API logic.

Overview

A composable is a function that packages reusable Vue state and behavior with Composition API features. Composables reduce duplication and keep components focused on presentation while reusable concerns such as fetching, mouse tracking, pagination, or authentication live in dedicated modules.

Key concepts

  • Name composables with a use prefix
  • Return reactive state and useful actions
  • Keep each composable focused
  • Clean up listeners, timers, and requests

What is a composable?

A composable is a JavaScript or TypeScript function that uses Vue's Composition API to encapsulate stateful logic. Components call the function to receive reactive state, computed values, and actions without copying the implementation.

Components reuse visual interfaces; composables reuse functionality. A useProducts composable, for example, can own product data, loading status, errors, and refresh behavior while several components decide how to display those values.

Why composables are useful

Repeated feature logic is difficult to maintain because every copy must receive the same fixes. Extracting that logic creates one clear implementation and makes components smaller.

  • Remove duplicated stateful logic
  • Group related behavior in one module
  • Make components easier to read
  • Reuse features across pages
  • Test logic separately from the interface

Folder and naming convention

Composable names conventionally begin with use. Projects commonly place them in src/composables, although the exact folder structure is a team choice.

Output
src/
 components/
 composables/
    useCounter.js
    useProducts.js
    useOnlineStatus.js
 App.vue
 main.js

Creating a simple composable

JavaScript
// composables/useCounter.js
import { computed, ref } from "vue";

export function useCounter(initialValue = 0) {
  const count = ref(initialValue);
  const doubled = computed(() => count.value * 2);

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

  function reset() {
    count.value = initialValue;
  }

  return { count, doubled, increment, reset };
}

Using a composable

Call the composable inside script setup and use its returned refs and functions directly in the template.

Vue SFC
<script setup>
import { useCounter } from "@/composables/useCounter";

const { count, doubled, increment, reset } = useCounter(5);
</script>

<template>
  <p>Count: {{ count }}</p>
  <p>Double: {{ doubled }}</p>
  <button @click="increment">Increase</button>
  <button @click="reset">Reset</button>
</template>

A reusable API composable

A composable can coordinate related asynchronous state so every consumer handles loading and errors consistently.

JavaScript
// composables/useProducts.js
import { onMounted, ref } from "vue";

export function useProducts() {
  const products = ref([]);
  const loading = ref(false);
  const error = ref("");

  async function loadProducts() {
    loading.value = true;
    error.value = "";

    try {
      const response = await fetch("/api/products");

      if (!response.ok) {
        throw new Error("Unable to load products");
      }

      products.value = await response.json();
    } catch (reason) {
      error.value = reason instanceof Error
        ? reason.message
        : "Something went wrong";
    } finally {
      loading.value = false;
    }
  }

  onMounted(loadProducts);

  return { products, loading, error, loadProducts };
}
Vue SFC
<script setup>
import { useProducts } from "@/composables/useProducts";

const { products, loading, error, loadProducts } = useProducts();
</script>

<template>
  <p v-if="loading">Loading...</p>
  <button v-else-if="error" @click="loadProducts">
    Try again: {{ error }}
  </button>
  <ul v-else>
    <li v-for="product in products" :key="product.id">
      {{ product.name }}
    </li>
  </ul>
</template>

Using lifecycle hooks

Composables can register lifecycle hooks for the component that calls them. If a composable creates an external listener, timer, or subscription, it should also remove it.

JavaScript
import { onMounted, onUnmounted, ref } from "vue";

export function useOnlineStatus() {
  const isOnline = ref(navigator.onLine);

  function update() {
    isOnline.value = navigator.onLine;
  }

  onMounted(() => {
    window.addEventListener("online", update);
    window.addEventListener("offline", update);
  });

  onUnmounted(() => {
    window.removeEventListener("online", update);
    window.removeEventListener("offline", update);
  });

  return { isOnline };
}

Passing reactive inputs

A composable can accept refs as parameters. Watching the ref lets it respond whenever the caller changes the input.

JavaScript
import { ref, watch } from "vue";

export function useSearch(query) {
  const results = ref([]);

  watch(query, async (value) => {
    if (!value.trim()) {
      results.value = [];
      return;
    }

    const response = await fetch(
      "/api/search?q=" + encodeURIComponent(value)
    );
    results.value = response.ok ? await response.json() : [];
  });

  return { results };
}

Local state and shared state

Reactive state created inside a composable function is normally new for every call. Two components calling useCounter each receive independent counters. This isolation is often the desired behavior.

State declared at module scope is shared by every caller. Use that pattern deliberately for simple shared state; for larger application-wide state with actions and Devtools support, Pinia is usually clearer.

JavaScript
import { ref } from "vue";

// Shared by every caller because it is outside the function.
const theme = ref("light");

export function useTheme() {
  function toggleTheme() {
    theme.value = theme.value === "light" ? "dark" : "light";
  }

  return { theme, toggleTheme };
}

Composables and utility functions

A normal utility function transforms values and does not need Vue reactivity. A composable is appropriate when logic uses refs, computed values, watchers, lifecycle hooks, dependency injection, or other Vue features.

Benefits of composables

  • Reusable reactive logic
  • Cleaner and smaller components
  • Less duplication
  • Feature-based organization
  • Explicit inputs and outputs
  • Simpler isolated testing
  • A flexible replacement for many mixin use cases

Best practices

  • Prefix composable functions with use
  • Give each composable one clear responsibility
  • Accept inputs instead of relying on hidden component state
  • Return only the state and actions consumers need
  • Preserve reactivity by returning refs
  • Clean up listeners, timers, subscriptions, and obsolete requests
  • Expose readonly state when consumers should use actions
  • Document whether state is per-call or shared

Common beginner mistakes

  • Combining unrelated features in one large composable
  • Forgetting to return values or actions
  • Creating a composable for a simple non-reactive utility
  • Assuming separate calls automatically share state
  • Accidentally sharing module-level state
  • Starting listeners without cleaning them up
  • Mutating composable state in many uncontrolled places
  • Ignoring loading, error, and race conditions in API composables

Key takeaways

  • A composable is a reusable function containing Vue logic
  • Composable names conventionally begin with use
  • They can return refs, computed values, and actions
  • Lifecycle hooks registered in a composable belong to its calling component
  • State inside the function is usually independent for each call
  • Module-level state is shared
  • Focused composables reduce duplication and improve maintainability
  • Pinia is often a better fit for complex global application state
Let's learn with DevBrainBox AI