SyntaxStudy
Sign Up
Vue.js Building Reusable Composables
Vue.js Beginner 1 min read

Building Reusable Composables

Composables are the Composition API's answer to Mixin and HOC patterns from Vue 2 and React respectively. A composable is a plain JavaScript function whose name conventionally starts with `use` and which calls Composition API functions internally. Because composables can maintain their own reactive state, register lifecycle hooks, and watch for changes, they encapsulate a complete feature slice independently of any component. The key advantage over mixins is explicitness: when you call `const { x, y } = useMouse()` you know exactly where `x` and `y` come from. Mixins merged everything into the component instance invisibly, leading to naming collisions and unclear dependency graphs. Composables avoid both issues. Well-designed composables accept options or reactive arguments to make them flexible, return reactive refs so the caller's template stays reactive, and handle their own cleanup in `onUnmounted`. They can also call other composables — composables compose.
Example
// src/composables/useFetch.ts
import { ref, watchEffect, toValue, type MaybeRefOrGetter } from 'vue';

export function useFetch<T>(url: MaybeRefOrGetter<string>) {
  const data      = ref<T | null>(null);
  const error     = ref<Error | null>(null);
  const isLoading = ref(false);

  watchEffect(async (onCleanup) => {
    const controller = new AbortController();
    onCleanup(() => controller.abort());

    isLoading.value = true;
    error.value     = null;
    try {
      const res    = await fetch(toValue(url), { signal: controller.signal });
      if (!res.ok) throw new Error(`HTTP ${res.status}`);
      data.value   = (await res.json()) as T;
    } catch (e) {
      if ((e as Error).name !== 'AbortError') error.value = e as Error;
    } finally {
      isLoading.value = false;
    }
  });

  return { data, error, isLoading };
}

// ── Component usage ───────────────────────────
<script setup lang="ts">
import { ref, computed }   from 'vue';
import { useFetch }        from '@/composables/useFetch';

const userId  = ref(1);
const url     = computed(() => `/api/users/${userId.value}`);
const { data: user, isLoading, error } = useFetch(url);
</script>
<template>
  <p v-if="isLoading">Loading…</p>
  <p v-else-if="error">{{ error.message }}</p>
  <pre v-else>{{ user }}</pre>
</template>