Vue3 composition patterns
Skill goharabbas321/zeoel-framework/.agents/skills/zeoel/skills/vue3-composition-patterns
Vue 3 Composition API patterns for composables, reactivity (ref, reactive, computed, watch), provide/inject, defineModel, Teleport, Suspense, async components, TypeScript integration, and performance optimization. Activate when building or architecting Vue 3 applications.From its SKILL.md
npx -y skills add goharabbas321/zeoel-framework --skill vue3-composition-patternsAssembled from the repository path, not quoted from the project. Check it against their README if it does not work.
2 things to look at
- no licenseNo license file was found in the repository. Code published without one is not open source by default, so using it at work is a question for whoever answers licensing questions where you are.
- 0 stars0 stars. Stars are a popularity signal and not a quality one, but at this level it is likely that nobody has read this closely except its author, and you would be relying on your own review.
SKILL.md
10.6 KB, ~2.7k tokens by cl100k_base, as published. Nobody here has run it
Vue 3 Composition API Patterns
Overview
The Composition API is Vue 3's primary API for building components. It replaces the Options API's rigid structure with composable functions, enabling better code organization, TypeScript inference, and logic reuse. This skill covers production-grade patterns for the Composition API, <script setup>, composables, and Vue 3.5+ features.
When to Use
- Building or refactoring Vue 3 components with
<script setup> - Creating reusable composables (
use*functions) - Managing complex reactive state with
ref,reactive,computed,watch - Implementing dependency injection with
provide/inject - Using advanced features: Teleport, Suspense, async components
- Writing type-safe Vue components with TypeScript
- Optimizing Vue app performance
Reactivity Fundamentals
ref vs reactive vs shallowRef
import { ref, reactive, shallowRef, triggerRef } from 'vue'
// ref — wraps ANY value (primitives + objects). Access via .value
const count = ref(0)
count.value++ // triggers reactivity
// reactive — deep reactive proxy for OBJECTS ONLY. No .value needed
const state = reactive({
user: { name: 'Ali', email: '[email protected]' },
items: [],
})
state.user.name = 'Hassan' // triggers reactivity
// shallowRef — only .value reassignment triggers updates (not deep mutations)
const heavyList = shallowRef<Item[]>([])
heavyList.value = [...heavyList.value, newItem] // triggers
heavyList.value.push(newItem) // does NOT trigger — use triggerRef(heavyList)
When to use which:
| Type | Use For | Gotcha |
|---|---|---|
ref | Primitives, single values, template refs | Must use .value in script |
reactive | Complex state objects with multiple fields | Cannot reassign root; destructuring loses reactivity |
shallowRef | Large arrays/objects where deep tracking is expensive | Manual triggerRef() for mutations |
Computed and Watchers
import { ref, computed, watch, watchEffect, watchPostEffect } from 'vue'
// Computed — derived state (cached, lazy)
const firstName = ref('Ali')
const lastName = ref('Hassan')
const fullName = computed(() => `${firstName.value} ${lastName.value}`)
// Writable computed
const fullNameWritable = computed({
get: () => `${firstName.value} ${lastName.value}`,
set: (val: string) => {
const [first, ...rest] = val.split(' ')
firstName.value = first
lastName.value = rest.join(' ')
},
})
// watch — explicit source, old/new values, lazy by default
watch(count, (newVal, oldVal) => {
console.log(`Changed from ${oldVal} to ${newVal}`)
})
// Watch multiple sources
watch([firstName, lastName], ([newFirst, newLast], [oldFirst, oldLast]) => {
// fires when either changes
})
// Deep watch on reactive object
watch(() => state.user, (newUser) => {
// fires on any nested change
}, { deep: true })
// watchEffect — auto-tracks dependencies, runs immediately
watchEffect(() => {
document.title = `${count.value} items`
})
// watchPostEffect — runs after DOM updates
watchPostEffect(() => {
// safe to read updated DOM here
})
Composables Architecture
// composables/useCounter.ts — Reusable stateful logic
import { ref, computed, type Ref } from 'vue'
interface UseCounterOptions {
min?: number
max?: number
initial?: number
}
export function useCounter(options: UseCounterOptions = {}) {
const { min = 0, max = Infinity, initial = 0 } = options
const count = ref(initial)
const increment = () => {
if (count.value < max) count.value++
}
const decrement = () => {
if (count.value > min) count.value--
}
const reset = () => { count.value = initial }
const isMin = computed(() => count.value <= min)
const isMax = computed(() => count.value >= max)
return { count, increment, decrement, reset, isMin, isMax }
}
// composables/useFetch.ts — Async data fetching composable
import { ref, shallowRef, watchEffect, type Ref } from 'vue'
interface UseFetchReturn<T> {
data: Ref<T | null>
error: Ref<Error | null>
isLoading: Ref<boolean>
refresh: () => Promise<void>
}
export function useFetch<T>(url: Ref<string> | string): UseFetchReturn<T> {
const data = shallowRef<T | null>(null)
const error = ref<Error | null>(null)
const isLoading = ref(false)
async function doFetch() {
isLoading.value = true
error.value = null
try {
const resolvedUrl = typeof url === 'string' ? url : url.value
const response = await fetch(resolvedUrl)
if (!response.ok) throw new Error(`HTTP ${response.status}`)
data.value = await response.json()
} catch (e) {
error.value = e instanceof Error ? e : new Error(String(e))
} finally {
isLoading.value = false
}
}
// Auto-refetch when URL changes (if url is a ref)
if (typeof url !== 'string') {
watchEffect(() => { doFetch() })
} else {
doFetch()
}
return { data, error, isLoading, refresh: doFetch }
}
<!-- Usage in component -->
<script setup lang="ts">
import { ref } from 'vue'
import { useFetch } from '@/composables/useFetch'
import { useCounter } from '@/composables/useCounter'
const apiUrl = ref('/api/users')
const { data: users, isLoading, error } = useFetch<User[]>(apiUrl)
const { count, increment, decrement } = useCounter({ min: 0, max: 100 })
</script>
Provide / Inject (Dependency Injection)
// providers/theme.ts — Type-safe provide/inject
import { provide, inject, ref, type InjectionKey, type Ref } from 'vue'
interface ThemeContext {
theme: Ref<'light' | 'dark'>
toggleTheme: () => void
}
export const ThemeKey: InjectionKey<ThemeContext> = Symbol('theme')
export function provideTheme() {
const theme = ref<'light' | 'dark'>('light')
const toggleTheme = () => {
theme.value = theme.value === 'light' ? 'dark' : 'light'
}
provide(ThemeKey, { theme, toggleTheme })
return { theme, toggleTheme }
}
export function useTheme(): ThemeContext {
const ctx = inject(ThemeKey)
if (!ctx) throw new Error('useTheme() must be used within a ThemeProvider')
return ctx
}
defineModel (Vue 3.4+)
<!-- Two-way binding with defineModel -->
<script setup lang="ts">
// Parent passes v-model:title="pageTitle"
const title = defineModel<string>('title', { required: true })
const isOpen = defineModel<boolean>('open', { default: false })
// Computed transform on model
const titleUpperCase = computed({
get: () => title.value.toUpperCase(),
set: (val) => { title.value = val },
})
</script>
<template>
<input v-model="title" />
<p>{{ titleUpperCase }}</p>
</template>
defineProps with TypeScript
<script setup lang="ts">
// Type-based props (preferred in TS projects)
interface Props {
title: string
count?: number
items: string[]
status: 'active' | 'inactive' | 'pending'
onClick?: (id: string) => void
}
const props = withDefaults(defineProps<Props>(), {
count: 0,
status: 'active',
})
// Generic components (Vue 3.3+)
// <script setup lang="ts" generic="T extends { id: string }">
// const props = defineProps<{ items: T[]; selected?: T }>()
// const emit = defineEmits<{ select: [item: T] }>()
</script>
Teleport, Suspense, and Async Components
<!-- Teleport — render content elsewhere in the DOM -->
<template>
<Teleport to="body">
<div v-if="showModal" class="modal-overlay">
<div class="modal-content">
<slot />
</div>
</div>
</Teleport>
</template>
<!-- Suspense — handle async component loading -->
<template>
<Suspense>
<template #default>
<AsyncDashboard />
</template>
<template #fallback>
<LoadingSkeleton />
</template>
</Suspense>
</template>
<script setup>
import { defineAsyncComponent } from 'vue'
const AsyncDashboard = defineAsyncComponent({
loader: () => import('./Dashboard.vue'),
loadingComponent: LoadingSkeleton,
errorComponent: ErrorFallback,
delay: 200,
timeout: 10000,
})
</script>
Vue 3.5+ Features
<script setup lang="ts">
import { useTemplateRef, onMounted } from 'vue'
// useTemplateRef (3.5+) — type-safe template refs
const inputRef = useTemplateRef<HTMLInputElement>('emailInput')
onMounted(() => {
inputRef.value?.focus()
})
</script>
<template>
<input ref="emailInput" type="email" />
</template>
Performance Optimization
<template>
<!-- v-once — render once, never re-render -->
<footer v-once>
<p>© 2025 Company Name. All rights reserved.</p>
</footer>
<!-- v-memo — skip re-render unless dependencies change -->
<div v-for="item in list" :key="item.id" v-memo="[item.id, item.selected]">
<ExpensiveComponent :item="item" />
</div>
</template>
<script setup>
import { shallowReactive, markRaw } from 'vue'
// shallowReactive — only root-level properties are reactive
const state = shallowReactive({
config: markRaw(hugeConfigObject), // markRaw — never make this reactive
items: [],
})
</script>
Guidelines
- Use
<script setup>for all new components — less boilerplate, better TS inference - Prefer
refoverreactivefor most state — avoids destructuring pitfalls - Name composables
use*and return reactive refs, not raw values - Use
InjectionKeywith provide/inject for type safety - Use
shallowReffor large data structures that don't need deep tracking - Use
defineModelinstead of manualemit('update:modelValue')patterns - Use
computedfor derived state — never usewatchto set another ref
Anti-Patterns
- ❌ Destructuring
reactive()objects — loses reactivity (const { name } = reactive({...})) - ❌ Using
watchto compute derived state thatcomputedcan handle - ❌ Creating composables that return non-reactive values (return
ref, not plain values) - ❌ Using
ref()for complex nested objects that need deep tracking (usereactive) - ❌ Calling
inject()outside ofsetup()or composable context - ❌ Forgetting to handle the loading/error states with
Suspense - ❌ Using Options API (
data(),methods,computed) in new Vue 3 components
Related Skills
nuxt4-patterns— SSR, data fetching, route rules for Nuxt 4vite-patterns— Build tool and dev server patterns for Vue/Viteui-to-vue— Converting design screenshots to Vue 3 componentstailwindcss-v4— Styling Vue components with Tailwind
What ships with it
Read from the repository
Just SKILL.md. No reference files, no scripts.