A Vue composable is a function that uses the Composition API to package and reuse stateful logic across multiple components. Unlike plain utility helpers such as formatDate(), composables manage reactive ref state, watchers, and lifecycle hooks.
By convention, every composable function name starts with use, such as useFetch, useMouse, or useDebounce. This makes it immediately obvious where reactive state comes from inside any <script setup> block.
flowchart TD
Comp["useFetch(urlSource)"] --> State["data, error, isLoading refs"]
State --> CardA["PostCard.vue (own state instance)"]
State --> CardB["UserCard.vue (own state instance)"]
use Prefix: Follow useFeatureName so developers recognize Composition API hooks at a glance.toValue(): Using toValue(urlSource) (Vue 3.3+) lets callers pass a plain string, a ref, or a () => ... getter function.{ data, error, isLoading } lets callers destructure cleanly without losing reactivity.Here is a reusable src/composables/useFetch.js composable that re-fetches automatically whenever a reactive URL getter changes.
import { ref, watchEffect, toValue } from 'vue';
export function useFetch(urlSource) {
const data = ref(null);
const error = ref(null);
const isLoading = ref(true);
async function fetchData() {
const url = toValue(urlSource);
isLoading.value = true;
error.value = null;
try {
const res = await fetch(url);
if (!res.ok) throw new Error(`HTTP ${res.status}`);
data.value = await res.json();
} catch (err) {
error.value = err.message;
} finally {
isLoading.value = false;
}
}
watchEffect(() => {
fetchData();
});
return { data, error, isLoading, refetch: fetchData };
}
Consume useFetch inside any component's <script setup> block. Each component instance gets its own isolated reactive state.
<script setup>
import { ref } from 'vue';
import { useFetch } from './composables/useFetch';
const postId = ref(1);
const { data: post, error, isLoading } = useFetch(
() => `https://jsonplaceholder.typicode.com/posts/${postId.value}`
);
</script>
<template>
<div class="post-viewer">
<button @click="postId++">Next Post (ID: {{ postId }})</button>
<p v-if="isLoading">Loading post...</p>
<p v-else-if="error" class="error">Error: {{ error }}</p>
<article v-else-if="post">
<h3>{{ post.title }}</h3>
<p>{{ post.body }}</p>
</article>
</div>
</template>
reactive() Object from a Composable: If a caller destructures const { data } = useFetch(...) from a reactive object, reactivity breaks. Always return a plain object holding ref values.setTimeout or Event Clicks: Composables that register onMounted or watchEffect must be called synchronously at the top level of <script setup>.const data = ref(null) outside export function useFetch() shares one ref across every component instance. Keep per-instance refs inside the composable function body.No. Each call to useFetch() creates fresh ref instances for that component. If you actually want shared global state, use Pinia or declare state outside the function intentionally.
Place shared composables in src/composables/ (for example src/composables/useFetch.js), and keep feature-specific composables near the components that use them.
VueUse instead of writing common composables myself?Yes. VueUse is a well-tested collection of Vue 3 composables like useLocalStorage, useDebounceFn, and useMediaQuery.
Sign in to track your learning journey, earn industry-recognized certificates, and join our elite developer community.
Quick Access With