--- title: Compose Composables for Complex Logic impact: MEDIUM impactDescription: Building composables from other composables creates reusable, testable building blocks type: best-practice tags: [vue3, composables, composition-api, patterns, code-organization] --- # Compose Composables for Complex Logic **Impact: MEDIUM** - Composables can (and should) call other composables. This composition pattern allows you to build complex functionality from smaller, focused, reusable pieces. Each composable handles one concern, and higher-level composables combine them. This is one of the key advantages of the Composition API over mixins - dependencies are explicit and traceable. ## Task Checklist - [ ] Extract reusable logic into focused, single-purpose composables - [ ] Build complex composables by combining simpler ones - [ ] Ensure each composable has a single responsibility - [ ] Pass data between composed composables via parameters or refs **Example: Building a Mouse Tracker from Smaller Composables** ```javascript // composables/useEventListener.js - Low-level building block import { onMounted, onUnmounted, toValue } from 'vue'; export function useEventListener(target, event, callback) { onMounted(() => { const el = toValue(target); el.addEventListener(event, callback); }); onUnmounted(() => { const el = toValue(target); el.removeEventListener(event, callback); }); } // composables/useMouse.js - Composes useEventListener import { ref } from 'vue'; import { useEventListener } from './useEventListener'; export function useMouse() { const x = ref(0); const y = ref(0); function update(event) { x.value = event.pageX; y.value = event.pageY; } // Reuse the event listener composable useEventListener(window, 'mousemove', update); return { x, y }; } // composables/useMouseInElement.js - Composes useMouse import { ref, computed } from 'vue'; import { useMouse } from './useMouse'; export function useMouseInElement(elementRef) { const { x, y } = useMouse(); const elementX = computed(() => { if (!elementRef.value) return 0; const rect = elementRef.value.getBoundingClientRect(); return x.value - rect.left; }); const elementY = computed(() => { if (!elementRef.value) return 0; const rect = elementRef.value.getBoundingClientRect(); return y.value - rect.top; }); const isOutside = computed(() => { if (!elementRef.value) return true; const rect = elementRef.value.getBoundingClientRect(); return ( x.value < rect.left || x.value > rect.right || y.value < rect.top || y.value > rect.bottom ); }); return { x, y, elementX, elementY, isOutside }; } ``` ## Pattern: Composable Dependency Chain ```javascript // Layer 1: Primitives export function useEventListener(target, event, callback) { /* ... */ } export function useInterval(callback, delay) { /* ... */ } export function useTimeout(callback, delay) { /* ... */ } // Layer 2: Building on primitives export function useWindowSize() { const width = ref(window.innerWidth); const height = ref(window.innerHeight); useEventListener(window, 'resize', () => { width.value = window.innerWidth; height.value = window.innerHeight; }); return { width, height }; } export function useOnline() { const isOnline = ref(navigator.onLine); useEventListener(window, 'online', () => (isOnline.value = true)); useEventListener(window, 'offline', () => (isOnline.value = false)); return { isOnline }; } // Layer 3: Complex features combining multiple composables export function useAutoSave(dataRef, saveFunction, options = {}) { const { debounce = 1000, onlyWhenOnline = true } = options; const { isOnline } = useOnline(); const isSaving = ref(false); const lastSaved = ref(null); let timeoutId = null; watch( dataRef, (newData) => { if (onlyWhenOnline && !isOnline.value) return; clearTimeout(timeoutId); timeoutId = setTimeout(async () => { isSaving.value = true; try { await saveFunction(newData); lastSaved.value = new Date(); } finally { isSaving.value = false; } }, debounce); }, { deep: true }, ); return { isSaving, lastSaved, isOnline }; } ``` ## Pattern: Code Organization with Composition Extract inline composables when a component gets complex: ```vue ``` ```vue ``` ## Passing Data Between Composed Composables ```javascript // Composables can accept refs from other composables export function useFilteredProducts(products, filters) { return computed(() => { let result = toValue(products); if (filters.value.category) { result = result.filter((p) => p.category === filters.value.category); } if (filters.value.minPrice > 0) { result = result.filter((p) => p.price >= filters.value.minPrice); } return result; }); } export function useSortedProducts(products, sortConfig) { return computed(() => { const items = [...toValue(products)]; const { field, order } = sortConfig.value; return items.sort((a, b) => { const comparison = a[field] > b[field] ? 1 : -1; return order === 'asc' ? comparison : -comparison; }); }); } // Usage - composables are chained through their outputs const { products, isLoading } = useFetch('/api/products'); const { filters } = useFilters(); const filteredProducts = useFilteredProducts(products, filters); const { sortConfig } = useSortConfig(); const sortedProducts = useSortedProducts(filteredProducts, sortConfig); ``` ## Advantages Over Mixins | Composables | Mixins | | --------------------------------- | --------------------------------- | | Explicit dependencies via imports | Implicit dependencies | | Clear data flow via parameters | Unclear which mixin provides what | | No namespace collisions | Properties can conflict | | Easy to trace and debug | Hard to track origins | | TypeScript-friendly | Poor TypeScript support | ## Reference - [Vue.js Composables](https://vuejs.org/guide/reusability/composables.html) - [Vue.js Composables vs Mixins](https://vuejs.org/guide/reusability/composables.html#comparisons-with-other-techniques)