iwantcoding.com
🔥 Daily 👥 Rooms 🏆 Top Log in Sign up

Composables

A composable is a function that uses Vues reactivity APIs and returns reactive state plus operations. They are Vues equivalent of React hooks: reusable, composable, framework-aware. Use them to extract logic that uses ref/reactive/computed/watch/onMounted and keep components focused on layout.

Reusable composables: useFetch, useDebounced, useStorage

EXAMPLE
// composables/useFetch.ts — fetch with loading, error, and re-fetch
import { ref, watchEffect, type Ref } from 'vue';

export function useFetch<T>(url: Ref<string> | string) {
  const data    = ref<T | null>(null);
  const error   = ref<Error | null>(null);
  const loading = ref(false);
  const controller = ref<AbortController | null>(null);

  async function run() {
    controller.value?.abort();
    const c = new AbortController();
    controller.value = c;

    const u = typeof url === 'string' ? url : url.value;
    loading.value = true; error.value = null;
    try {
      const res = await fetch(u, { signal: c.signal });
      if (!res.ok) throw new Error('HTTP ' + res.status);
      data.value = (await res.json()) as T;
    } catch (e: any) {
      if (e.name !== 'AbortError') error.value = e;
    } finally {
      loading.value = false;
    }
  }

  // Re-run automatically when the URL ref changes
  if (typeof url !== 'string') watchEffect(run);
  else run();

  return { data, error, loading, refresh: run };
}

// composables/useDebounced.ts — debounce any ref
import { ref, watch, onScopeDispose } from 'vue';

export function useDebounced<T>(source: Ref<T>, delayMs = 250): Ref<T> {
  const out = ref<T>(source.value);
  let timer: ReturnType<typeof setTimeout> | null = null;

  watch(source, (v) => {
    if (timer) clearTimeout(timer);
    timer = setTimeout(() => { out.value = v; }, delayMs);
  });

  onScopeDispose(() => { if (timer) clearTimeout(timer); });
  return out;
}

// composables/useStorage.ts — reactive localStorage with JSON
import { ref, watch } from 'vue';

export function useStorage<T>(key: string, initial: T) {
  const raw = localStorage.getItem(key);
  const state = ref<T>(raw ? (JSON.parse(raw) as T) : initial);

  watch(state, (v) => localStorage.setItem(key, JSON.stringify(v)), { deep: true });

  function reset() { localStorage.removeItem(key); state.value = initial; }
  return { state, reset };
}

// Usage in a component
<script setup lang='ts'>
import { ref } from 'vue';
import { useFetch }     from '@/composables/useFetch';
import { useDebounced } from '@/composables/useDebounced';
import { useStorage }   from '@/composables/useStorage';

const query = ref('');
const debounced = useDebounced(query, 300);
const url = computed(() => '/api/search?q=' + encodeURIComponent(debounced.value));
const { data, error, loading } = useFetch<{ id: number; title: string }[]>(url);

const { state: prefs, reset } = useStorage('shop:prefs', { sort: 'newest', density: 'cozy' });
</script>

<template>
  <input v-model='query' placeholder='Search'>
  <p v-if='loading'>Loading...</p>
  <p v-else-if='error'>Error: {{ error.message }}</p>
  <ul v-else><li v-for='r in data' :key='r.id'>{{ r.title }}</li></ul>
</template>

Why it matters

Composables return reactive refs that the consuming component owns and unwraps in templates. Resist the urge to return plain values — once the value is no longer a ref, you have broken the reactivity link and the consumer needs to re-pull manually. Keep the contract: take refs in, return refs out.

Tip: Tweak the snippet with Try it Yourself », then sit the quiz at the bottom of the page.

Example

Example
// useCounter.js
import { ref } from 'vue';
export function useCounter() {
    const count = ref(0);
    function inc() { count.value++; }
    return { count, inc };
}
Try it Yourself »

Discussion

Loading…