Composables are functions that use Vue's Composition API to encapsulate and reuse stateful logic. They're the Vue 3 equivalent of mixins, but better.
Composable vs Mixin
Mixins (Vue 2 style - problematic)
javascript// mixin.js export const counterMixin = { data() { return { count: 0 } // Where does 'count' come from in component? }, methods: { increment() { this.count++ } // Name conflicts? } } // component export default { mixins: [counterMixin, anotherMixin], // What if both have 'count'? // Can't tell which mixin provides what }
Composables (Vue 3 - clear and explicit)
typescript// useCounter.ts import { ref } from 'vue' export function useCounter(initialValue = 0) { const count = ref(initialValue) function increment() { count.value++ } function decrement() { count.value-- } return { count, increment, decrement } } // component <script setup> import { useCounter } from './useCounter' // Explicit destructuring - clear what's being used const { count, increment } = useCounter(10) const { count: otherCount, increment: otherIncrement } = useCounter(0) // No conflicts, easy renaming! </script>
Anatomy of a Composable
typescriptimport { ref, computed, onMounted, onUnmounted } from 'vue' export function useFeature(options = {}) { // 1. Reactive State const state = ref(initialValue) const loading = ref(false) const error = ref<Error | null>(null) // 2. Computed Properties const derived = computed(() => state.value * 2) // 3. Methods function doSomething() { state.value++ } async function fetchData() { loading.value = true try { state.value = await api.getData() } catch (e) { error.value = e as Error } finally { loading.value = false } } // 4. Lifecycle Hooks onMounted(() => { // Setup }) onUnmounted(() => { // Cleanup }) // 5. Return Values return { // State (refs) state, loading, error, derived, // Methods doSomething, fetchData } }
Naming Convention
Composables should start with use:
typescript// ✅ Good names useCounter() useMouse() useLocalStorage() useFetch() useAuth() // ❌ Bad names counter() getMouse() localStorageHelper()
Example: useMouse
typescript// useMouse.ts import { ref, onMounted, onUnmounted } from 'vue' export function useMouse() { const x = ref(0) const y = ref(0) function update(event: MouseEvent) { x.value = event.clientX y.value = event.clientY } onMounted(() => { window.addEventListener('mousemove', update) }) onUnmounted(() => { window.removeEventListener('mousemove', update) }) return { x, y } }
vue<!-- Usage --> <script setup> import { useMouse } from './useMouse' const { x, y } = useMouse() </script> <template> <p>Mouse: {{ x }}, {{ y }}</p> </template>
Example: useLocalStorage
typescript// useLocalStorage.ts import { ref, watch } from 'vue' export function useLocalStorage<T>(key: string, defaultValue: T) { // Read from localStorage or use default const stored = localStorage.getItem(key) const value = ref<T>( stored !== null ? JSON.parse(stored) : defaultValue ) // Sync to localStorage on changes watch( value, (newValue) => { if (newValue === null) { localStorage.removeItem(key) } else { localStorage.setItem(key, JSON.stringify(newValue)) } }, { deep: true } ) return value }
vue<!-- Usage --> <script setup> import { useLocalStorage } from './useLocalStorage' const theme = useLocalStorage('theme', 'light') const savedItems = useLocalStorage('cart', []) function toggleTheme() { theme.value = theme.value === 'light' ? 'dark' : 'light' } </script>
Composables Can Use Other Composables
typescript// useMouseInElement.ts import { ref, computed } from 'vue' import { useMouse } from './useMouse' import { useElementBounds } from './useElementBounds' export function useMouseInElement(target: Ref<HTMLElement | null>) { const { x: mouseX, y: mouseY } = useMouse() const { left, top, width, height } = useElementBounds(target) const x = computed(() => mouseX.value - left.value) const y = computed(() => mouseY.value - top.value) const isInside = computed(() => x.value >= 0 && x.value <= width.value && y.value >= 0 && y.value <= height.value ) return { x, y, isInside } }