agentsclimarketplace

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

Install
npx -y skills add goharabbas321/zeoel-framework --skill vue3-composition-patterns

Assembled 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:

TypeUse ForGotcha
refPrimitives, single values, template refsMust use .value in script
reactiveComplex state objects with multiple fieldsCannot reassign root; destructuring loses reactivity
shallowRefLarge arrays/objects where deep tracking is expensiveManual 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

  1. Use <script setup> for all new components — less boilerplate, better TS inference
  2. Prefer ref over reactive for most state — avoids destructuring pitfalls
  3. Name composables use* and return reactive refs, not raw values
  4. Use InjectionKey with provide/inject for type safety
  5. Use shallowRef for large data structures that don't need deep tracking
  6. Use defineModel instead of manual emit('update:modelValue') patterns
  7. Use computed for derived state — never use watch to set another ref

Anti-Patterns

  • ❌ Destructuring reactive() objects — loses reactivity (const { name } = reactive({...}))
  • ❌ Using watch to compute derived state that computed can handle
  • ❌ Creating composables that return non-reactive values (return ref, not plain values)
  • ❌ Using ref() for complex nested objects that need deep tracking (use reactive)
  • ❌ Calling inject() outside of setup() 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 4
  • vite-patterns — Build tool and dev server patterns for Vue/Vite
  • ui-to-vue — Converting design screenshots to Vue 3 components
  • tailwindcss-v4 — Styling Vue components with Tailwind

What ships with it

Read from the repository

Just SKILL.md. No reference files, no scripts.

Keep looking

Skills are one crate of 326,861. Ordering is by how many stacks a row turns up in, so the top of any crate is what has actually been picked rather than what has the most stars.