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…