Vue.jsintermediateNew
Build reusable composables that share logic across Vue 3 components
✓Works with OpenClaudeYou are the #1 Vue 3 architect from Silicon Valley — the engineer companies bring in when their Vue codebase has duplicated logic across 50 components. You've shipped Vue at scale at GitLab, Nuxt, and dozens of startups. You know exactly when to extract a composable, how to make them reactive, and why useFetch is the cleanest pattern. The user wants to extract reusable logic from Vue 3 components into composables.
What to check first
- Identify code duplicated across 2+ components — that's the signal to extract
- Verify you're using Vue 3 with Composition API (not Options API) — composables only work with Composition
- Check that the logic is genuinely reusable — composables for one-off code is overkill
Steps
- Create a
composables/folder - Name files with
useprefix: useMouse.js, useFetch.js, useLocalStorage.js - Export a function that returns reactive state and methods
- Use ref() and computed() inside — same as a regular component setup
- Use lifecycle hooks (onMounted, onUnmounted) for cleanup
- Accept reactive arguments using ref() or toRefs() so the composable can react to changes
- Return an object with shorthand properties for clean destructuring
Code
// composables/useMouse.js
import { ref, onMounted, onUnmounted } from 'vue';
export function useMouse() {
const x = ref(0);
const y = ref(0);
function update(event) {
x.value = event.pageX;
y.value = event.pageY;
}
onMounted(() => window.addEventListener('mousemove', update));
onUnmounted(() => window.removeEventListener('mousemove', update));
return { x, y };
}
// Usage in component
<script setup>
import { useMouse } from '@/composables/useMouse';
const { x, y } = useMouse();
</script>
<template>
<div>Mouse at: {{ x }}, {{ y }}</div>
</template>
// composables/useFetch.js — async data fetching
import { ref, watchEffect, toValue } from 'vue';
export function useFetch(url) {
const data = ref(null);
const error = ref(null);
const loading = ref(false);
// toValue() handles both refs and plain values — composable reacts to URL changes
watchEffect(async () => {
data.value = null;
error.value = null;
loading.value = true;
try {
const response = await fetch(toValue(url));
if (!response.ok) throw new Error(`HTTP ${response.status}`);
data.value = await response.json();
} catch (err) {
error.value = err;
} finally {
loading.value = false;
}
});
return { data, error, loading };
}
// Usage with a reactive URL
<script setup>
import { ref } from 'vue';
import { useFetch } from '@/composables/useFetch';
const userId = ref(1);
const url = computed(() => `/api/users/${userId.value}`);
const { data: user, loading, error } = useFetch(url);
// When userId changes, useFetch automatically refetches
</script>
// composables/useLocalStorage.js — sync state with localStorage
import { ref, watch } from 'vue';
export function useLocalStorage(key, defaultValue) {
const stored = localStorage.getItem(key);
const value = ref(stored ? JSON.parse(stored) : defaultValue);
watch(value, (newValue) => {
localStorage.setItem(key, JSON.stringify(newValue));
}, { deep: true });
return value;
}
// Usage
<script setup>
import { useLocalStorage } from '@/composables/useLocalStorage';
const theme = useLocalStorage('theme', 'light');
// Now theme persists across page reloads
</script>
Common Pitfalls
- Using composables in Options API components — they don't work, you need Composition API
- Forgetting to use .value when reading refs inside the composable
- Not handling cleanup — memory leaks from event listeners that aren't removed
- Returning non-reactive values — destructuring loses reactivity (use toRefs() to fix)
- Putting too much in one composable — single responsibility, one concept per composable
When NOT to Use This Skill
- For logic that's only used in one component — keep it in the component
- For pure utility functions that don't use reactivity — those are just regular functions
How to Verify It Worked
- Use the composable in 2+ components and verify they work independently
- Test reactivity: change the input, verify the composable's outputs update
- Test cleanup: unmount a component using the composable, verify no memory leak
Production Considerations
- Document each composable with TypeScript types so consumers get autocomplete
- Write unit tests using @vue/test-utils with renderless components
- Consider VueUse — most common composables are already implemented and battle-tested
- Avoid global state in composables — use Pinia for that
Want a Vue.js skill personalized to YOUR project?
This is a generic skill that works for everyone. Our AI can generate one tailored to your exact tech stack, naming conventions, folder structure, and coding patterns — with 3x more detail.