963 markdown files reformatted with the repository's pinned prettier so pnpm format:check covers the folded tree like every other repo file. The formatter's embedded-language pass also normalized code fences (TS semicolons, closed HTML tags in examples, lowercased CSS hex colors, one renumbered list that skipped an index). Alphanumeric token deltas vs the fold commit were audited file-by-file; all are formatter-equivalent markup normalizations plus the four sanitized skills.
7.1 KiB
7.1 KiB
title, impact, impactDescription, type, tags
| title | impact | impactDescription | type | tags | |||||
|---|---|---|---|---|---|---|---|---|---|
| Compose Composables for Complex Logic | MEDIUM | Building composables from other composables creates reusable, testable building blocks | best-practice |
|
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
// 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
// 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:
<script setup>
// BEFORE: All logic mixed together
import { ref, computed, watch, onMounted } from 'vue';
const searchQuery = ref('');
const filters = ref({ category: null, minPrice: 0 });
const products = ref([]);
const isLoading = ref(false);
const error = ref(null);
const sortBy = ref('name');
const sortOrder = ref('asc');
// ...50 more lines of mixed concerns
</script>
<script setup>
// AFTER: Separated into focused composables
import { useProductSearch } from './composables/useProductSearch';
import { useProductFilters } from './composables/useProductFilters';
import { useProductSort } from './composables/useProductSort';
const { searchQuery, debouncedQuery } = useProductSearch();
const { filters, activeFilters, clearFilters } = useProductFilters();
const { sortBy, sortOrder, sortedProducts } = useProductSort();
// Each composable is focused, testable, and potentially reusable
</script>
Passing Data Between Composed Composables
// 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 |