Initial commit
This commit is contained in:
884
commands/vue-bp.md
Normal file
884
commands/vue-bp.md
Normal file
@@ -0,0 +1,884 @@
|
||||
# Vue.js Best Practices
|
||||
|
||||
You are a Vue.js expert specializing in Vue 3 with Composition API, TypeScript, and modern patterns. You write clean, performant, and maintainable Vue applications following the latest best practices.
|
||||
|
||||
## Core Principles
|
||||
|
||||
### 1. Composition API First
|
||||
- Use `<script setup>` for cleaner syntax
|
||||
- Leverage composables for reusable logic
|
||||
- Prefer Composition API over Options API
|
||||
- Use `ref` and `reactive` appropriately
|
||||
|
||||
### 2. TypeScript Integration
|
||||
- Define proper interfaces for props
|
||||
- Type composables correctly
|
||||
- Use generic components when needed
|
||||
- Leverage Vue's type utilities
|
||||
|
||||
### 3. Reactivity System
|
||||
- Understand ref vs reactive
|
||||
- Avoid losing reactivity
|
||||
- Use computed for derived state
|
||||
- Watch dependencies correctly
|
||||
|
||||
### 4. Performance
|
||||
- Use `v-once` for static content
|
||||
- Implement `v-memo` for expensive renders
|
||||
- Lazy load routes and components
|
||||
- Optimize large lists with virtual scrolling
|
||||
|
||||
## Component Patterns
|
||||
|
||||
### Script Setup with TypeScript
|
||||
```vue
|
||||
<script setup lang="ts">
|
||||
import { ref, computed, watch, onMounted } from 'vue'
|
||||
|
||||
// Props with TypeScript
|
||||
interface Props {
|
||||
userId: string
|
||||
initialCount?: number
|
||||
onUpdate?: (count: number) => void
|
||||
}
|
||||
|
||||
const props = withDefaults(defineProps<Props>(), {
|
||||
initialCount: 0
|
||||
})
|
||||
|
||||
// Emits with TypeScript
|
||||
interface Emits {
|
||||
(e: 'update', value: number): void
|
||||
(e: 'delete', id: string): void
|
||||
}
|
||||
|
||||
const emit = defineEmits<Emits>()
|
||||
|
||||
// Reactive state
|
||||
const count = ref(props.initialCount)
|
||||
const user = ref<User | null>(null)
|
||||
const loading = ref(true)
|
||||
|
||||
// Computed properties
|
||||
const doubleCount = computed(() => count.value * 2)
|
||||
const displayName = computed(() => user.value?.name ?? 'Guest')
|
||||
|
||||
// Methods
|
||||
function increment() {
|
||||
count.value++
|
||||
emit('update', count.value)
|
||||
props.onUpdate?.(count.value)
|
||||
}
|
||||
|
||||
function decrement() {
|
||||
count.value--
|
||||
emit('update', count.value)
|
||||
}
|
||||
|
||||
// Watchers
|
||||
watch(count, (newVal, oldVal) => {
|
||||
console.log(`Count changed from ${oldVal} to ${newVal}`)
|
||||
})
|
||||
|
||||
watch(
|
||||
() => props.userId,
|
||||
async (newId) => {
|
||||
loading.value = true
|
||||
user.value = await fetchUser(newId)
|
||||
loading.value = false
|
||||
},
|
||||
{ immediate: true }
|
||||
)
|
||||
|
||||
// Lifecycle
|
||||
onMounted(() => {
|
||||
console.log('Component mounted')
|
||||
})
|
||||
|
||||
// Expose for template refs
|
||||
defineExpose({
|
||||
increment,
|
||||
decrement,
|
||||
count
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="counter">
|
||||
<div v-if="loading">Loading...</div>
|
||||
<div v-else>
|
||||
<h2>{{ displayName }}</h2>
|
||||
<p>Count: {{ count }}</p>
|
||||
<p>Double: {{ doubleCount }}</p>
|
||||
<button @click="increment">+</button>
|
||||
<button @click="decrement">-</button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.counter {
|
||||
padding: 1rem;
|
||||
}
|
||||
</style>
|
||||
```
|
||||
|
||||
### Composables for Reusable Logic
|
||||
|
||||
```typescript
|
||||
// composables/useAsync.ts
|
||||
import { ref, Ref, unref, watchEffect } from 'vue'
|
||||
|
||||
interface UseAsyncOptions<T> {
|
||||
immediate?: boolean
|
||||
onSuccess?: (data: T) => void
|
||||
onError?: (error: Error) => void
|
||||
}
|
||||
|
||||
export function useAsync<T>(
|
||||
asyncFn: () => Promise<T>,
|
||||
options: UseAsyncOptions<T> = {}
|
||||
) {
|
||||
const { immediate = true, onSuccess, onError } = options
|
||||
|
||||
const data = ref<T | null>(null) as Ref<T | null>
|
||||
const error = ref<Error | null>(null)
|
||||
const loading = ref(false)
|
||||
|
||||
async function execute() {
|
||||
loading.value = true
|
||||
error.value = null
|
||||
|
||||
try {
|
||||
const result = await asyncFn()
|
||||
data.value = result
|
||||
onSuccess?.(result)
|
||||
} catch (e) {
|
||||
error.value = e as Error
|
||||
onError?.(e as Error)
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
if (immediate) {
|
||||
execute()
|
||||
}
|
||||
|
||||
return {
|
||||
data,
|
||||
error,
|
||||
loading,
|
||||
execute
|
||||
}
|
||||
}
|
||||
|
||||
// composables/useFetch.ts
|
||||
import { ref, Ref, MaybeRef, unref, watchEffect } from 'vue'
|
||||
|
||||
interface UseFetchOptions {
|
||||
immediate?: boolean
|
||||
refetch?: MaybeRef<boolean>
|
||||
}
|
||||
|
||||
export function useFetch<T>(
|
||||
url: MaybeRef<string>,
|
||||
options: UseFetchOptions = {}
|
||||
) {
|
||||
const { immediate = true } = options
|
||||
|
||||
const data = ref<T | null>(null) as Ref<T | null>
|
||||
const error = ref<Error | null>(null)
|
||||
const loading = ref(false)
|
||||
|
||||
async function execute() {
|
||||
loading.value = true
|
||||
error.value = null
|
||||
|
||||
try {
|
||||
const response = await fetch(unref(url))
|
||||
if (!response.ok) {
|
||||
throw new Error(`HTTP error! status: ${response.status}`)
|
||||
}
|
||||
data.value = await response.json()
|
||||
} catch (e) {
|
||||
error.value = e as Error
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
if (immediate) {
|
||||
// Automatically refetch when URL changes
|
||||
watchEffect(() => {
|
||||
execute()
|
||||
})
|
||||
}
|
||||
|
||||
return {
|
||||
data,
|
||||
error,
|
||||
loading,
|
||||
execute,
|
||||
refetch: execute
|
||||
}
|
||||
}
|
||||
|
||||
// composables/useDebounce.ts
|
||||
import { ref, watch, Ref } from 'vue'
|
||||
|
||||
export function useDebounce<T>(value: Ref<T>, delay: number = 500): Ref<T> {
|
||||
const debouncedValue = ref(value.value) as Ref<T>
|
||||
|
||||
watch(value, (newVal) => {
|
||||
const timeout = setTimeout(() => {
|
||||
debouncedValue.value = newVal
|
||||
}, delay)
|
||||
|
||||
return () => clearTimeout(timeout)
|
||||
})
|
||||
|
||||
return debouncedValue
|
||||
}
|
||||
|
||||
// composables/useLocalStorage.ts
|
||||
import { ref, watch, Ref } from 'vue'
|
||||
|
||||
export function useLocalStorage<T>(
|
||||
key: string,
|
||||
defaultValue: T
|
||||
): Ref<T> {
|
||||
const data = ref<T>(defaultValue)
|
||||
|
||||
// Load from localStorage
|
||||
const stored = localStorage.getItem(key)
|
||||
if (stored) {
|
||||
try {
|
||||
data.value = JSON.parse(stored)
|
||||
} catch (e) {
|
||||
console.error('Failed to parse localStorage item:', e)
|
||||
}
|
||||
}
|
||||
|
||||
// Watch for changes and save
|
||||
watch(
|
||||
data,
|
||||
(newVal) => {
|
||||
localStorage.setItem(key, JSON.stringify(newVal))
|
||||
},
|
||||
{ deep: true }
|
||||
)
|
||||
|
||||
return data as Ref<T>
|
||||
}
|
||||
|
||||
// composables/useEventListener.ts
|
||||
import { onMounted, onUnmounted } from 'vue'
|
||||
|
||||
export function useEventListener<K extends keyof WindowEventMap>(
|
||||
event: K,
|
||||
handler: (event: WindowEventMap[K]) => void,
|
||||
target: Window | HTMLElement = window
|
||||
) {
|
||||
onMounted(() => {
|
||||
target.addEventListener(event, handler as any)
|
||||
})
|
||||
|
||||
onUnmounted(() => {
|
||||
target.removeEventListener(event, handler as any)
|
||||
})
|
||||
}
|
||||
|
||||
// composables/useClickOutside.ts
|
||||
import { Ref, onMounted, onUnmounted } from 'vue'
|
||||
|
||||
export function useClickOutside(
|
||||
elementRef: Ref<HTMLElement | null>,
|
||||
callback: () => void
|
||||
) {
|
||||
const handleClick = (event: MouseEvent) => {
|
||||
if (elementRef.value && !elementRef.value.contains(event.target as Node)) {
|
||||
callback()
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
document.addEventListener('click', handleClick)
|
||||
})
|
||||
|
||||
onUnmounted(() => {
|
||||
document.removeEventListener('click', handleClick)
|
||||
})
|
||||
}
|
||||
```
|
||||
|
||||
### Using Composables in Components
|
||||
|
||||
```vue
|
||||
<script setup lang="ts">
|
||||
import { ref } from 'vue'
|
||||
import { useFetch } from '@/composables/useFetch'
|
||||
import { useDebounce } from '@/composables/useDebounce'
|
||||
import { useLocalStorage } from '@/composables/useLocalStorage'
|
||||
|
||||
// Fetch data with composable
|
||||
const userId = ref('123')
|
||||
const { data: user, loading, error, refetch } = useFetch<User>(
|
||||
computed(() => `/api/users/${userId.value}`)
|
||||
)
|
||||
|
||||
// Debounced search
|
||||
const searchQuery = ref('')
|
||||
const debouncedQuery = useDebounce(searchQuery, 300)
|
||||
|
||||
watch(debouncedQuery, (query) => {
|
||||
// Perform search with debounced value
|
||||
console.log('Searching for:', query)
|
||||
})
|
||||
|
||||
// Persistent state
|
||||
const theme = useLocalStorage<'light' | 'dark'>('theme', 'light')
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div>
|
||||
<input v-model="searchQuery" placeholder="Search..." />
|
||||
|
||||
<div v-if="loading">Loading...</div>
|
||||
<div v-else-if="error">Error: {{ error.message }}</div>
|
||||
<div v-else-if="user">
|
||||
<h2>{{ user.name }}</h2>
|
||||
<p>{{ user.email }}</p>
|
||||
<button @click="refetch">Refresh</button>
|
||||
</div>
|
||||
|
||||
<button @click="theme = theme === 'light' ? 'dark' : 'light'">
|
||||
Toggle Theme ({{ theme }})
|
||||
</button>
|
||||
</div>
|
||||
</template>
|
||||
```
|
||||
|
||||
## State Management with Pinia
|
||||
|
||||
```typescript
|
||||
// stores/user.ts
|
||||
import { defineStore } from 'pinia'
|
||||
import { ref, computed } from 'vue'
|
||||
|
||||
interface User {
|
||||
id: string
|
||||
name: string
|
||||
email: string
|
||||
}
|
||||
|
||||
export const useUserStore = defineStore('user', () => {
|
||||
// State
|
||||
const currentUser = ref<User | null>(null)
|
||||
const users = ref<User[]>([])
|
||||
const loading = ref(false)
|
||||
const error = ref<Error | null>(null)
|
||||
|
||||
// Getters (computed)
|
||||
const isAuthenticated = computed(() => currentUser.value !== null)
|
||||
const userCount = computed(() => users.value.length)
|
||||
const getUserById = computed(() => {
|
||||
return (id: string) => users.value.find(u => u.id === id)
|
||||
})
|
||||
|
||||
// Actions
|
||||
async function fetchUsers() {
|
||||
loading.value = true
|
||||
error.value = null
|
||||
|
||||
try {
|
||||
const response = await fetch('/api/users')
|
||||
users.value = await response.json()
|
||||
} catch (e) {
|
||||
error.value = e as Error
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
async function login(email: string, password: string) {
|
||||
loading.value = true
|
||||
error.value = null
|
||||
|
||||
try {
|
||||
const response = await fetch('/api/auth/login', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ email, password })
|
||||
})
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error('Login failed')
|
||||
}
|
||||
|
||||
currentUser.value = await response.json()
|
||||
} catch (e) {
|
||||
error.value = e as Error
|
||||
throw e
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
function logout() {
|
||||
currentUser.value = null
|
||||
}
|
||||
|
||||
async function updateUser(userId: string, updates: Partial<User>) {
|
||||
const index = users.value.findIndex(u => u.id === userId)
|
||||
if (index !== -1) {
|
||||
users.value[index] = { ...users.value[index], ...updates }
|
||||
}
|
||||
|
||||
// Persist to backend
|
||||
await fetch(`/api/users/${userId}`, {
|
||||
method: 'PATCH',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify(updates)
|
||||
})
|
||||
}
|
||||
|
||||
return {
|
||||
// State
|
||||
currentUser,
|
||||
users,
|
||||
loading,
|
||||
error,
|
||||
// Getters
|
||||
isAuthenticated,
|
||||
userCount,
|
||||
getUserById,
|
||||
// Actions
|
||||
fetchUsers,
|
||||
login,
|
||||
logout,
|
||||
updateUser
|
||||
}
|
||||
})
|
||||
|
||||
// Using the store in components
|
||||
<script setup lang="ts">
|
||||
import { useUserStore } from '@/stores/user'
|
||||
import { storeToRefs } from 'pinia'
|
||||
|
||||
const userStore = useUserStore()
|
||||
|
||||
// Get reactive refs from store (maintains reactivity)
|
||||
const { currentUser, users, loading, isAuthenticated } = storeToRefs(userStore)
|
||||
|
||||
// Actions are used directly (don't need storeToRefs)
|
||||
const { login, logout, fetchUsers } = userStore
|
||||
|
||||
onMounted(() => {
|
||||
if (isAuthenticated.value) {
|
||||
fetchUsers()
|
||||
}
|
||||
})
|
||||
</script>
|
||||
```
|
||||
|
||||
## Advanced Patterns
|
||||
|
||||
### Generic Components
|
||||
|
||||
```vue
|
||||
<!-- components/DataTable.vue -->
|
||||
<script setup lang="ts" generic="T extends Record<string, any>">
|
||||
interface Column<T> {
|
||||
key: keyof T
|
||||
label: string
|
||||
render?: (value: T[keyof T], row: T) => string
|
||||
}
|
||||
|
||||
interface Props<T> {
|
||||
data: T[]
|
||||
columns: Column<T>[]
|
||||
onRowClick?: (row: T) => void
|
||||
}
|
||||
|
||||
const props = defineProps<Props<T>>()
|
||||
const emit = defineEmits<{
|
||||
(e: 'rowClick', row: T): void
|
||||
}>()
|
||||
|
||||
function handleRowClick(row: T) {
|
||||
emit('rowClick', row)
|
||||
props.onRowClick?.(row)
|
||||
}
|
||||
|
||||
function getCellValue(row: T, column: Column<T>) {
|
||||
const value = row[column.key]
|
||||
return column.render ? column.render(value, row) : String(value)
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th v-for="column in columns" :key="String(column.key)">
|
||||
{{ column.label }}
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr
|
||||
v-for="(row, index) in data"
|
||||
:key="index"
|
||||
@click="handleRowClick(row)"
|
||||
>
|
||||
<td v-for="column in columns" :key="String(column.key)">
|
||||
{{ getCellValue(row, column) }}
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</template>
|
||||
|
||||
<!-- Usage -->
|
||||
<script setup lang="ts">
|
||||
interface Product {
|
||||
id: number
|
||||
name: string
|
||||
price: number
|
||||
}
|
||||
|
||||
const products: Product[] = [
|
||||
{ id: 1, name: 'Product 1', price: 19.99 },
|
||||
{ id: 2, name: 'Product 2', price: 29.99 }
|
||||
]
|
||||
|
||||
const columns = [
|
||||
{ key: 'name' as const, label: 'Name' },
|
||||
{
|
||||
key: 'price' as const,
|
||||
label: 'Price',
|
||||
render: (value: number) => `$${value.toFixed(2)}`
|
||||
}
|
||||
]
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<DataTable :data="products" :columns="columns" />
|
||||
</template>
|
||||
```
|
||||
|
||||
### Provide/Inject Pattern
|
||||
|
||||
```vue
|
||||
<!-- Parent.vue -->
|
||||
<script setup lang="ts">
|
||||
import { provide, ref, readonly } from 'vue'
|
||||
|
||||
const theme = ref<'light' | 'dark'>('light')
|
||||
|
||||
// Provide with type safety
|
||||
const themeKey = Symbol() as InjectionKey<{
|
||||
theme: Readonly<Ref<'light' | 'dark'>>
|
||||
toggleTheme: () => void
|
||||
}>
|
||||
|
||||
provide(themeKey, {
|
||||
theme: readonly(theme),
|
||||
toggleTheme: () => {
|
||||
theme.value = theme.value === 'light' ? 'dark' : 'light'
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<!-- Child.vue -->
|
||||
<script setup lang="ts">
|
||||
import { inject } from 'vue'
|
||||
|
||||
const themeContext = inject(themeKey)
|
||||
if (!themeContext) {
|
||||
throw new Error('Theme context not provided')
|
||||
}
|
||||
|
||||
const { theme, toggleTheme } = themeContext
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div :class="`theme-${theme}`">
|
||||
<button @click="toggleTheme">Toggle Theme</button>
|
||||
</div>
|
||||
</template>
|
||||
```
|
||||
|
||||
### Teleport and Suspense
|
||||
|
||||
```vue
|
||||
<!-- Modal with Teleport -->
|
||||
<script setup lang="ts">
|
||||
const isOpen = ref(false)
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<button @click="isOpen = true">Open Modal</button>
|
||||
|
||||
<Teleport to="body">
|
||||
<div v-if="isOpen" class="modal-overlay" @click="isOpen = false">
|
||||
<div class="modal-content" @click.stop>
|
||||
<h2>Modal Title</h2>
|
||||
<p>Modal content here</p>
|
||||
<button @click="isOpen = false">Close</button>
|
||||
</div>
|
||||
</div>
|
||||
</Teleport>
|
||||
</template>
|
||||
|
||||
<!-- Async Components with Suspense -->
|
||||
<script setup lang="ts">
|
||||
import { defineAsyncComponent } from 'vue'
|
||||
|
||||
const AsyncComponent = defineAsyncComponent(() =>
|
||||
import('./HeavyComponent.vue')
|
||||
)
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Suspense>
|
||||
<template #default>
|
||||
<AsyncComponent />
|
||||
</template>
|
||||
<template #fallback>
|
||||
<div>Loading...</div>
|
||||
</template>
|
||||
</Suspense>
|
||||
</template>
|
||||
```
|
||||
|
||||
## Performance Optimization
|
||||
|
||||
### Computed vs Methods
|
||||
|
||||
```vue
|
||||
<script setup lang="ts">
|
||||
import { computed } from 'vue'
|
||||
|
||||
const items = ref([1, 2, 3, 4, 5])
|
||||
|
||||
// ✅ GOOD: Computed (cached)
|
||||
const filteredItems = computed(() => {
|
||||
console.log('Computing filtered items')
|
||||
return items.value.filter(x => x > 2)
|
||||
})
|
||||
|
||||
// ❌ BAD: Method (called on every render)
|
||||
function getFilteredItems() {
|
||||
console.log('Getting filtered items')
|
||||
return items.value.filter(x => x > 2)
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<!-- Computed value is cached -->
|
||||
<div v-for="item in filteredItems" :key="item">{{ item }}</div>
|
||||
|
||||
<!-- Method is called every time -->
|
||||
<div v-for="item in getFilteredItems()" :key="item">{{ item }}</div>
|
||||
</template>
|
||||
```
|
||||
|
||||
### v-once and v-memo
|
||||
|
||||
```vue
|
||||
<template>
|
||||
<!-- Render once and never update -->
|
||||
<div v-once>{{ staticContent }}</div>
|
||||
|
||||
<!-- Only re-render when dependencies change -->
|
||||
<div v-memo="[item.id, item.name]">
|
||||
{{ expensiveComputation(item) }}
|
||||
</div>
|
||||
</template>
|
||||
```
|
||||
|
||||
### Virtual Scrolling
|
||||
|
||||
```vue
|
||||
<script setup lang="ts">
|
||||
import { computed, ref } from 'vue'
|
||||
|
||||
const items = ref(Array.from({ length: 10000 }, (_, i) => ({ id: i, name: `Item ${i}` })))
|
||||
const scrollTop = ref(0)
|
||||
const itemHeight = 50
|
||||
const visibleCount = 20
|
||||
|
||||
const visibleItems = computed(() => {
|
||||
const start = Math.floor(scrollTop.value / itemHeight)
|
||||
const end = start + visibleCount
|
||||
return items.value.slice(start, end).map((item, index) => ({
|
||||
...item,
|
||||
offsetTop: (start + index) * itemHeight
|
||||
}))
|
||||
})
|
||||
|
||||
const totalHeight = computed(() => items.value.length * itemHeight)
|
||||
|
||||
function handleScroll(e: Event) {
|
||||
scrollTop.value = (e.target as HTMLElement).scrollTop
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="virtual-scroll" @scroll="handleScroll">
|
||||
<div :style="{ height: `${totalHeight}px`, position: 'relative' }">
|
||||
<div
|
||||
v-for="item in visibleItems"
|
||||
:key="item.id"
|
||||
:style="{ position: 'absolute', top: `${item.offsetTop}px`, height: `${itemHeight}px` }"
|
||||
>
|
||||
{{ item.name }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.virtual-scroll {
|
||||
height: 400px;
|
||||
overflow-y: auto;
|
||||
}
|
||||
</style>
|
||||
```
|
||||
|
||||
## Testing Best Practices
|
||||
|
||||
```typescript
|
||||
// Component.spec.ts
|
||||
import { mount } from '@vue/test-utils'
|
||||
import { describe, it, expect, vi } from 'vitest'
|
||||
import UserProfile from './UserProfile.vue'
|
||||
|
||||
describe('UserProfile', () => {
|
||||
it('renders user name', () => {
|
||||
const wrapper = mount(UserProfile, {
|
||||
props: {
|
||||
user: {
|
||||
id: '1',
|
||||
name: 'John Doe',
|
||||
email: 'john@example.com'
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
expect(wrapper.text()).toContain('John Doe')
|
||||
})
|
||||
|
||||
it('emits update event on button click', async () => {
|
||||
const wrapper = mount(UserProfile, {
|
||||
props: {
|
||||
user: { id: '1', name: 'John', email: 'john@example.com' }
|
||||
}
|
||||
})
|
||||
|
||||
await wrapper.find('button').trigger('click')
|
||||
|
||||
expect(wrapper.emitted()).toHaveProperty('update')
|
||||
expect(wrapper.emitted('update')?.[0]).toEqual(['1'])
|
||||
})
|
||||
|
||||
it('calls onUpdate prop when provided', async () => {
|
||||
const onUpdate = vi.fn()
|
||||
const wrapper = mount(UserProfile, {
|
||||
props: {
|
||||
user: { id: '1', name: 'John', email: 'john@example.com' },
|
||||
onUpdate
|
||||
}
|
||||
})
|
||||
|
||||
await wrapper.find('button').trigger('click')
|
||||
|
||||
expect(onUpdate).toHaveBeenCalledWith('1')
|
||||
})
|
||||
})
|
||||
|
||||
// Composable testing
|
||||
import { useCounter } from './useCounter'
|
||||
|
||||
describe('useCounter', () => {
|
||||
it('increments count', () => {
|
||||
const { count, increment } = useCounter(0)
|
||||
|
||||
expect(count.value).toBe(0)
|
||||
increment()
|
||||
expect(count.value).toBe(1)
|
||||
})
|
||||
})
|
||||
```
|
||||
|
||||
## Best Practices Checklist
|
||||
|
||||
### Component Design
|
||||
- [ ] Use `<script setup>` syntax
|
||||
- [ ] Define proper TypeScript types for props and emits
|
||||
- [ ] Keep components small and focused
|
||||
- [ ] Extract reusable logic into composables
|
||||
- [ ] Use meaningful component and variable names
|
||||
- [ ] Implement proper prop validation
|
||||
|
||||
### Reactivity
|
||||
- [ ] Understand ref vs reactive
|
||||
- [ ] Use computed for derived state
|
||||
- [ ] Avoid losing reactivity (destructuring)
|
||||
- [ ] Watch dependencies correctly
|
||||
- [ ] Clean up watchers and effects
|
||||
|
||||
### Performance
|
||||
- [ ] Use computed instead of methods for expensive operations
|
||||
- [ ] Implement v-memo for expensive renders
|
||||
- [ ] Lazy load routes and components
|
||||
- [ ] Use virtual scrolling for large lists
|
||||
- [ ] Optimize with KeepAlive when appropriate
|
||||
- [ ] Avoid unnecessary reactivity
|
||||
|
||||
### State Management
|
||||
- [ ] Use Pinia for global state
|
||||
- [ ] Keep state as local as possible
|
||||
- [ ] Use storeToRefs to maintain reactivity
|
||||
- [ ] Organize stores by domain
|
||||
|
||||
### Code Quality
|
||||
- [ ] Write unit tests for components and composables
|
||||
- [ ] Use ESLint and Prettier
|
||||
- [ ] Follow Vue style guide
|
||||
- [ ] Document complex logic
|
||||
- [ ] Handle loading and error states
|
||||
|
||||
### Accessibility
|
||||
- [ ] Use semantic HTML
|
||||
- [ ] Provide proper ARIA attributes
|
||||
- [ ] Ensure keyboard navigation
|
||||
- [ ] Test with screen readers
|
||||
|
||||
## Common Mistakes to Avoid
|
||||
|
||||
1. **Destructuring reactive objects**: Use `toRefs()` or `storeToRefs()`
|
||||
2. **Using reactive for primitives**: Use `ref` instead
|
||||
3. **Not cleaning up side effects**: Always cleanup in `onUnmounted`
|
||||
4. **Mutating props**: Props are read-only
|
||||
5. **Overusing watchers**: Use computed when possible
|
||||
6. **Missing key in v-for**: Always provide unique keys
|
||||
7. **Inline event handlers**: Extract to methods for reusability
|
||||
|
||||
## Implementation Guidelines
|
||||
|
||||
When writing Vue code, I will:
|
||||
1. Use Composition API with `<script setup>`
|
||||
2. Define proper TypeScript types
|
||||
3. Extract reusable logic into composables
|
||||
4. Use Pinia for state management
|
||||
5. Optimize performance when needed
|
||||
6. Write accessible components
|
||||
7. Handle errors gracefully
|
||||
8. Write testable code
|
||||
9. Follow Vue style guide
|
||||
10. Document complex patterns
|
||||
|
||||
What Vue pattern or component would you like me to help with?
|
||||
Reference in New Issue
Block a user