Keep composables thin — functional core, imperative shell
Separate pure business logic from the reactivity wrapper. The composable acts as a thin "imperative shell" that wires reactive state to pure functions (the "functional core").
This makes business logic testable without Vue context and keeps composables focused on reactivity plumbing.
Example:
typescript
// Functional core — pure, testable without Vue
function calculateDiscount(price: number, tier: string): number {
if (tier === "gold") return price * 0.8
if (tier === "silver") return price * 0.9
return price
}
// Imperative shell — thin composable
export function useDiscount(price: Ref<number>, tier: Ref<string>) {
const discounted = computed(() => calculateDiscount(price.value, tier.value))
return { discounted }
}typescript
// Bad — business logic buried in reactivity
export function useDiscount(price: Ref<number>, tier: Ref<string>) {
const discounted = computed(() => {
if (tier.value === "gold") return price.value * 0.8
if (tier.value === "silver") return price.value * 0.9
return price.value
})
return { discounted }
}Rule for AI agents
- PREFER extracting calculations to pure helper functions; composable only handles reactivityEslint rule
No ESLint rule available