SuperAPI AI Directory

vue-patterns

Frontend & websites

View repository

Updated 10-16 09:33

About this skill

Vue 3 Frontend Patterns

Modern frontend patterns for Vue 3 with Composition API.

When to Activate

  • Building Vue 3 components (Composition API, props, emits)
  • Managing state with Pinia
  • Writing composables (equivalent to React custom hooks)
  • Implementing data fetching patterns
  • Optimizing performance (computed, v-memo, defineAsyncComponent)
  • Working with forms and validation
  • Using provide/inject for cross-component state

Component Patterns

defineProps / defineEmits (strongly typed)

interface Props {
  user: User
  variant?: 'default' | 'outlined'
}

const props = defineProps<Props>()
const emit = defineEmits<{
  select: [id: string]
  close: []
}>()

const props = withDefaults(defineProps<Props>(), {
  variant: 'default'
})

Basic structure of the Composition API

<script setup lang="ts">
import { ref, computed, watch, onMounted } from 'vue'

const count = ref(0)
const doubled = computed(() => count.value * 2)

watch(count, (newVal, oldVal) => {
  console.log(`changed from ${oldVal} to ${newVal}`)
})

onMounted(() => {
  // 初始化邏輯
})
</script>

provide / inject (cross-layer passing)

// 父層
const theme = ref<'light' | 'dark'>('light')
provide('theme', theme)

// 子孫層
const theme = inject<Ref<'light' | 'dark'>>('theme')

Composables (equivalent to React Custom Hooks)

Data fetching Composable

export function useQuery<T>(fetcher: () => Promise<T>) {
  const data = ref<T | null>(null)
  const loading = ref(false)
  const error = ref<Error | null>(null)

  async function refetch() {
    loading.value = true
    error.value = null
    try {
      data.value = await fetcher()
    } catch (e) {
      error.value = e as Error
    } finally {
      loading.value = false
    }
  }

  refetch()
  return { data, loading, error, refetch }
}

Debounce Composable

export function useDebounce<T>(source: Ref<T>, delay: number) {
  const debounced = ref<T>(source.value) as Ref<T>

  watch(source, (val) => {
    const timer = setTimeout(() => { debounced.value = val }, delay)
    return () => clearTimeout(timer)
  })

  return debounced
}

State Management(Pinia)

export const useUserStore = defineStore('user', () => {
  const users = ref<User[]>([])
  const loading = ref(false)

  const activeUsers = computed(() => users.value.filter(u => u.active))

  async function fetchUsers() {
    loading.value = true
    try {
      users.value = await api.getUsers()
    } finally {
      loading.value = false
    }
  }

  return { users, loading, activeUsers, fetchUsers }
})

Performance optimization

// computed 取代 methods 做計算
const sortedList = computed(() => [...items.value].sort(...))

// v-memo(大型列表)
<div v-for="item in list" :key="item.id" v-memo="[item.id, selected === item.id]">

// defineAsyncComponent(懶載入)
const HeavyChart = defineAsyncComponent(() => import('./HeavyChart.vue'))

Form handling

const form = ref({ email: '', name: '' })
const errors = ref<Record<string, string>>({})

function validate(): boolean {
  const e: Record<string, string> = {}
  if (!form.value.email.includes('@')) e.email = '請輸入有效的 Email'
  if (!form.value.name.trim()) e.name = '名稱不能為空'
  errors.value = e
  return Object.keys(e).length === 0
}

async function handleSubmit() {
  if (!validate()) return
  await submitForm(form.value)
}

If the project has introduced Zod (requires TypeScript), you can switch to schema.safeParse() instead of manual validate.

Common anti-patterns (avoid)

// ❌ 直接修改 props
props.user.name = 'new name'
// ✅ emit 讓父層處理
emit('update:user', { ...props.user, name: 'new name' })

// ❌ v-for 與 v-if 同元素
<div v-for="item in list" v-if="item.active">
// ✅ computed 過濾
const activeList = computed(() => list.value.filter(i => i.active))

// ❌ 忘記 .value
count = 1
// ✅
count.value = 1