Skip to content

Instantly share code, notes, and snippets.

Show Gist options
  • Select an option

  • Save suntong/5d6f29f47930bb6b06baad9c21097d8e to your computer and use it in GitHub Desktop.

Select an option

Save suntong/5d6f29f47930bb6b06baad9c21097d8e to your computer and use it in GitHub Desktop.

Generated by claude-opus-4-5-20251101-thinking-32k on 2025-12-02, much comprehensive than the other two or three AI models.

# Vue Comprehensive Reference for Backend Developers

Table of Contents


# 1. Mental Model Shift

# 1.1 Vue's Reactivity vs Backend State Management

Backend Mental Model (What You Know)

# Backend: Explicit state changes, request/response cycle
class OrderService:
    def __init__(self, db: Database):
        self.db = db
    
    def update_order(self, order_id: str, status: str) -> Order:
        order = self.db.get(order_id)
        order.status = status          # Mutation
        self.db.save(order)             # Explicit persistence
        self.notify_subscribers(order)  # Explicit notification
        return order                    # Explicit return to caller

Vue Mental Model (What You Need)

// Frontend: Declarative reactivity - changes automatically propagate
import { ref, computed, watch } from 'vue'

const orderStatus = ref<string>('pending')  // Reactive state
const isPending = computed(() => orderStatus.value === 'pending')  // Auto-updates

// When this changes, ALL dependent UI automatically re-renders
orderStatus.value = 'shipped'  // No manual notification needed

Key Paradigm Differences

Backend Concept Vue Equivalent Key Difference
Database state ref() / reactive() Lives in memory, triggers UI updates
Service methods Composables / Component methods No explicit "save" - mutations propagate
Event bus / Message queue watch() / emit() Synchronous by default, same thread
Request/Response Reactive data flow No request needed - UI observes state
Repository pattern Pinia stores Centralized, but reactive
DTOs TypeScript interfaces Same concept, stricter at compile time

The Reactivity System Under the Hood

// Vue wraps your data with Proxies (similar to Python descriptors or Java dynamic proxies)
import { ref, effect } from 'vue'

const count = ref(0)

// This is essentially what Vue's template compiler does:
effect(() => {
  // This function re-runs whenever `count.value` changes
  console.log(`Count is: ${count.value}`)
})

count.value++  // Console: "Count is: 1" (automatic!)
count.value++  // Console: "Count is: 2" (automatic!)

Backend Analogy: Think of it like database triggers + materialized views. When base data changes, derived views automatically update.


# 1.2 Component Thinking and Service Architecture

Backend: Layered Architecture

┌─────────────────────────────────────────┐
│           Controllers/Handlers          │  ← Entry points
├─────────────────────────────────────────┤
│              Services                   │  ← Business logic
├─────────────────────────────────────────┤
│            Repositories                 │  ← Data access
├─────────────────────────────────────────┤
│              Database                   │  ← Persistence
└─────────────────────────────────────────┘

Vue: Component Tree Architecture

┌─────────────────────────────────────────┐
│              App.vue                    │  ← Root component
├─────────────────────────────────────────┤
│   ┌─────────────┐  ┌─────────────────┐  │
│   │ NavBar.vue  │  │ MainContent.vue │  │  ← Layout components
│   └─────────────┘  └────────┬────────┘  │
│                             │           │
│            ┌────────────────┼───────┐   │
│            ▼                ▼       ▼   │
│      ┌─────────┐    ┌───────────┐ ...   │  ← Feature components
│      │OrderList│    │OrderDetail│       │
│      └────┬────┘    └───────────┘       │
│           ▼                             │
│      ┌─────────┐                        │  ← UI components
│      │OrderItem│                        │
│      └─────────┘                        │
├─────────────────────────────────────────┤
│     Composables (useOrders, useAuth)    │  ← Shared logic (like services)
├─────────────────────────────────────────┤
│     Pinia Stores (orderStore, etc)      │  ← Global state (like repositories)
└─────────────────────────────────────────┘

Component Responsibilities Mapped

Backend Layer Vue Equivalent Responsibility
Controller Page/View Component Route handling, orchestration
Service Composable (use*.ts) Reusable business logic
Repository Pinia Store State management, data caching
DTO/Entity TypeScript Interface Type definitions
Middleware Navigation Guards / Interceptors Cross-cutting concerns

Single File Component (SFC) Structure

<!-- OrderCard.vue -->
<script setup lang="ts">
// This is your "controller" logic - runs once on component mount
import { computed } from 'vue'
import type { Order } from '@/types'

// Props = Input parameters (like controller method params)
const props = defineProps<{
  order: Order
}>()

// Emits = Output events (like return values or callbacks)
const emit = defineEmits<{
  cancel: [orderId: string]
  update: [order: Order]
}>()

// Computed = Derived data (like calculated fields)
const formattedTotal = computed(() => 
  new Intl.NumberFormat('en-US', { style: 'currency', currency: 'USD' })
    .format(props.order.total)
)

// Methods = Actions
const handleCancel = () => {
  emit('cancel', props.order.id)
}
</script>

<template>
  <!-- This is your "view" layer - declarative HTML with bindings -->
  <div class="order-card">
    <h3>Order #{{ order.id }}</h3>
    <p>Total: {{ formattedTotal }}</p>
    <span :class="['status', order.status]">{{ order.status }}</span>
    <button @click="handleCancel">Cancel Order</button>
  </div>
</template>

<style scoped>
/* Scoped CSS - only affects this component (like CSS modules) */
.order-card {
  padding: 1rem;
  border: 1px solid #e0e0e0;
}
.status.pending { color: orange; }
.status.shipped { color: green; }
</style>

# 1.3 Template Syntax as Compiled Render Functions

Templates Are NOT Just HTML

Vue templates compile to optimized JavaScript render functions at build time.

<!-- What you write -->
<template>
  <div>
    <span>{{ message }}</span>
    <button @click="increment">+1</button>
  </div>
</template>
// What Vue compiles it to (simplified)
import { openBlock, createElementBlock, createElementVNode, toDisplayString } from 'vue'

function render(_ctx) {
  return (openBlock(), createElementBlock("div", null, [
    createElementVNode("span", null, toDisplayString(_ctx.message), 1 /* TEXT */),
    createElementVNode("button", { onClick: _ctx.increment }, "+1")
  ]))
}

Why This Matters for Backend Developers

  1. It's not string interpolation - No XSS vulnerabilities from {{ userInput }}
  2. Compile-time optimization - Vue analyzes your template and generates optimized code
  3. Static hoisting - Static content is hoisted out of render function
  4. Patch flags - Vue knows exactly what can change (see the 1 /* TEXT */ flag)

Template vs JSX vs Render Functions

// Option 1: Template (recommended for most cases)
// In .vue file's <template> block

// Option 2: JSX (when you need more programmatic control)
const OrderList = defineComponent({
  setup(props) {
    return () => (
      <div>
        {props.orders.map(order => (
          <OrderCard key={order.id} order={order} />
        ))}
      </div>
    )
  }
})

// Option 3: Render function (rare, maximum control)
import { h } from 'vue'

const OrderList = defineComponent({
  setup(props) {
    return () => h('div', {}, 
      props.orders.map(order => 
        h(OrderCard, { key: order.id, order })
      )
    )
  }
})

Backend Analogy: Think of templates like SQL views or LINQ expressions - you write declarative syntax, the engine optimizes execution.


# 2. Syntax & Core Concepts

# 2.1 Template Syntax & Dynamic Content

Text Interpolation

<template>
  <!-- Basic interpolation (auto-escaped, XSS-safe) -->
  <p>{{ message }}</p>
  
  <!-- JavaScript expressions (not statements!) -->
  <p>{{ message.toUpperCase() }}</p>
  <p>{{ items.length > 0 ? 'Has items' : 'Empty' }}</p>
  <p>{{ formatDate(order.createdAt) }}</p>
  
  <!-- Raw HTML (use with caution - XSS risk if user-generated) -->
  <div v-html="trustedHtmlContent"></div>
</template>

Attribute Bindings

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

const imageUrl = ref('/images/product.jpg')
const isActive = ref(true)
const buttonType = ref<'submit' | 'button'>('submit')
const inputAttrs = ref({
  id: 'email',
  placeholder: 'Enter email',
  'data-testid': 'email-input'
})
</script>

<template>
  <!-- Dynamic attribute binding with : (shorthand for v-bind:) -->
  <img :src="imageUrl" :alt="product.name">
  
  <!-- Boolean attributes (renders or omits based on truthiness) -->
  <button :disabled="isLoading">Submit</button>
  
  <!-- Dynamic attribute name -->
  <button :[dynamicAttrName]="value">Click</button>
  
  <!-- Class bindings -->
  <div :class="{ active: isActive, 'text-danger': hasError }"></div>
  <div :class="[baseClass, isActive ? 'active' : '']"></div>
  <div :class="[{ active: isActive }, errorClass]"></div>
  
  <!-- Style bindings -->
  <div :style="{ color: textColor, fontSize: fontSize + 'px' }"></div>
  <div :style="[baseStyles, overrideStyles]"></div>
  
  <!-- Bind multiple attributes at once -->
  <input v-bind="inputAttrs">
  <!-- Equivalent to: <input :id="inputAttrs.id" :placeholder="inputAttrs.placeholder" ...> -->
</template>

Event Handling

<script setup lang="ts">
const count = ref(0)

// Inline handlers for simple operations
// Method handlers for complex logic
const handleClick = (event: MouseEvent) => {
  console.log('Clicked at:', event.clientX, event.clientY)
}

const handleSubmit = async (event: Event) => {
  event.preventDefault()
  // Process form...
}

const handleKeydown = (event: KeyboardEvent, customArg: string) => {
  console.log('Key:', event.key, 'Custom:', customArg)
}
</script>

<template>
  <!-- Basic event with @ (shorthand for v-on:) -->
  <button @click="count++">{{ count }}</button>
  
  <!-- Method handler (receives native event) -->
  <button @click="handleClick">Click Me</button>
  
  <!-- Passing arguments AND accessing event -->
  <input @keydown="handleKeydown($event, 'custom-value')">
  
  <!-- Event modifiers (replaces common event handling patterns) -->
  <form @submit.prevent="handleSubmit">  <!-- event.preventDefault() -->
    <button @click.stop="doSomething">   <!-- event.stopPropagation() -->
    <a @click.prevent>Disabled Link</a>  <!-- Prevent default only -->
  </form>
  
  <!-- Key modifiers -->
  <input @keyup.enter="submitForm">      <!-- Only on Enter key -->
  <input @keyup.ctrl.enter="submitForm"> <!-- Ctrl + Enter -->
  <input @keydown.esc="closeModal">
  
  <!-- Mouse modifiers -->
  <button @click.right="openContextMenu">Right Click</button>
  <button @click.middle="openInNewTab">Middle Click</button>
  
  <!-- Once modifier (like { once: true } in addEventListener) -->
  <button @click.once="trackFirstInteraction">Track Once</button>
  
  <!-- Passive modifier (for scroll performance) -->
  <div @scroll.passive="onScroll">Scrollable content</div>
</template>

# 2.2 Composition API with TypeScript

Basic Component Structure

<!-- UserProfile.vue -->
<script setup lang="ts">
import { ref, computed, onMounted } from 'vue'
import { useRoute } from 'vue-router'
import { useUserStore } from '@/stores/user'
import type { User, UserUpdatePayload } from '@/types'

// ============ TYPE DEFINITIONS ============
interface Props {
  userId: string
  showActions?: boolean  // Optional with default
}

// ============ PROPS ============
const props = withDefaults(defineProps<Props>(), {
  showActions: true
})

// ============ EMITS ============
const emit = defineEmits<{
  (e: 'update', payload: UserUpdatePayload): void
  (e: 'delete', userId: string): void
  // Vue 3.3+ alternative syntax:
  // update: [payload: UserUpdatePayload]
  // delete: [userId: string]
}>()

// ============ COMPOSABLES / DEPENDENCIES ============
const route = useRoute()
const userStore = useUserStore()

// ============ STATE ============
const isEditing = ref(false)
const formData = ref<Partial<User>>({})
const validationErrors = ref<Record<string, string>>({})

// ============ COMPUTED ============
const user = computed(() => userStore.getUserById(props.userId))
const fullName = computed(() => 
  user.value ? `${user.value.firstName} ${user.value.lastName}` : ''
)
const isValid = computed(() => Object.keys(validationErrors.value).length === 0)

// ============ METHODS ============
const startEditing = () => {
  formData.value = { ...user.value }
  isEditing.value = true
}

const saveChanges = async () => {
  if (!isValid.value) return
  
  emit('update', {
    userId: props.userId,
    changes: formData.value
  })
  isEditing.value = false
}

const handleDelete = () => {
  if (confirm('Are you sure?')) {
    emit('delete', props.userId)
  }
}

// ============ LIFECYCLE ============
onMounted(async () => {
  if (!user.value) {
    await userStore.fetchUser(props.userId)
  }
})
</script>

<template>
  <div class="user-profile">
    <template v-if="user">
      <h2>{{ fullName }}</h2>
      <p>{{ user.email }}</p>
      
      <div v-if="showActions" class="actions">
        <button @click="startEditing">Edit</button>
        <button @click="handleDelete" class="danger">Delete</button>
      </div>
    </template>
    <p v-else>Loading user...</p>
  </div>
</template>

Exposing Methods/State to Parent Components

<!-- ChildComponent.vue -->
<script setup lang="ts">
import { ref } from 'vue'

const internalCounter = ref(0)

const reset = () => {
  internalCounter.value = 0
}

const increment = () => {
  internalCounter.value++
}

// Explicitly expose to parent (like public methods in a class)
defineExpose({
  reset,
  increment,
  // Can also expose refs
  counter: internalCounter
})
</script>
<!-- ParentComponent.vue -->
<script setup lang="ts">
import { ref } from 'vue'
import ChildComponent from './ChildComponent.vue'

// Template ref to access child
const childRef = ref<InstanceType<typeof ChildComponent> | null>(null)

const resetChild = () => {
  childRef.value?.reset()
  console.log(childRef.value?.counter)  // Access exposed state
}
</script>

<template>
  <ChildComponent ref="childRef" />
  <button @click="resetChild">Reset Child</button>
</template>

# 2.3 ref() vs reactive() vs computed()

Quick Comparison

Feature ref() reactive() computed()
Use case Any value Objects/arrays only Derived values
Access .value in JS Direct access .value (readonly)
Reassign ✅ Yes ❌ No (lose reactivity) ❌ No (derived)
Template Auto-unwrapped Direct access Auto-unwrapped
Destructure ❌ Loses reactivity ❌ Loses reactivity N/A

ref() - Universal Reactive Primitive

import { ref, Ref } from 'vue'

// Primitives
const count = ref(0)                          // Ref<number>
const name = ref<string | null>(null)         // Explicit typing for unions
const isLoading = ref(false)                  // Ref<boolean>

// Objects (wraps in reactive internally)
const user = ref<User | null>(null)           // Ref<User | null>

// Access/modify with .value
count.value++
user.value = { id: '1', name: 'John' }

// Can be reassigned entirely
const items = ref<Item[]>([])
items.value = await fetchItems()  // ✅ Works!

// Type helper for refs
function useCounter(): { count: Ref<number>; increment: () => void } {
  const count = ref(0)
  const increment = () => count.value++
  return { count, increment }
}

reactive() - For Complex Objects

import { reactive, toRefs } from 'vue'

interface FormState {
  email: string
  password: string
  rememberMe: boolean
  errors: Record<string, string>
}

// Creates a reactive proxy (like Vue 2's data())
const form = reactive<FormState>({
  email: '',
  password: '',
  rememberMe: false,
  errors: {}
})

// Direct access (no .value needed)
form.email = '[email protected]'
form.errors.email = 'Invalid format'

// ⚠️ WARNING: Cannot reassign the whole object!
// form = { ...form, email: 'new@example.com' }  // ❌ Loses reactivity!

// Use Object.assign or individual properties
Object.assign(form, initialState)  // ✅ Maintains reactivity

// Converting to refs for composable returns
function useForm() {
  const form = reactive<FormState>({
    email: '',
    password: '',
    rememberMe: false,
    errors: {}
  })
  
  // toRefs converts each property to a ref
  return {
    ...toRefs(form),  // { email: Ref<string>, password: Ref<string>, ... }
    submit: () => { /* ... */ }
  }
}

// Usage maintains reactivity when destructured
const { email, password, submit } = useForm()
email.value = '[email protected]'  // ✅ Still reactive!

computed() - Derived State (Memoized)

import { ref, computed, ComputedRef } from 'vue'

const firstName = ref('John')
const lastName = ref('Doe')
const items = ref<Item[]>([])
const filter = ref<'all' | 'active' | 'completed'>('all')

// Read-only computed
const fullName: ComputedRef<string> = computed(() => 
  `${firstName.value} ${lastName.value}`
)

// Computed with complex logic (automatically cached)
const filteredItems = computed(() => {
  console.log('Filtering...')  // Only runs when dependencies change
  
  switch (filter.value) {
    case 'active':
      return items.value.filter(item => !item.completed)
    case 'completed':
      return items.value.filter(item => item.completed)
    default:
      return items.value
  }
})

// Computed with getter/setter (writable computed)
const fullNameWritable = computed({
  get: () => `${firstName.value} ${lastName.value}`,
  set: (newValue: string) => {
    const [first, ...rest] = newValue.split(' ')
    firstName.value = first
    lastName.value = rest.join(' ')
  }
})

fullNameWritable.value = 'Jane Smith'  // Sets firstName='Jane', lastName='Smith'

// Typed computed for generics
function useFiltered<T>(
  items: Ref<T[]>,
  predicate: (item: T) => boolean
): ComputedRef<T[]> {
  return computed(() => items.value.filter(predicate))
}

Backend Analogy Summary

Vue Concept Backend Equivalent
ref() Mutable variable with change tracking (Observable pattern)
reactive() Entity/Model object with property change detection
computed() Materialized view / Cached derived property

# 2.4 Component Lifecycle Hooks

Lifecycle Diagram

Component Instance
        │
        ▼
  ┌─────────────┐
  │   setup()   │ ← Composition API entry (no hook needed)
  └──────┬──────┘
         │
         ▼
  ┌─────────────────┐
  │ onBeforeMount() │ ← Before DOM insertion
  └────────┬────────┘
           │
           ▼
  ┌─────────────┐
  │ onMounted() │ ← DOM ready, refs accessible
  └──────┬──────┘
         │
         │ (reactive data changes)
         ▼
  ┌──────────────────┐
  │ onBeforeUpdate() │ ← Before re-render
  └────────┬─────────┘
           │
           ▼
  ┌─────────────┐
  │ onUpdated() │ ← After re-render
  └──────┬──────┘
         │
         │ (component unmounting)
         ▼
  ┌───────────────────┐
  │ onBeforeUnmount() │ ← Before teardown
  └─────────┬─────────┘
            │
            ▼
  ┌───────────────┐
  │ onUnmounted() │ ← Cleanup complete
  └───────────────┘

Practical Examples with Backend Comparisons

<script setup lang="ts">
import {
  onBeforeMount,
  onMounted,
  onBeforeUpdate,
  onUpdated,
  onBeforeUnmount,
  onUnmounted,
  onErrorCaptured,
  onActivated,
  onDeactivated,
  ref
} from 'vue'

const data = ref<DataType | null>(null)
let websocket: WebSocket | null = null
let intervalId: number | null = null

// ═══════════════════════════════════════════════════════════
// onMounted - Most commonly used
// Backend equivalent: @PostConstruct, __init__ after DI complete
// ═══════════════════════════════════════════════════════════
onMounted(async () => {
  // DOM is ready - can access template refs
  // Fetch initial data
  data.value = await fetchData()
  
  // Set up subscriptions
  websocket = new WebSocket('wss://api.example.com')
  websocket.onmessage = handleMessage
  
  // Start intervals
  intervalId = window.setInterval(pollForUpdates, 30000)
})

// ═══════════════════════════════════════════════════════════
// onUnmounted - Critical for cleanup
// Backend equivalent: @PreDestroy, __del__, IDisposable.Dispose()
// ═══════════════════════════════════════════════════════════
onUnmounted(() => {
  // Prevent memory leaks!
  websocket?.close()
  if (intervalId) clearInterval(intervalId)
})

// ═══════════════════════════════════════════════════════════
// onBeforeMount
// Backend equivalent: Late initialization before service starts
// ═══════════════════════════════════════════════════════════
onBeforeMount(() => {
  console.log('Component will mount - DOM not yet available')
})

// ═══════════════════════════════════════════════════════════
// onBeforeUpdate / onUpdated
// Backend equivalent: Database triggers, @PreUpdate/@PostUpdate
// ═══════════════════════════════════════════════════════════
onBeforeUpdate(() => {
  console.log('Data changed, about to re-render')
})

onUpdated(() => {
  console.log('DOM updated to reflect new data')
  // Careful: Don't mutate state here (infinite loop!)
})

// ═══════════════════════════════════════════════════════════
// onErrorCaptured - Error boundary
// Backend equivalent: Exception handler middleware, @ExceptionHandler
// ═══════════════════════════════════════════════════════════
onErrorCaptured((error, instance, info) => {
  console.error('Error in child component:', error)
  console.log('Component:', instance)
  console.log('Info:', info)
  
  // Report to monitoring service
  errorReporter.capture(error, { componentInfo: info })
  
  // Return false to propagate, true to stop propagation
  return false
})

// ═══════════════════════════════════════════════════════════
// onActivated / onDeactivated - For <KeepAlive> cached components
// Backend equivalent: Connection pool acquire/release
// ═══════════════════════════════════════════════════════════
onActivated(() => {
  // Component reactivated from cache
  console.log('Component brought back from cache')
})

onDeactivated(() => {
  // Component cached instead of destroyed
  console.log('Component cached')
})
</script>

SSR-Specific Hooks

import { onServerPrefetch } from 'vue'

// Only runs during Server-Side Rendering
onServerPrefetch(async () => {
  // Fetch data needed for SSR
  // Equivalent to getServerSideProps in Next.js
  await store.fetchInitialData()
})

# 2.5 Template Directives

v-if / v-else-if / v-else (Conditional Rendering)

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

type Status = 'pending' | 'processing' | 'completed' | 'failed'

const user = ref<User | null>(null)
const isLoading = ref(true)
const status = ref<Status>('pending')
const items = ref<Item[]>([])
</script>

<template>
  <!-- Basic conditional -->
  <div v-if="isLoading">Loading...</div>
  <div v-else-if="user">Welcome, {{ user.name }}</div>
  <div v-else>Please log in</div>
  
  <!-- With template for multiple elements (no extra DOM wrapper) -->
  <template v-if="user">
    <h1>{{ user.name }}</h1>
    <p>{{ user.email }}</p>
    <UserActions :user="user" />
  </template>
  
  <!-- Complex conditions -->
  <div v-if="status === 'completed' && items.length > 0">
    Processing complete!
  </div>
  
  <!-- v-show: CSS-based toggle (keep in DOM, toggle display) -->
  <!-- Use for frequent toggles to avoid mount/unmount cost -->
  <div v-show="isDropdownOpen">
    Dropdown content (always in DOM, just hidden)
  </div>
</template>

v-if vs v-show Decision Guide:

  • v-if: Higher toggle cost, lower initial cost. Use when condition rarely changes.
  • v-show: Lower toggle cost, higher initial cost. Use for frequent toggles.

v-for (List Rendering)

<script setup lang="ts">
interface Item {
  id: string
  name: string
  category: string
}

const items = ref<Item[]>([
  { id: '1', name: 'Item 1', category: 'A' },
  { id: '2', name: 'Item 2', category: 'B' },
])

const objectData = ref({
  name: 'Product',
  price: 29.99,
  inStock: true
})
</script>

<template>
  <!-- Always use :key with a unique identifier (not index!) -->
  <ul>
    <li v-for="item in items" :key="item.id">
      {{ item.name }}
    </li>
  </ul>
  
  <!-- With index (when needed) -->
  <div v-for="(item, index) in items" :key="item.id">
    {{ index + 1 }}. {{ item.name }}
  </div>
  
  <!-- Iterating over object properties -->
  <div v-for="(value, key, index) in objectData" :key="key">
    {{ index }}. {{ key }}: {{ value }}
  </div>
  
  <!-- Range (1 to n) -->
  <span v-for="n in 5" :key="n">{{ n }}</span>
  <!-- Output: 1 2 3 4 5 -->
  
  <!-- With <template> for multiple elements -->
  <template v-for="item in items" :key="item.id">
    <dt>{{ item.name }}</dt>
    <dd>{{ item.category }}</dd>
  </template>
  
  <!-- ⚠️ v-if with v-for: Use computed or template wrapper -->
  <!-- BAD: v-for and v-if on same element -->
  
  <!-- GOOD: Filter in computed -->
  <li v-for="item in activeItems" :key="item.id">
    {{ item.name }}
  </li>
  
  <!-- GOOD: Wrap v-if in template -->
  <template v-for="item in items" :key="item.id">
    <li v-if="item.isActive">{{ item.name }}</li>
  </template>
</template>

<script setup lang="ts">
const activeItems = computed(() => 
  items.value.filter(item => item.isActive)
)
</script>

v-model (Two-Way Binding)

<script setup lang="ts">
const text = ref('')
const selected = ref<string[]>([])
const toggle = ref(false)
const rating = ref(0)
</script>

<template>
  <!-- Basic text input -->
  <input v-model="text" type="text">
  
  <!-- Modifiers -->
  <input v-model.lazy="text">      <!-- Sync on 'change' instead of 'input' -->
  <input v-model.number="rating">  <!-- Auto-convert to number -->
  <input v-model.trim="text">      <!-- Trim whitespace -->
  <input v-model.lazy.trim="text"> <!-- Can combine modifiers -->
  
  <!-- Textarea -->
  <textarea v-model="text"></textarea>
  
  <!-- Checkbox (boolean) -->
  <input v-model="toggle" type="checkbox">
  
  <!-- Checkbox (array of values) -->
  <input v-model="selected" type="checkbox" value="option1">
  <input v-model="selected" type="checkbox" value="option2">
  
  <!-- Radio -->
  <input v-model="selected" type="radio" value="option1">
  <input v-model="selected" type="radio" value="option2">
  
  <!-- Select -->
  <select v-model="selected">
    <option value="">Choose...</option>
    <option value="a">Option A</option>
    <option value="b">Option B</option>
  </select>
  
  <!-- Select multiple -->
  <select v-model="selected" multiple>
    <option value="a">A</option>
    <option value="b">B</option>
  </select>
</template>

v-model on Components (Custom Two-Way Binding)

<!-- CustomInput.vue -->
<script setup lang="ts">
const model = defineModel<string>()  // Vue 3.4+

// Or with options
const model = defineModel<string>({ required: true })
</script>

<template>
  <input :value="model" @input="model = ($event.target as HTMLInputElement).value">
</template>
<!-- Parent.vue -->
<template>
  <!-- These are equivalent -->
  <CustomInput v-model="searchQuery" />
  <CustomInput :modelValue="searchQuery" @update:modelValue="val => searchQuery = val" />
</template>

Multiple v-model Bindings

<!-- UserForm.vue -->
<script setup lang="ts">
const firstName = defineModel<string>('firstName')
const lastName = defineModel<string>('lastName')
const email = defineModel<string>('email')
</script>

<template>
  <input v-model="firstName" placeholder="First name">
  <input v-model="lastName" placeholder="Last name">
  <input v-model="email" type="email" placeholder="Email">
</template>
<!-- Parent.vue -->
<template>
  <UserForm 
    v-model:firstName="user.firstName"
    v-model:lastName="user.lastName"
    v-model:email="user.email"
  />
</template>

# 2.6 Slots & Scoped Slots

Basic Slots (Like Template Partials)

<!-- Card.vue - The slot provider -->
<template>
  <div class="card">
    <div class="card-header">
      <slot name="header">Default Header</slot>
    </div>
    <div class="card-body">
      <slot>Default content (unnamed/default slot)</slot>
    </div>
    <div class="card-footer">
      <slot name="footer"></slot>
    </div>
  </div>
</template>
<!-- Parent.vue - Using the slots -->
<template>
  <Card>
    <!-- Named slot with #shorthand (v-slot:header) -->
    <template #header>
      <h2>My Custom Header</h2>
    </template>
    
    <!-- Default slot content (no template needed for simple cases) -->
    <p>This goes in the default slot</p>
    <p>Multiple elements are fine</p>
    
    <!-- Named slot with v-slot: syntax -->
    <template v-slot:footer>
      <button>Save</button>
      <button>Cancel</button>
    </template>
  </Card>
</template>

Scoped Slots (Passing Data Back to Parent)

<!-- DataTable.vue - Pass data to parent via slot props -->
<script setup lang="ts">
interface Column<T> {
  key: keyof T
  label: string
}

interface Props<T> {
  items: T[]
  columns: Column<T>[]
}

const props = defineProps<Props<any>>()
</script>

<template>
  <table>
    <thead>
      <tr>
        <th v-for="col in columns" :key="String(col.key)">
          <!-- Header slot with column data -->
          <slot name="header" :column="col">
            {{ col.label }}
          </slot>
        </th>
      </tr>
    </thead>
    <tbody>
      <tr v-for="(item, index) in items" :key="index">
        <td v-for="col in columns" :key="String(col.key)">
          <!-- Cell slot with full context -->
          <slot 
            name="cell" 
            :item="item" 
            :column="col" 
            :value="item[col.key]"
            :index="index"
          >
            {{ item[col.key] }}
          </slot>
        </td>
      </tr>
    </tbody>
  </table>
</template>
<!-- Parent.vue - Receiving scoped slot data -->
<script setup lang="ts">
interface User {
  id: string
  name: string
  email: string
  status: 'active' | 'inactive'
}

const users = ref<User[]>([/* ... */])
const columns = [
  { key: 'name', label: 'Name' },
  { key: 'email', label: 'Email' },
  { key: 'status', label: 'Status' },
]
</script>

<template>
  <DataTable :items="users" :columns="columns">
    <!-- Destructure slot props -->
    <template #cell="{ item, column, value }">
      <!-- Custom rendering based on column -->
      <template v-if="column.key === 'status'">
        <StatusBadge :status="value" />
      </template>
      <template v-else-if="column.key === 'name'">
        <RouterLink :to="`/users/${item.id}`">
          {{ value }}
        </RouterLink>
      </template>
      <template v-else>
        {{ value }}
      </template>
    </template>
  </DataTable>
</template>

Render Functions Slot Pattern

<!-- RenderlessComponent.vue - Logic only, parent controls all rendering -->
<script setup lang="ts">
import { ref, computed } from 'vue'

const isOpen = ref(false)
const toggle = () => isOpen.value = !isOpen.value
const open = () => isOpen.value = true
const close = () => isOpen.value = false

// Expose everything via default slot
defineSlots<{
  default(props: {
    isOpen: boolean
    toggle: () => void
    open: () => void
    close: () => void
  }): any
}>()
</script>

<template>
  <slot 
    :isOpen="isOpen" 
    :toggle="toggle" 
    :open="open" 
    :close="close" 
  />
</template>
<!-- Usage -->
<template>
  <Toggle v-slot="{ isOpen, toggle }">
    <button @click="toggle">
      {{ isOpen ? 'Close' : 'Open' }}
    </button>
    <div v-if="isOpen" class="dropdown">
      Dropdown content
    </div>
  </Toggle>
</template>

# 2.7 Composables

Composables are Vue's equivalent of backend services/utilities - reusable functions that encapsulate reactive state and logic.

Basic Composable Pattern

// composables/useCounter.ts
import { ref, computed, type Ref, type ComputedRef } from 'vue'

interface UseCounterOptions {
  initialValue?: number
  min?: number
  max?: number
  step?: number
}

interface UseCounterReturn {
  count: Ref<number>
  doubled: ComputedRef<number>
  increment: () => void
  decrement: () => void
  reset: () => void
  set: (value: number) => void
}

export function useCounter(options: UseCounterOptions = {}): UseCounterReturn {
  const { 
    initialValue = 0, 
    min = -Infinity, 
    max = Infinity,
    step = 1 
  } = options
  
  const count = ref(initialValue)
  
  const doubled = computed(() => count.value * 2)
  
  const increment = () => {
    if (count.value + step <= max) {
      count.value += step
    }
  }
  
  const decrement = () => {
    if (count.value - step >= min) {
      count.value -= step
    }
  }
  
  const reset = () => {
    count.value = initialValue
  }
  
  const set = (value: number) => {
    count.value = Math.max(min, Math.min(max, value))
  }
  
  return {
    count,
    doubled,
    increment,
    decrement,
    reset,
    set
  }
}

Data Fetching Composable

// composables/useFetch.ts
import { ref, shallowRef, computed, watch, type Ref } from 'vue'

interface UseFetchOptions<T> {
  immediate?: boolean
  initialData?: T
  onError?: (error: Error) => void
}

interface UseFetchReturn<T> {
  data: Ref<T | null>
  error: Ref<Error | null>
  isLoading: Ref<boolean>
  isSuccess: ComputedRef<boolean>
  isError: ComputedRef<boolean>
  execute: () => Promise<void>
  refresh: () => Promise<void>
}

export function useFetch<T>(
  url: string | Ref<string>,
  options: UseFetchOptions<T> = {}
): UseFetchReturn<T> {
  const { immediate = true, initialData = null, onError } = options
  
  const data = shallowRef<T | null>(initialData)
  const error = ref<Error | null>(null)
  const isLoading = ref(false)
  
  const isSuccess = computed(() => !isLoading.value && !error.value && data.value !== null)
  const isError = computed(() => error.value !== null)
  
  const execute = async () => {
    const targetUrl = typeof url === 'string' ? url : url.value
    
    isLoading.value = true
    error.value = null
    
    try {
      const response = await fetch(targetUrl)
      
      if (!response.ok) {
        throw new Error(`HTTP ${response.status}: ${response.statusText}`)
      }
      
      data.value = await response.json()
    } catch (e) {
      error.value = e instanceof Error ? e : new Error(String(e))
      onError?.(error.value)
    } finally {
      isLoading.value = false
    }
  }
  
  // Re-fetch when URL changes
  if (typeof url !== 'string') {
    watch(url, () => execute())
  }
  
  // Initial fetch
  if (immediate) {
    execute()
  }
  
  return {
    data,
    error,
    isLoading,
    isSuccess,
    isError,
    execute,
    refresh: execute
  }
}

Async Composable with Cleanup

// composables/useWebSocket.ts
import { ref, onUnmounted, type Ref } from 'vue'

interface UseWebSocketOptions {
  immediate?: boolean
  reconnect?: boolean
  reconnectInterval?: number
}

interface UseWebSocketReturn<T> {
  data: Ref<T | null>
  status: Ref<'CONNECTING' | 'OPEN' | 'CLOSING' | 'CLOSED'>
  send: (message: string | object) => void
  connect: () => void
  disconnect: () => void
}

export function useWebSocket<T = any>(
  url: string,
  options: UseWebSocketOptions = {}
): UseWebSocketReturn<T> {
  const { immediate = true, reconnect = true, reconnectInterval = 3000 } = options
  
  const data = ref<T | null>(null) as Ref<T | null>
  const status = ref<'CONNECTING' | 'OPEN' | 'CLOSING' | 'CLOSED'>('CLOSED')
  
  let ws: WebSocket | null = null
  let reconnectTimeout: ReturnType<typeof setTimeout> | null = null
  
  const connect = () => {
    if (ws?.readyState === WebSocket.OPEN) return
    
    status.value = 'CONNECTING'
    ws = new WebSocket(url)
    
    ws.onopen = () => {
      status.value = 'OPEN'
    }
    
    ws.onmessage = (event) => {
      try {
        data.value = JSON.parse(event.data)
      } catch {
        data.value = event.data as T
      }
    }
    
    ws.onclose = () => {
      status.value = 'CLOSED'
      ws = null
      
      if (reconnect) {
        reconnectTimeout = setTimeout(connect, reconnectInterval)
      }
    }
    
    ws.onerror = () => {
      ws?.close()
    }
  }
  
  const disconnect = () => {
    if (reconnectTimeout) {
      clearTimeout(reconnectTimeout)
      reconnectTimeout = null
    }
    ws?.close()
    ws = null
  }
  
  const send = (message: string | object) => {
    if (ws?.readyState === WebSocket.OPEN) {
      ws.send(typeof message === 'string' ? message : JSON.stringify(message))
    }
  }
  
  // Auto-cleanup on component unmount
  onUnmounted(() => {
    disconnect()
  })
  
  if (immediate) {
    connect()
  }
  
  return {
    data,
    status,
    send,
    connect,
    disconnect
  }
}

Using Composables in Components

<script setup lang="ts">
import { watch } from 'vue'
import { useFetch } from '@/composables/useFetch'
import { useWebSocket } from '@/composables/useWebSocket'
import { useCounter } from '@/composables/useCounter'

interface User {
  id: string
  name: string
}

// Multiple composables work together seamlessly
const { 
  data: users, 
  isLoading, 
  error, 
  refresh 
} = useFetch<User[]>('/api/users')

const { 
  data: realtimeUpdate, 
  status: wsStatus 
} = useWebSocket<{ type: string; payload: User }>('wss://api.example.com/ws')

const { count: page, increment: nextPage, decrement: prevPage } = useCounter({
  initialValue: 1,
  min: 1
})

// Composables can react to each other
watch(realtimeUpdate, (update) => {
  if (update?.type === 'USER_UPDATED') {
    refresh()  // Re-fetch when websocket notifies of changes
  }
})
</script>

Composable Best Practices

// ✅ DO: Start with "use" prefix
export function useAuth() { }

// ✅ DO: Accept refs OR values (use unref/toValue)
import { toValue, type MaybeRefOrGetter } from 'vue'

export function useFetch<T>(url: MaybeRefOrGetter<string>) {
  const actualUrl = toValue(url)  // Works with ref, getter, or raw value
}

// ✅ DO: Return object for destructuring
export function useCounter() {
  return { count, increment, decrement }  // Easy to pick what you need
}

// ✅ DO: Handle cleanup in the composable
export function useEventListener(target: EventTarget, event: string, handler: EventListener) {
  onMounted(() => target.addEventListener(event, handler))
  onUnmounted(() => target.removeEventListener(event, handler))  // Cleanup!
}

// ✅ DO: Use shallowRef for large objects
const items = shallowRef<LargeObject[]>([])  // Only tracks reference change

// ✅ DO: Make side effects optional
export function useFetch(url: string, { immediate = true } = {}) {
  if (immediate) execute()  // Caller can control when to fetch
}

# 3. Modern Patterns & Best Practices

# 3.1 Component Composition Patterns

The Prop Drilling Problem

<!-- ❌ BAD: Prop drilling through multiple levels -->
<!-- App.vue → Layout.vue → Sidebar.vue → UserMenu.vue → Avatar.vue -->

<!-- App.vue -->
<template>
  <Layout :user="user" :theme="theme" :notifications="notifications" />
</template>

<!-- Layout.vue - Just passing through! -->
<template>
  <Sidebar :user="user" :theme="theme" :notifications="notifications" />
</template>

<!-- This continues for every level... -->

Solution 1: Provide/Inject (Dependency Injection)

<!-- App.vue - Provide at the top -->
<script setup lang="ts">
import { provide, ref } from 'vue'
import type { User, Theme } from '@/types'

const user = ref<User | null>(null)
const theme = ref<Theme>('light')

// Provide with injection keys for type safety
provide('user', user)
provide('theme', theme)
</script>
<!-- Avatar.vue - Inject where needed (any depth) -->
<script setup lang="ts">
import { inject } from 'vue'
import type { Ref } from 'vue'
import type { User } from '@/types'

// Type-safe injection
const user = inject<Ref<User | null>>('user')

// With default value
const theme = inject('theme', ref('light'))
</script>

Solution 2: Composables for Shared State

// composables/useCurrentUser.ts
import { ref, computed, readonly } from 'vue'
import type { User } from '@/types'

// Module-level state (singleton pattern)
const user = ref<User | null>(null)
const isLoading = ref(false)

export function useCurrentUser() {
  const isAuthenticated = computed(() => user.value !== null)
  
  const login = async (credentials: { email: string; password: string }) => {
    isLoading.value = true
    try {
      const response = await authApi.login(credentials)
      user.value = response.user
    } finally {
      isLoading.value = false
    }
  }
  
  const logout = () => {
    user.value = null
  }
  
  return {
    user: readonly(user),  // Prevent external mutations
    isLoading: readonly(isLoading),
    isAuthenticated,
    login,
    logout
  }
}
<!-- Any component at any level -->
<script setup lang="ts">
import { useCurrentUser } from '@/composables/useCurrentUser'

const { user, isAuthenticated, logout } = useCurrentUser()
</script>

<template>
  <div v-if="isAuthenticated">
    Welcome, {{ user?.name }}
    <button @click="logout">Logout</button>
  </div>
</template>

Solution 3: Pinia Store (Recommended for Complex State)

// stores/user.ts
import { defineStore } from 'pinia'
import { ref, computed } from 'vue'
import type { User } from '@/types'

export const useUserStore = defineStore('user', () => {
  // State
  const user = ref<User | null>(null)
  const isLoading = ref(false)
  
  // Getters
  const isAuthenticated = computed(() => user.value !== null)
  const fullName = computed(() => 
    user.value ? `${user.value.firstName} ${user.value.lastName}` : ''
  )
  
  // Actions
  async function login(credentials: { email: string; password: string }) {
    isLoading.value = true
    try {
      const response = await authApi.login(credentials)
      user.value = response.user
      return { success: true }
    } catch (error) {
      return { success: false, error }
    } finally {
      isLoading.value = false
    }
  }
  
  function logout() {
    user.value = null
  }
  
  return {
    user,
    isLoading,
    isAuthenticated,
    fullName,
    login,
    logout
  }
})

Compound Component Pattern (Like Headless UI)

<!-- components/Tabs/TabsRoot.vue -->
<script setup lang="ts">
import { provide, ref, type InjectionKey, type Ref } from 'vue'

export interface TabsContext {
  activeTab: Ref<string>
  setActiveTab: (id: string) => void
}

export const TabsKey: InjectionKey<TabsContext> = Symbol('Tabs')

const props = withDefaults(defineProps<{
  defaultValue?: string
  modelValue?: string
}>(), {
  defaultValue: ''
})

const emit = defineEmits<{
  'update:modelValue': [value: string]
}>()

const internalValue = ref(props.defaultValue)

const activeTab = computed({
  get: () => props.modelValue ?? internalValue.value,
  set: (val) => {
    internalValue.value = val
    emit('update:modelValue', val)
  }
})

provide(TabsKey, {
  activeTab,
  setActiveTab: (id: string) => { activeTab.value = id }
})
</script>

<template>
  <div class="tabs">
    <slot />
  </div>
</template>
<!-- components/Tabs/TabsTrigger.vue -->
<script setup lang="ts">
import { inject, computed } from 'vue'
import { TabsKey, type TabsContext } from './TabsRoot.vue'

const props = defineProps<{
  value: string
}>()

const context = inject(TabsKey) as TabsContext

const isActive = computed(() => context.activeTab.value === props.value)
</script>

<template>
  <button 
    :class="['tab-trigger', { active: isActive }]"
    @click="context.setActiveTab(value)"
  >
    <slot />
  </button>
</template>
<!-- components/Tabs/TabsContent.vue -->
<script setup lang="ts">
import { inject, computed } from 'vue'
import { TabsKey, type TabsContext } from './TabsRoot.vue'

const props = defineProps<{
  value: string
}>()

const context = inject(TabsKey) as TabsContext
const isActive = computed(() => context.activeTab.value === props.value)
</script>

<template>
  <div v-if="isActive" class="tab-content">
    <slot />
  </div>
</template>
<!-- Usage - Clean, composable API -->
<template>
  <Tabs v-model="activeTab">
    <TabsList>
      <TabsTrigger value="account">Account</TabsTrigger>
      <TabsTrigger value="security">Security</TabsTrigger>
      <TabsTrigger value="billing">Billing</TabsTrigger>
    </TabsList>
    
    <TabsContent value="account">
      <AccountSettings />
    </TabsContent>
    <TabsContent value="security">
      <SecuritySettings />
    </TabsContent>
    <TabsContent value="billing">
      <BillingSettings />
    </TabsContent>
  </Tabs>
</template>

# 3.2 State Management: Local vs Pinia vs TanStack Query

Decision Matrix

State Type Solution Example
UI state (single component) ref() / reactive() Modal open/close, form inputs
UI state (shared) Composable or Pinia Theme, sidebar collapsed
Server cache TanStack Query API data, user lists
Global app state Pinia Current user, shopping cart
URL state Vue Router Filters, pagination
Form state VeeValidate / FormKit Complex forms

Local Component State

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

// Simple values
const isOpen = ref(false)
const searchQuery = ref('')

// Related state grouped together
const pagination = reactive({
  page: 1,
  pageSize: 20,
  total: 0
})

// Form state
const form = reactive({
  email: '',
  password: '',
  rememberMe: false
})
</script>

Pinia Store (Client State)

// stores/cart.ts
import { defineStore } from 'pinia'
import { ref, computed } from 'vue'

interface CartItem {
  productId: string
  name: string
  price: number
  quantity: number
}

export const useCartStore = defineStore('cart', () => {
  // State
  const items = ref<CartItem[]>([])
  const isOpen = ref(false)
  
  // Getters
  const totalItems = computed(() => 
    items.value.reduce((sum, item) => sum + item.quantity, 0)
  )
  
  const totalPrice = computed(() =>
    items.value.reduce((sum, item) => sum + item.price * item.quantity, 0)
  )
  
  const isEmpty = computed(() => items.value.length === 0)
  
  // Actions
  function addItem(product: Omit<CartItem, 'quantity'>) {
    const existing = items.value.find(i => i.productId === product.productId)
    
    if (existing) {
      existing.quantity++
    } else {
      items.value.push({ ...product, quantity: 1 })
    }
  }
  
  function removeItem(productId: string) {
    const index = items.value.findIndex(i => i.productId === productId)
    if (index > -1) {
      items.value.splice(index, 1)
    }
  }
  
  function updateQuantity(productId: string, quantity: number) {
    const item = items.value.find(i => i.productId === productId)
    if (item) {
      item.quantity = Math.max(0, quantity)
      if (item.quantity === 0) {
        removeItem(productId)
      }
    }
  }
  
  function clear() {
    items.value = []
  }
  
  // Persistence (optional)
  function $hydrate() {
    const saved = localStorage.getItem('cart')
    if (saved) {
      items.value = JSON.parse(saved)
    }
  }
  
  function $persist() {
    localStorage.setItem('cart', JSON.stringify(items.value))
  }
  
  return {
    items,
    isOpen,
    totalItems,
    totalPrice,
    isEmpty,
    addItem,
    removeItem,
    updateQuantity,
    clear,
    $hydrate,
    $persist
  }
}, {
  // Pinia plugin for automatic persistence
  persist: true  // Requires pinia-plugin-persistedstate
})

TanStack Query (Server State)

// composables/useProducts.ts
import { useQuery, useMutation, useQueryClient } from '@tanstack/vue-query'
import type { Product, CreateProductDTO } from '@/types'

// Query keys factory (prevents typos, enables type safety)
export const productKeys = {
  all: ['products'] as const,
  lists: () => [...productKeys.all, 'list'] as const,
  list: (filters: ProductFilters) => [...productKeys.lists(), filters] as const,
  details: () => [...productKeys.all, 'detail'] as const,
  detail: (id: string) => [...productKeys.details(), id] as const,
}

interface ProductFilters {
  category?: string
  search?: string
  page?: number
}

// Fetch products list
export function useProducts(filters: MaybeRef<ProductFilters> = {}) {
  return useQuery({
    queryKey: computed(() => productKeys.list(toValue(filters))),
    queryFn: async ({ queryKey }) => {
      const [, , filterParams] = queryKey
      const response = await api.get<{ data: Product[]; total: number }>('/products', {
        params: filterParams
      })
      return response.data
    },
    staleTime: 5 * 60 * 1000,  // Consider fresh for 5 minutes
  })
}

// Fetch single product
export function useProduct(id: MaybeRef<string>) {
  return useQuery({
    queryKey: computed(() => productKeys.detail(toValue(id))),
    queryFn: async () => {
      const response = await api.get<Product>(`/products/${toValue(id)}`)
      return response.data
    },
    enabled: computed(() => !!toValue(id)),  // Only fetch if id exists
  })
}

// Create product mutation
export function useCreateProduct() {
  const queryClient = useQueryClient()
  
  return useMutation({
    mutationFn: async (data: CreateProductDTO) => {
      const response = await api.post<Product>('/products', data)
      return response.data
    },
    onSuccess: (newProduct) => {
      // Invalidate list queries to refetch
      queryClient.invalidateQueries({ queryKey: productKeys.lists() })
      
      // Optionally, add to cache directly
      queryClient.setQueryData(
        productKeys.detail(newProduct.id),
        newProduct
      )
    }
  })
}

// Update product mutation
export function useUpdateProduct() {
  const queryClient = useQueryClient()
  
  return useMutation({
    mutationFn: async ({ id, data }: { id: string; data: Partial<Product> }) => {
      const response = await api.patch<Product>(`/products/${id}`, data)
      return response.data
    },
    onMutate: async ({ id, data }) => {
      // Cancel outgoing refetches
      await queryClient.cancelQueries({ queryKey: productKeys.detail(id) })
      
      // Snapshot previous value
      const previous = queryClient.getQueryData(productKeys.detail(id))
      
      // Optimistically update
      queryClient.setQueryData(productKeys.detail(id), (old: Product) => ({
        ...old,
        ...data
      }))
      
      return { previous }
    },
    onError: (err, { id }, context) => {
      // Rollback on error
      if (context?.previous) {
        queryClient.setQueryData(productKeys.detail(id), context.previous)
      }
    },
    onSettled: (data, error, { id }) => {
      // Refetch after mutation
      queryClient.invalidateQueries({ queryKey: productKeys.detail(id) })
    }
  })
}

// Delete mutation
export function useDeleteProduct() {
  const queryClient = useQueryClient()
  
  return useMutation({
    mutationFn: async (id: string) => {
      await api.delete(`/products/${id}`)
    },
    onSuccess: (_, id) => {
      // Remove from cache
      queryClient.removeQueries({ queryKey: productKeys.detail(id) })
      queryClient.invalidateQueries({ queryKey: productKeys.lists() })
    }
  })
}
<!-- ProductList.vue - Using TanStack Query -->
<script setup lang="ts">
import { ref, computed } from 'vue'
import { useProducts, useDeleteProduct } from '@/composables/useProducts'

const filters = ref({ category: '', search: '', page: 1 })

const { 
  data, 
  isLoading, 
  isError, 
  error,
  isFetching,  // True when refetching in background
  refetch 
} = useProducts(filters)

const products = computed(() => data.value?.data ?? [])
const total = computed(() => data.value?.total ?? 0)

const deleteMutation = useDeleteProduct()

const handleDelete = async (id: string) => {
  if (confirm('Delete this product?')) {
    await deleteMutation.mutateAsync(id)
  }
}
</script>

<template>
  <div>
    <!-- Filters -->
    <input v-model="filters.search" placeholder="Search...">
    <select v-model="filters.category">
      <option value="">All Categories</option>
      <option value="electronics">Electronics</option>
    </select>
    
    <!-- Loading states -->
    <div v-if="isLoading">Loading...</div>
    <div v-else-if="isError">Error: {{ error?.message }}</div>
    
    <template v-else>
      <!-- Background refetch indicator -->
      <span v-if="isFetching" class="refetching">Updating...</span>
      
      <ul>
        <li v-for="product in products" :key="product.id">
          {{ product.name }} - ${{ product.price }}
          <button 
            @click="handleDelete(product.id)"
            :disabled="deleteMutation.isPending.value"
          >
            Delete
          </button>
        </li>
      </ul>
      
      <Pagination 
        :total="total" 
        v-model:page="filters.page" 
      />
    </template>
  </div>
</template>

# 3.3 TypeScript Integration

Props with Generics

<!-- GenericList.vue -->
<script setup lang="ts" generic="T extends { id: string | number }">
import { computed } from 'vue'

// Generic props
const props = defineProps<{
  items: T[]
  selected?: T | null
  keyField?: keyof T
  labelField?: keyof T
}>()

const emit = defineEmits<{
  select: [item: T]
  delete: [item: T]
}>()

// Generic slots with proper typing
defineSlots<{
  default(props: { item: T; index: number }): any
  empty(): any
  header(): any
}>()

const getKey = (item: T) => item[props.keyField ?? 'id']
const getLabel = (item: T) => String(item[props.labelField ?? 'id'])

const isEmpty = computed(() => props.items.length === 0)
</script>

<template>
  <div class="generic-list">
    <div class="header">
      <slot name="header" />
    </div>
    
    <ul v-if="!isEmpty">
      <li 
        v-for="(item, index) in items" 
        :key="String(getKey(item))"
        :class="{ selected: selected?.id === item.id }"
        @click="emit('select', item)"
      >
        <slot :item="item" :index="index">
          {{ getLabel(item) }}
        </slot>
      </li>
    </ul>
    
    <div v-else class="empty">
      <slot name="empty">No items</slot>
    </div>
  </div>
</template>
<!-- Usage with full type inference -->
<script setup lang="ts">
interface User {
  id: string
  name: string
  email: string
  role: 'admin' | 'user'
}

const users = ref<User[]>([])
const selectedUser = ref<User | null>(null)

const handleSelect = (user: User) => {  // Type inferred!
  selectedUser.value = user
}
</script>

<template>
  <GenericList 
    :items="users"
    :selected="selectedUser"
    label-field="name"
    @select="handleSelect"
  >
    <template #default="{ item, index }">
      <!-- item is typed as User -->
      <span>{{ index + 1 }}. {{ item.name }} ({{ item.role }})</span>
    </template>
  </GenericList>
</template>

Complex Event Typing

<script setup lang="ts">
interface FormData {
  email: string
  password: string
}

interface ValidationResult {
  valid: boolean
  errors: Record<string, string[]>
}

// Complex emit signatures
const emit = defineEmits<{
  // Tuple syntax (Vue 3.3+)
  submit: [data: FormData, event: Event]
  validate: [result: ValidationResult]
  
  // Function syntax (for complex overloads)
  (e: 'update:modelValue', value: string): void
  (e: 'change', value: string, previousValue: string): void
}>()

const handleSubmit = (event: Event) => {
  event.preventDefault()
  emit('submit', { email: '', password: '' }, event)
}
</script>

Type-Safe Provide/Inject

// types/injection-keys.ts
import type { InjectionKey, Ref, ComputedRef } from 'vue'
import type { User, Theme } from './models'

// Define injection keys with full typing
export const UserKey: InjectionKey<Ref<User | null>> = Symbol('User')
export const ThemeKey: InjectionKey<Ref<Theme>> = Symbol('Theme')

export interface ToastContext {
  show: (message: string, type?: 'success' | 'error' | 'info') => void
  hide: (id: string) => void
}
export const ToastKey: InjectionKey<ToastContext> = Symbol('Toast')

// Type-safe provide/inject helpers
export function useToast(): ToastContext {
  const toast = inject(ToastKey)
  if (!toast) {
    throw new Error('useToast must be used within a ToastProvider')
  }
  return toast
}
<!-- Provider.vue -->
<script setup lang="ts">
import { provide, ref } from 'vue'
import { UserKey, ThemeKey, ToastKey } from '@/types/injection-keys'

const user = ref<User | null>(null)
const theme = ref<Theme>('light')

const toastContext: ToastContext = {
  show: (message, type = 'info') => { /* ... */ },
  hide: (id) => { /* ... */ }
}

// Type-checked provide
provide(UserKey, user)       // ✅ Type matches
provide(ThemeKey, theme)     // ✅ Type matches
provide(ToastKey, toastContext)

// provide(UserKey, 'string') // ❌ Type error!
</script>

Utility Types for Vue

// types/utils.ts
import type { Ref, ComputedRef, UnwrapRef } from 'vue'

// Props that can be either value or ref
type MaybeRef<T> = T | Ref<T>

// Extract props type from component
type ExtractProps<T> = T extends new () => { $props: infer P } ? P : never

// Make certain props required
type RequireProps<T, K extends keyof T> = Omit<T, K> & Required<Pick<T, K>>

// Props with defaults removed from required
type PropsWithDefaults<T, D extends Partial<T>> = Omit<T, keyof D> & Partial<Pick<T, keyof D>>

// Async component data type
type AsyncData<T> = {
  data: Ref<T | null>
  pending: Ref<boolean>
  error: Ref<Error | null>
  refresh: () => Promise<void>
}

// Form field type helper
interface FormField<T> {
  value: Ref<T>
  error: Ref<string | null>
  touched: Ref<boolean>
  validate: () => boolean
  reset: () => void
}

# 3.4 Performance Optimization

watch vs watchEffect

import { ref, watch, watchEffect, watchPostEffect, watchSyncEffect } from 'vue'

const searchQuery = ref('')
const userId = ref<string | null>(null)
const filters = ref({ category: '', sort: 'name' })

// ═══════════════════════════════════════════════════════════
// watchEffect: Auto-tracks dependencies, runs immediately
// Use for: Side effects that depend on reactive state
// ═══════════════════════════════════════════════════════════
watchEffect(() => {
  // Automatically tracks searchQuery
  console.log(`Searching for: ${searchQuery.value}`)
  
  // Cleanup function
  return () => {
    console.log('Cleanup previous effect')
  }
})

// With async (onCleanup pattern)
watchEffect(async (onCleanup) => {
  const controller = new AbortController()
  
  onCleanup(() => controller.abort())
  
  const results = await fetch(`/api/search?q=${searchQuery.value}`, {
    signal: controller.signal
  })
})

// ═══════════════════════════════════════════════════════════
// watch: Explicit dependencies, doesn't run immediately by default
// Use for: When you need old/new values, or conditional watching
// ═══════════════════════════════════════════════════════════

// Single source
watch(searchQuery, (newValue, oldValue) => {
  console.log(`Changed from "${oldValue}" to "${newValue}"`)
}, { immediate: true })  // Run on mount

// Multiple sources
watch(
  [searchQuery, filters],
  ([newQuery, newFilters], [oldQuery, oldFilters]) => {
    console.log('Query or filters changed')
  }
)

// Getter function (for nested properties)
watch(
  () => filters.value.category,
  (newCategory) => {
    console.log('Category changed:', newCategory)
  }
)

// Deep watching
watch(
  filters,
  (newFilters) => {
    console.log('Any filter property changed')
  },
  { deep: true }  // Watches nested changes
)

// Once (Vue 3.4+)
watch(
  userId,
  (id) => {
    if (id) analytics.identify(id)
  },
  { once: true }  // Only triggers once
)

// ═══════════════════════════════════════════════════════════
// Timing options
// ═══════════════════════════════════════════════════════════

// Default: pre (before DOM update)
watch(data, callback, { flush: 'pre' })

// After DOM update (when you need updated DOM)
watch(data, callback, { flush: 'post' })
// Or use watchPostEffect
watchPostEffect(() => {
  // DOM is updated here
})

// Synchronous (use sparingly - can cause perf issues)
watch(data, callback, { flush: 'sync' })
watchSyncEffect(() => { /* ... */ })

Computed vs Methods vs Watch

import { ref, computed, watch } from 'vue'

const items = ref<Item[]>([])
const filterTerm = ref('')

// ✅ COMPUTED: For derived/transformed data (cached)
const filteredItems = computed(() => {
  console.log('Computing filtered items...')  // Only logs when deps change
  return items.value.filter(item => 
    item.name.includes(filterTerm.value)
  )
})

// Template uses filteredItems multiple times? Only computed once!
// <div>{{ filteredItems.length }} items</div>
// <ul><li v-for="item in filteredItems">...</li></ul>

// ❌ METHOD: Would recalculate on every render
const getFilteredItems = () => {
  console.log('Method called...')  // Logs on EVERY render
  return items.value.filter(item => 
    item.name.includes(filterTerm.value)
  )
}

// ✅ WATCH: For side effects (API calls, localStorage, etc.)
watch(filteredItems, (newItems) => {
  // Don't use computed for side effects!
  analytics.track('filter_results', { count: newItems.length })
  localStorage.setItem('lastFilter', filterTerm.value)
})

Memoization Patterns

// composables/useMemoize.ts
export function useMemoize<T extends (...args: any[]) => any>(
  fn: T,
  keyResolver?: (...args: Parameters<T>) => string
): T {
  const cache = new Map<string, ReturnType<T>>()
  
  return ((...args: Parameters<T>) => {
    const key = keyResolver ? keyResolver(...args) : JSON.stringify(args)
    
    if (cache.has(key)) {
      return cache.get(key)!
    }
    
    const result = fn(...args)
    cache.set(key, result)
    return result
  }) as T
}

// Usage
const expensiveCalculation = useMemoize((data: number[]) => {
  return data.reduce((acc, val) => acc + Math.sqrt(val), 0)
})

Virtual Lists for Large Data

<!-- Using @tanstack/vue-virtual -->
<script setup lang="ts">
import { ref } from 'vue'
import { useVirtualizer } from '@tanstack/vue-virtual'

interface Item {
  id: string
  content: string
}

const props = defineProps<{
  items: Item[]
}>()

const parentRef = ref<HTMLElement | null>(null)

const virtualizer = useVirtualizer({
  count: props.items.length,
  getScrollElement: () => parentRef.value,
  estimateSize: () => 50,  // Estimated row height
  overscan: 5,  // Render 5 extra items outside viewport
})
</script>

<template>
  <div 
    ref="parentRef" 
    class="virtual-list"
    :style="{ height: '400px', overflow: 'auto' }"
  >
    <div
      :style="{
        height: `${virtualizer.getTotalSize()}px`,
        width: '100%',
        position: 'relative'
      }"
    >
      <div
        v-for="virtualRow in virtualizer.getVirtualItems()"
        :key="virtualRow.key"
        :style="{
          position: 'absolute',
          top: 0,
          left: 0,
          width: '100%',
          height: `${virtualRow.size}px`,
          transform: `translateY(${virtualRow.start}px)`
        }"
      >
        {{ items[virtualRow.index].content }}
      </div>
    </div>
  </div>
</template>

Component Lazy Loading

import { defineAsyncComponent } from 'vue'

// Basic async component
const AsyncModal = defineAsyncComponent(() => 
  import('./components/HeavyModal.vue')
)

// With loading/error states
const AsyncDashboard = defineAsyncComponent({
  loader: () => import('./views/Dashboard.vue'),
  loadingComponent: LoadingSpinner,
  errorComponent: ErrorDisplay,
  delay: 200,      // Show loading after 200ms
  timeout: 10000,  // Error after 10s
  suspensible: true,  // Works with <Suspense>
  onError(error, retry, fail, attempts) {
    if (attempts <= 3) {
      retry()
    } else {
      fail()
    }
  }
})

// Conditional loading
const showHeavyComponent = ref(false)
const HeavyComponent = computed(() => 
  showHeavyComponent.value 
    ? defineAsyncComponent(() => import('./HeavyComponent.vue'))
    : null
)

# 3.5 Error Boundaries and Logging

Error Boundary Component

<!-- components/ErrorBoundary.vue -->
<script setup lang="ts">
import { ref, onErrorCaptured } from 'vue'

interface Props {
  fallback?: string
  onError?: (error: Error, info: string) => void
}

const props = withDefaults(defineProps<Props>(), {
  fallback: 'Something went wrong'
})

const error = ref<Error | null>(null)
const errorInfo = ref<string>('')

onErrorCaptured((err, instance, info) => {
  error.value = err instanceof Error ? err : new Error(String(err))
  errorInfo.value = info
  
  // Call optional handler
  props.onError?.(error.value, info)
  
  // Report to monitoring service
  errorReporter.capture(error.value, {
    componentName: instance?.$options?.name,
    info
  })
  
  // Prevent propagation
  return false
})

const reset = () => {
  error.value = null
  errorInfo.value = ''
}
</script>

<template>
  <slot v-if="!error" />
  
  <div v-else class="error-boundary">
    <slot name="error" :error="error" :reset="reset">
      <div class="error-fallback">
        <p>{{ fallback }}</p>
        <pre v-if="isDev">{{ error.message }}</pre>
        <button @click="reset">Try Again</button>
      </div>
    </slot>
  </div>
</template>
<!-- Usage -->
<template>
  <ErrorBoundary @error="handleError">
    <RiskyComponent />
    
    <template #error="{ error, reset }">
      <div class="custom-error">
        <h3>Oops!</h3>
        <p>{{ error.message }}</p>
        <button @click="reset">Retry</button>
        <button @click="reportIssue(error)">Report Issue</button>
      </div>
    </template>
  </ErrorBoundary>
</template>

Global Error Handling

// main.ts
import { createApp } from 'vue'
import * as Sentry from '@sentry/vue'

const app = createApp(App)

// Global error handler (uncaught errors)
app.config.errorHandler = (err, instance, info) => {
  // Log to console in dev
  console.error('Vue Error:', err)
  console.log('Component:', instance)
  console.log('Info:', info)
  
  // Report to monitoring
  Sentry.captureException(err, {
    extra: {
      componentName: instance?.$options?.name,
      info
    }
  })
}

// Global warning handler (dev only)
app.config.warnHandler = (msg, instance, trace) => {
  console.warn('Vue Warning:', msg)
  console.log('Trace:', trace)
}

// Sentry integration
Sentry.init({
  app,
  dsn: import.meta.env.VITE_SENTRY_DSN,
  integrations: [
    Sentry.browserTracingIntegration({ router }),
    Sentry.replayIntegration()
  ],
  tracesSampleRate: 0.1,
  replaysSessionSampleRate: 0.1,
  replaysOnErrorSampleRate: 1.0
})

app.mount('#app')

Logging Service Composable

// composables/useLogger.ts
type LogLevel = 'debug' | 'info' | 'warn' | 'error'

interface LogEntry {
  level: LogLevel
  message: string
  context?: Record<string, any>
  timestamp: Date
  component?: string
}

class Logger {
  private static instance: Logger
  private logs: LogEntry[] = []
  private remoteEndpoint?: string
  
  constructor(config?: { remoteEndpoint?: string }) {
    this.remoteEndpoint = config?.remoteEndpoint
  }
  
  static getInstance(config?: { remoteEndpoint?: string }): Logger {
    if (!Logger.instance) {
      Logger.instance = new Logger(config)
    }
    return Logger.instance
  }
  
  private log(level: LogLevel, message: string, context?: Record<string, any>) {
    const entry: LogEntry = {
      level,
      message,
      context,
      timestamp: new Date()
    }
    
    this.logs.push(entry)
    
    // Console output in dev
    if (import.meta.env.DEV) {
      const method = level === 'error' ? 'error' : level === 'warn' ? 'warn' : 'log'
      console[method](`[${level.toUpperCase()}]`, message, context ?? '')
    }
    
    // Remote logging for errors
    if (level === 'error' && this.remoteEndpoint) {
      this.sendToRemote(entry)
    }
  }
  
  private async sendToRemote(entry: LogEntry) {
    try {
      await fetch(this.remoteEndpoint!, {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify(entry)
      })
    } catch {
      console.warn('Failed to send log to remote')
    }
  }
  
  debug = (msg: string, ctx?: Record<string, any>) => this.log('debug', msg, ctx)
  info = (msg: string, ctx?: Record<string, any>) => this.log('info', msg, ctx)
  warn = (msg: string, ctx?: Record<string, any>) => this.log('warn', msg, ctx)
  error = (msg: string, ctx?: Record<string, any>) => this.log('error', msg, ctx)
  
  getLogs = () => [...this.logs]
  clear = () => { this.logs = [] }
}

export function useLogger(componentName?: string) {
  const logger = Logger.getInstance({
    remoteEndpoint: import.meta.env.VITE_LOG_ENDPOINT
  })
  
  return {
    debug: (msg: string, ctx?: Record<string, any>) => 
      logger.debug(msg, { ...ctx, component: componentName }),
    info: (msg: string, ctx?: Record<string, any>) => 
      logger.info(msg, { ...ctx, component: componentName }),
    warn: (msg: string, ctx?: Record<string, any>) => 
      logger.warn(msg, { ...ctx, component: componentName }),
    error: (msg: string, ctx?: Record<string, any>) => 
      logger.error(msg, { ...ctx, component: componentName }),
  }
}

# 3.6 Dependency Injection (provide/inject)

Backend DI Comparison

// Backend (e.g., NestJS)
@Injectable()
class OrderService {
  constructor(
    private readonly db: DatabaseService,
    private readonly email: EmailService
  ) {}
}

// Vue equivalent
const OrderService = {
  setup() {
    const db = inject(DatabaseKey)!
    const email = inject(EmailKey)!
    
    return { /* ... */ }
  }
}

Type-Safe Injection System

// lib/injection.ts
import { inject, provide, type InjectionKey } from 'vue'

// Create typed injection helper
export function createInjection<T>(
  name: string,
  defaultValue?: T
) {
  const key: InjectionKey<T> = Symbol(name)
  
  const provideValue = (value: T) => {
    provide(key, value)
  }
  
  const injectValue = (fallback?: T): T => {
    const value = inject(key, fallback ?? defaultValue)
    if (value === undefined) {
      throw new Error(`Injection "${name}" not provided`)
    }
    return value
  }
  
  return {
    key,
    provide: provideValue,
    inject: injectValue
  }
}

// Usage
export const AuthService = createInjection<{
  user: Ref<User | null>
  login: (credentials: Credentials) => Promise<void>
  logout: () => void
}>('AuthService')

export const ThemeService = createInjection<{
  theme: Ref<'light' | 'dark'>
  toggle: () => void
}>('ThemeService')
<!-- providers/AppProvider.vue -->
<script setup lang="ts">
import { ref } from 'vue'
import { AuthService, ThemeService } from '@/lib/injection'

// Auth
const user = ref<User | null>(null)
const login = async (creds: Credentials) => { /* ... */ }
const logout = () => { user.value = null }

AuthService.provide({ user, login, logout })

// Theme
const theme = ref<'light' | 'dark'>('light')
const toggle = () => {
  theme.value = theme.value === 'light' ? 'dark' : 'light'
}

ThemeService.provide({ theme, toggle })
</script>

<template>
  <slot />
</template>
<!-- Any nested component -->
<script setup lang="ts">
import { AuthService, ThemeService } from '@/lib/injection'

// Type-safe injection
const { user, logout } = AuthService.inject()
const { theme, toggle } = ThemeService.inject()
</script>

<template>
  <div :class="theme">
    <span v-if="user">{{ user.name }}</span>
    <button @click="toggle">Toggle Theme</button>
    <button @click="logout">Logout</button>
  </div>
</template>

Plugin-Based Dependency Injection

// plugins/services.ts
import type { App, Plugin } from 'vue'

interface ServicesPluginOptions {
  apiBaseUrl: string
  analyticsKey?: string
}

export const servicesPlugin: Plugin<ServicesPluginOptions> = {
  install(app: App, options: ServicesPluginOptions) {
    // Create service instances
    const apiClient = new ApiClient(options.apiBaseUrl)
    const analytics = options.analyticsKey 
      ? new Analytics(options.analyticsKey)
      : null
    
    // Provide globally
    app.provide('apiClient', apiClient)
    app.provide('analytics', analytics)
    
    // Also available via globalProperties (Options API compatibility)
    app.config.globalProperties.$api = apiClient
  }
}

// main.ts
import { servicesPlugin } from './plugins/services'

app.use(servicesPlugin, {
  apiBaseUrl: import.meta.env.VITE_API_URL,
  analyticsKey: import.meta.env.VITE_ANALYTICS_KEY
})

# 4. Ecosystem Integration

# 4.1 Vue Router

Basic Setup with TypeScript

// router/index.ts
import { createRouter, createWebHistory, type RouteRecordRaw } from 'vue-router'

// Define route meta types
declare module 'vue-router' {
  interface RouteMeta {
    requiresAuth?: boolean
    roles?: string[]
    title?: string
    layout?: 'default' | 'auth' | 'blank'
  }
}

const routes: RouteRecordRaw[] = [
  {
    path: '/',
    name: 'home',
    component: () => import('@/views/Home.vue'),
    meta: { title: 'Home' }
  },
  {
    path: '/dashboard',
    name: 'dashboard',
    component: () => import('@/views/Dashboard.vue'),
    meta: { 
      requiresAuth: true,
      roles: ['admin', 'user'],
      title: 'Dashboard'
    }
  },
  {
    path: '/users/:id',
    name: 'user-detail',
    component: () => import('@/views/UserDetail.vue'),
    props: true,  // Pass :id as prop
    meta: { requiresAuth: true }
  },
  // Nested routes
  {
    path: '/settings',
    component: () => import('@/views/SettingsLayout.vue'),
    children: [
      {
        path: '',
        name: 'settings',
        redirect: 'profile'
      },
      {
        path: 'profile',
        name: 'settings-profile',
        component: () => import('@/views/settings/Profile.vue')
      },
      {
        path: 'security',
        name: 'settings-security',
        component: () => import('@/views/settings/Security.vue')
      }
    ]
  },
  // Catch-all 404
  {
    path: '/:pathMatch(.*)*',
    name: 'not-found',
    component: () => import('@/views/NotFound.vue')
  }
]

export const router = createRouter({
  history: createWebHistory(import.meta.env.BASE_URL),
  routes,
  scrollBehavior(to, from, savedPosition) {
    if (savedPosition) {
      return savedPosition
    }
    if (to.hash) {
      return { el: to.hash, behavior: 'smooth' }
    }
    return { top: 0 }
  }
})

Navigation Guards

// router/guards.ts
import type { Router } from 'vue-router'
import { useAuthStore } from '@/stores/auth'

export function setupGuards(router: Router) {
  // Global before guard (like middleware)
  router.beforeEach(async (to, from) => {
    const auth = useAuthStore()
    
    // Update document title
    document.title = to.meta.title 
      ? `${to.meta.title} | MyApp` 
      : 'MyApp'
    
    // Auth check
    if (to.meta.requiresAuth && !auth.isAuthenticated) {
      // Store intended destination
      return {
        name: 'login',
        query: { redirect: to.fullPath }
      }
    }
    
    // Role check
    if (to.meta.roles && !to.meta.roles.includes(auth.user?.role ?? '')) {
      return { name: 'forbidden' }
    }
    
    // Continue navigation
    return true
  })
  
  // After navigation
  router.afterEach((to, from, failure) => {
    if (failure) {
      console.error('Navigation failed:', failure)
      return
    }
    
    // Analytics tracking
    analytics.pageView(to.fullPath)
  })
  
  // Error handling
  router.onError((error) => {
    console.error('Router error:', error)
    
    // Handle chunk loading errors (lazy loading failed)
    if (error.message.includes('Failed to fetch dynamically imported module')) {
      window.location.reload()
    }
  })
}

// Per-route guards (in route definition)
const routes = [
  {
    path: '/admin',
    component: AdminLayout,
    beforeEnter: (to, from) => {
      // Route-specific guard
      const auth = useAuthStore()
      if (auth.user?.role !== 'admin') {
        return { name: 'forbidden' }
      }
    }
  }
]

Programmatic Navigation

<script setup lang="ts">
import { useRouter, useRoute } from 'vue-router'

const router = useRouter()
const route = useRoute()

// Current route info
console.log(route.params.id)      // URL params
console.log(route.query.search)   // Query string
console.log(route.meta.title)     // Meta data
console.log(route.name)           // Route name

// Navigation methods
const navigateToUser = async (userId: string) => {
  // By name (recommended - type-safe with typed routes)
  await router.push({ name: 'user-detail', params: { id: userId } })
  
  // By path
  await router.push(`/users/${userId}`)
  
  // With query params
  await router.push({ 
    name: 'users', 
    query: { page: 1, sort: 'name' } 
  })
  
  // Replace (no history entry)
  await router.replace({ name: 'home' })
  
  // Go back/forward
  router.back()
  router.forward()
  router.go(-2)  // Go back 2 steps
}

// Navigation with result handling
const safeNavigate = async () => {
  const result = await router.push({ name: 'dashboard' })
  
  if (result) {
    // Navigation was prevented or failed
    console.error('Navigation failed:', result)
  }
}
</script>

Route-Level Code Splitting

// Advanced lazy loading patterns
const routes = [
  {
    path: '/reports',
    component: () => import('@/views/Reports.vue'),
    // Webpack magic comments (Vite also supports some)
    // component: () => import(/* webpackChunkName: "reports" */ '@/views/Reports.vue'),
  },
  
  // Prefetch on hover/visibility
  {
    path: '/heavy-feature',
    component: defineAsyncComponent({
      loader: () => import('@/views/HeavyFeature.vue'),
      loadingComponent: LoadingSpinner,
      delay: 200,
      errorComponent: ErrorDisplay,
      timeout: 10000
    })
  }
]

// Prefetch utilities
export function usePrefetch() {
  const prefetchRoute = (name: string) => {
    const route = router.getRoutes().find(r => r.name === name)
    if (route?.components?.default && typeof route.components.default === 'function') {
      ;(route.components.default as () => Promise<any>)()
    }
  }
  
  return { prefetchRoute }
}

# 4.2 API Integration: TanStack Query vs Axios

Axios Setup with Interceptors

// lib/api.ts
import axios, { AxiosError, type AxiosInstance, type InternalAxiosRequestConfig } from 'axios'
import { useAuthStore } from '@/stores/auth'
import { router } from '@/router'

// Create instance
export const api: AxiosInstance = axios.create({
  baseURL: import.meta.env.VITE_API_URL,
  timeout: 30000,
  headers: {
    'Content-Type': 'application/json'
  }
})

// Request interceptor
api.interceptors.request.use(
  (config: InternalAxiosRequestConfig) => {
    const auth = useAuthStore()
    
    // Attach auth token
    if (auth.token) {
      config.headers.Authorization = `Bearer ${auth.token}`
    }
    
    // Add request ID for tracing
    config.headers['X-Request-ID'] = crypto.randomUUID()
    
    return config
  },
  (error) => {
    return Promise.reject(error)
  }
)

// Response interceptor
api.interceptors.response.use(
  (response) => {
    // Transform response if needed
    return response
  },
  async (error: AxiosError) => {
    const originalRequest = error.config
    
    // Handle 401 - Token expired
    if (error.response?.status === 401 && originalRequest) {
      const auth = useAuthStore()
      
      try {
        // Attempt token refresh
        await auth.refreshToken()
        
        // Retry original request
        return api(originalRequest)
      } catch (refreshError) {
        // Refresh failed - logout and redirect
        auth.logout()
        router.push({ name: 'login' })
        return Promise.reject(refreshError)
      }
    }
    
    // Handle other errors
    if (error.response?.status === 403) {
      router.push({ name: 'forbidden' })
    }
    
    if (error.response?.status === 500) {
      // Log to monitoring
      console.error('Server error:', error.response.data)
    }
    
    return Promise.reject(error)
  }
)

// Type-safe API methods
export interface ApiResponse<T> {
  data: T
  meta?: {
    total: number
    page: number
    pageSize: number
  }
}

export async function get<T>(url: string, params?: Record<string, any>): Promise<T> {
  const response = await api.get<T>(url, { params })
  return response.data
}

export async function post<T>(url: string, data?: unknown): Promise<T> {
  const response = await api.post<T>(url, data)
  return response.data
}

export async function put<T>(url: string, data?: unknown): Promise<T> {
  const response = await api.put<T>(url, data)
  return response.data
}

export async function del<T = void>(url: string): Promise<T> {
  const response = await api.delete<T>(url)
  return response.data
}

TanStack Query Configuration

// lib/query.ts
import { QueryClient, VueQueryPlugin } from '@tanstack/vue-query'
import type { App } from 'vue'

export const queryClient = new QueryClient({
  defaultOptions: {
    queries: {
      staleTime: 5 * 60 * 1000,     // 5 minutes
      gcTime: 30 * 60 * 1000,        // 30 minutes (formerly cacheTime)
      retry: (failureCount, error) => {
        // Don't retry on 4xx errors
        if (error instanceof Error && 'status' in error) {
          const status = (error as any).status
          if (status >= 400 && status < 500) return false
        }
        return failureCount < 3
      },
      refetchOnWindowFocus: import.meta.env.PROD,  // Only in production
      refetchOnReconnect: true,
    },
    mutations: {
      retry: 1,
      onError: (error) => {
        // Global mutation error handling
        console.error('Mutation error:', error)
      }
    }
  }
})

// Plugin setup
export function setupQuery(app: App) {
  app.use(VueQueryPlugin, {
    queryClient,
    enableDevtoolsV6Plugin: true
  })
}

Complete CRUD with TanStack Query

// composables/api/useUsers.ts
import { useQuery, useMutation, useQueryClient, useInfiniteQuery } from '@tanstack/vue-query'
import { api, type ApiResponse } from '@/lib/api'
import type { User, CreateUserDTO, UpdateUserDTO } from '@/types'

// Query key factory
export const userKeys = {
  all: ['users'] as const,
  lists: () => [...userKeys.all, 'list'] as const,
  list: (filters?: UserFilters) => [...userKeys.lists(), filters] as const,
  infinite: (filters?: UserFilters) => [...userKeys.lists(), 'infinite', filters] as const,
  details: () => [...userKeys.all, 'detail'] as const,
  detail: (id: string) => [...userKeys.details(), id] as const,
}

interface UserFilters {
  search?: string
  role?: string
  status?: 'active' | 'inactive'
  page?: number
  pageSize?: number
}

// List users
export function useUsers(filters?: MaybeRef<UserFilters>) {
  return useQuery({
    queryKey: computed(() => userKeys.list(toValue(filters))),
    queryFn: async () => {
      return api.get<ApiResponse<User[]>>('/users', { params: toValue(filters) })
    },
    placeholderData: (previousData) => previousData  // Keep previous data while fetching
  })
}

// Infinite scroll users
export function useInfiniteUsers(filters?: MaybeRef<Omit<UserFilters, 'page'>>) {
  return useInfiniteQuery({
    queryKey: computed(() => userKeys.infinite(toValue(filters))),
    queryFn: async ({ pageParam = 1 }) => {
      return api.get<ApiResponse<User[]>>('/users', { 
        params: { ...toValue(filters), page: pageParam } 
      })
    },
    initialPageParam: 1,
    getNextPageParam: (lastPage, allPages) => {
      const currentCount = allPages.flatMap(p => p.data).length
      if (currentCount < (lastPage.meta?.total ?? 0)) {
        return allPages.length + 1
      }
      return undefined
    }
  })
}

// Single user
export function useUser(id: MaybeRef<string>) {
  return useQuery({
    queryKey: computed(() => userKeys.detail(toValue(id))),
    queryFn: async () => {
      return api.get<User>(`/users/${toValue(id)}`)
    },
    enabled: computed(() => !!toValue(id))
  })
}

// Create user
export function useCreateUser() {
  const queryClient = useQueryClient()
  
  return useMutation({
    mutationFn: (data: CreateUserDTO) => 
      api.post<User>('/users', data),
    
    onSuccess: (newUser) => {
      // Add to cache
      queryClient.setQueryData(userKeys.detail(newUser.id), newUser)
      
      // Invalidate lists
      queryClient.invalidateQueries({ queryKey: userKeys.lists() })
    }
  })
}

// Update user
export function useUpdateUser() {
  const queryClient = useQueryClient()
  
  return useMutation({
    mutationFn: ({ id, data }: { id: string; data: UpdateUserDTO }) =>
      api.put<User>(`/users/${id}`, data),
    
    // Optimistic update
    onMutate: async ({ id, data }) => {
      await queryClient.cancelQueries({ queryKey: userKeys.detail(id) })
      
      const previous = queryClient.getQueryData<User>(userKeys.detail(id))
      
      queryClient.setQueryData<User>(userKeys.detail(id), (old) => 
        old ? { ...old, ...data } : undefined
      )
      
      return { previous, id }
    },
    
    onError: (error, variables, context) => {
      if (context?.previous) {
        queryClient.setQueryData(userKeys.detail(context.id), context.previous)
      }
    },
    
    onSettled: (data, error, { id }) => {
      queryClient.invalidateQueries({ queryKey: userKeys.detail(id) })
      queryClient.invalidateQueries({ queryKey: userKeys.lists() })
    }
  })
}

// Delete user
export function useDeleteUser() {
  const queryClient = useQueryClient()
  
  return useMutation({
    mutationFn: (id: string) => api.delete(`/users/${id}`),
    
    onSuccess: (_, id) => {
      queryClient.removeQueries({ queryKey: userKeys.detail(id) })
      queryClient.invalidateQueries({ queryKey: userKeys.lists() })
    }
  })
}

# 4.3 Form Handling

VeeValidate + Zod (Recommended Stack)

// schemas/user.ts
import { z } from 'zod'
import { toTypedSchema } from '@vee-validate/zod'

// Define schema with Zod
export const userSchema = z.object({
  email: z
    .string()
    .min(1, 'Email is required')
    .email('Invalid email format'),
  
  password: z
    .string()
    .min(8, 'Password must be at least 8 characters')
    .regex(/[A-Z]/, 'Password must contain an uppercase letter')
    .regex(/[0-9]/, 'Password must contain a number'),
  
  confirmPassword: z.string(),
  
  name: z
    .string()
    .min(2, 'Name must be at least 2 characters')
    .max(50, 'Name must be less than 50 characters'),
  
  age: z
    .number({ invalid_type_error: 'Age must be a number' })
    .min(18, 'Must be at least 18')
    .max(120, 'Invalid age')
    .optional(),
  
  role: z.enum(['admin', 'user', 'guest']),
  
  acceptTerms: z.literal(true, {
    errorMap: () => ({ message: 'You must accept the terms' })
  })
}).refine((data) => data.password === data.confirmPassword, {
  message: "Passwords don't match",
  path: ['confirmPassword']
})

// Convert to VeeValidate schema
export const userValidationSchema = toTypedSchema(userSchema)

// TypeScript type from schema
export type UserFormData = z.infer<typeof userSchema>
<!-- components/UserForm.vue -->
<script setup lang="ts">
import { useForm, useField } from 'vee-validate'
import { userValidationSchema, type UserFormData } from '@/schemas/user'

const emit = defineEmits<{
  submit: [data: UserFormData]
}>()

// Form setup
const { handleSubmit, errors, isSubmitting, meta, resetForm, setFieldError } = useForm({
  validationSchema: userValidationSchema,
  initialValues: {
    email: '',
    password: '',
    confirmPassword: '',
    name: '',
    role: 'user' as const,
    acceptTerms: false as const
  }
})

// Individual fields with useField
const { value: email, errorMessage: emailError } = useField<string>('email')
const { value: password, errorMessage: passwordError } = useField<string>('password')
const { value: confirmPassword, errorMessage: confirmPasswordError } = useField<string>('confirmPassword')
const { value: name, errorMessage: nameError } = useField<string>('name')
const { value: age, errorMessage: ageError } = useField<number | undefined>('age')
const { value: role } = useField<string>('role')
const { value: acceptTerms, errorMessage: acceptTermsError } = useField<boolean>('acceptTerms')

// Submit handler
const onSubmit = handleSubmit(async (values) => {
  try {
    emit('submit', values)
  } catch (error) {
    // Handle server-side validation errors
    if (error instanceof ApiError && error.errors) {
      Object.entries(error.errors).forEach(([field, message]) => {
        setFieldError(field as keyof UserFormData, message as string)
      })
    }
  }
})
</script>

<template>
  <form @submit="onSubmit" class="form">
    <div class="field">
      <label for="email">Email</label>
      <input 
        id="email" 
        v-model="email" 
        type="email"
        :class="{ 'error': emailError }"
      >
      <span v-if="emailError" class="error-message">{{ emailError }}</span>
    </div>
    
    <div class="field">
      <label for="password">Password</label>
      <input 
        id="password" 
        v-model="password" 
        type="password"
        :class="{ 'error': passwordError }"
      >
      <span v-if="passwordError" class="error-message">{{ passwordError }}</span>
    </div>
    
    <div class="field">
      <label for="confirmPassword">Confirm Password</label>
      <input 
        id="confirmPassword" 
        v-model="confirmPassword" 
        type="password"
        :class="{ 'error': confirmPasswordError }"
      >
      <span v-if="confirmPasswordError" class="error-message">{{ confirmPasswordError }}</span>
    </div>
    
    <div class="field">
      <label for="name">Name</label>
      <input id="name" v-model="name" type="text">
      <span v-if="nameError" class="error-message">{{ nameError }}</span>
    </div>
    
    <div class="field">
      <label for="age">Age (optional)</label>
      <input id="age" v-model.number="age" type="number">
      <span v-if="ageError" class="error-message">{{ ageError }}</span>
    </div>
    
    <div class="field">
      <label for="role">Role</label>
      <select id="role" v-model="role">
        <option value="user">User</option>
        <option value="admin">Admin</option>
        <option value="guest">Guest</option>
      </select>
    </div>
    
    <div class="field checkbox">
      <input id="acceptTerms" v-model="acceptTerms" type="checkbox">
      <label for="acceptTerms">I accept the terms and conditions</label>
      <span v-if="acceptTermsError" class="error-message">{{ acceptTermsError }}</span>
    </div>
    
    <button 
      type="submit" 
      :disabled="isSubmitting || !meta.valid"
    >
      {{ isSubmitting ? 'Submitting...' : 'Submit' }}
    </button>
  </form>
</template>

Reusable Form Field Component

<!-- components/FormField.vue -->
<script setup lang="ts">
import { useField } from 'vee-validate'
import { computed } from 'vue'

interface Props {
  name: string
  label: string
  type?: string
  placeholder?: string
  rules?: string
}

const props = withDefaults(defineProps<Props>(), {
  type: 'text',
  placeholder: ''
})

const { value, errorMessage, meta } = useField<string>(() => props.name)

const showError = computed(() => errorMessage.value && meta.touched)
</script>

<template>
  <div class="form-field" :class="{ 'has-error': showError }">
    <label :for="name">{{ label }}</label>
    <input
      :id="name"
      v-model="value"
      :type="type"
      :placeholder="placeholder"
      :aria-invalid="showError"
      :aria-describedby="showError ? `${name}-error` : undefined"
    >
    <span v-if="showError" :id="`${name}-error`" class="error" role="alert">
      {{ errorMessage }}
    </span>
  </div>
</template>

# 4.4 Testing Patterns

Vitest + Vue Test Utils Setup

// vitest.config.ts
import { defineConfig } from 'vitest/config'
import vue from '@vitejs/plugin-vue'
import { fileURLToPath } from 'node:url'

export default defineConfig({
  plugins: [vue()],
  test: {
    globals: true,
    environment: 'jsdom',
    setupFiles: ['./tests/setup.ts'],
    include: ['**/*.{test,spec}.{js,ts,vue}'],
    coverage: {
      provider: 'v8',
      reporter: ['text', 'json', 'html'],
      exclude: ['node_modules/', 'tests/']
    }
  },
  resolve: {
    alias: {
      '@': fileURLToPath(new URL('./src', import.meta.url))
    }
  }
})
// tests/setup.ts
import { config } from '@vue/test-utils'
import { createTestingPinia } from '@pinia/testing'
import { vi } from 'vitest'

// Global mocks
vi.mock('vue-router', async () => {
  const actual = await vi.importActual('vue-router')
  return {
    ...actual,
    useRouter: vi.fn(() => ({
      push: vi.fn(),
      replace: vi.fn(),
      back: vi.fn()
    })),
    useRoute: vi.fn(() => ({
      params: {},
      query: {},
      meta: {}
    }))
  }
})

// Global stubs
config.global.stubs = {
  teleport: true,
  transition: false
}

// Reset mocks between tests
beforeEach(() => {
  vi.clearAllMocks()
})

Component Testing

// components/__tests__/UserCard.spec.ts
import { describe, it, expect, vi } from 'vitest'
import { mount, flushPromises } from '@vue/test-utils'
import { createTestingPinia } from '@pinia/testing'
import UserCard from '../UserCard.vue'
import { useUserStore } from '@/stores/user'

describe('UserCard', () => {
  const defaultProps = {
    user: {
      id: '1',
      name: 'John Doe',
      email: '[email protected]',
      role: 'admin' as const
    }
  }
  
  const createWrapper = (props = defaultProps, options = {}) => {
    return mount(UserCard, {
      props,
      global: {
        plugins: [
          createTestingPinia({
            createSpy: vi.fn,
            stubActions: false  // Set to true to auto-stub all actions
          })
        ],
        ...options
      }
    })
  }
  
  it('renders user name and email', () => {
    const wrapper = createWrapper()
    
    expect(wrapper.text()).toContain('John Doe')
    expect(wrapper.text()).toContain('[email protected]')
  })
  
  it('displays admin badge for admin users', () => {
    const wrapper = createWrapper()
    
    expect(wrapper.find('.admin-badge').exists()).toBe(true)
  })
  
  it('hides admin badge for non-admin users', () => {
    const wrapper = createWrapper({
      user: { ...defaultProps.user, role: 'user' }
    })
    
    expect(wrapper.find('.admin-badge').exists()).toBe(false)
  })
  
  it('emits edit event when edit button is clicked', async () => {
    const wrapper = createWrapper()
    
    await wrapper.find('[data-testid="edit-button"]').trigger('click')
    
    expect(wrapper.emitted('edit')).toBeTruthy()
    expect(wrapper.emitted('edit')![0]).toEqual([defaultProps.user])
  })
  
  it('shows confirmation dialog before delete', async () => {
    const wrapper = createWrapper()
    const confirmSpy = vi.spyOn(window, 'confirm').mockReturnValue(true)
    
    await wrapper.find('[data-testid="delete-button"]').trigger('click')
    
    expect(confirmSpy).toHaveBeenCalledWith('Delete this user?')
    expect(wrapper.emitted('delete')).toBeTruthy()
  })
  
  it('does not emit delete when confirmation is cancelled', async () => {
    const wrapper = createWrapper()
    vi.spyOn(window, 'confirm').mockReturnValue(false)
    
    await wrapper.find('[data-testid="delete-button"]').trigger('click')
    
    expect(wrapper.emitted('delete')).toBeFalsy()
  })
})

Testing Composables

// composables/__tests__/useCounter.spec.ts
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest'
import { useCounter } from '../useCounter'

describe('useCounter', () => {
  it('initializes with default value', () => {
    const { count } = useCounter()
    expect(count.value).toBe(0)
  })
  
  it('initializes with custom value', () => {
    const { count } = useCounter({ initialValue: 10 })
    expect(count.value).toBe(10)
  })
  
  it('increments count', () => {
    const { count, increment } = useCounter()
    
    increment()
    expect(count.value).toBe(1)
    
    increment()
    expect(count.value).toBe(2)
  })
  
  it('respects max value', () => {
    const { count, increment } = useCounter({ initialValue: 9, max: 10 })
    
    increment()
    expect(count.value).toBe(10)
    
    increment()
    expect(count.value).toBe(10)  // Stays at max
  })
  
  it('computes doubled value', () => {
    const { count, doubled, increment } = useCounter()
    
    expect(doubled.value).toBe(0)
    
    increment()
    expect(doubled.value).toBe(2)
  })
})

Testing with TanStack Query

// composables/__tests__/useUsers.spec.ts
import { describe, it, expect, vi, beforeEach } from 'vitest'
import { flushPromises } from '@vue/test-utils'
import { QueryClient, VueQueryPlugin } from '@tanstack/vue-query'
import { mount } from '@vue/test-utils'
import { defineComponent, h } from 'vue'
import { useUsers } from '../useUsers'
import * as api from '@/lib/api'

vi.mock('@/lib/api')

describe('useUsers', () => {
  let queryClient: QueryClient
  
  beforeEach(() => {
    queryClient = new QueryClient({
      defaultOptions: {
        queries: {
          retry: false,
          gcTime: 0
        }
      }
    })
    vi.clearAllMocks()
  })
  
  const createTestComponent = (composableFn: () => any) => {
    let result: any
    
    const TestComponent = defineComponent({
      setup() {
        result = composableFn()
        return () => h('div')
      }
    })
    
    mount(TestComponent, {
      global: {
        plugins: [[VueQueryPlugin, { queryClient }]]
      }
    })
    
    return { result }
  }
  
  it('fetches users successfully', async () => {
    const mockUsers = [
      { id: '1', name: 'User 1' },
      { id: '2', name: 'User 2' }
    ]
    
    vi.mocked(api.get).mockResolvedValue({ data: mockUsers, meta: { total: 2 } })
    
    const { result } = createTestComponent(() => useUsers())
    
    expect(result.isLoading.value).toBe(true)
    
    await flushPromises()
    
    expect(result.isLoading.value).toBe(false)
    expect(result.data.value?.data).toEqual(mockUsers)
  })
  
  it('handles error state', async () => {
    vi.mocked(api.get).mockRejectedValue(new Error('Network error'))
    
    const { result } = createTestComponent(() => useUsers())
    
    await flushPromises()
    
    expect(result.isError.value).toBe(true)
    expect(result.error.value?.message).toBe('Network error')
  })
})

# 4.5 DevTools and Debugging

Vue DevTools Setup

// main.ts - Enable devtools in production (if needed)
const app = createApp(App)

// Vue 3 devtools are automatically enabled in dev
// For production debugging (use sparingly):
if (import.meta.env.VITE_ENABLE_DEVTOOLS === 'true') {
  app.config.devtools = true
}

Custom DevTools Plugin

// plugins/devtools.ts
import { setupDevtoolsPlugin, type DevtoolsPluginApi } from '@vue/devtools-api'
import type { App } from 'vue'

export function setupCustomDevtools(app: App) {
  setupDevtoolsPlugin(
    {
      id: 'my-app-devtools',
      label: 'My App DevTools',
      packageName: 'my-app',
      homepage: 'https://myapp.com',
      app
    },
    (api: DevtoolsPluginApi<{}>) => {
      // Add custom inspector
      api.addInspector({
        id: 'my-app-state',
        label: 'App State',
        icon: 'storage'
      })
      
      api.on.getInspectorTree((payload) => {
        if (payload.inspectorId === 'my-app-state') {
          payload.rootNodes = [
            {
              id: 'stores',
              label: 'Pinia Stores',
              children: [
                { id: 'user-store', label: 'User Store' },
                { id: 'cart-store', label: 'Cart Store' }
              ]
            }
          ]
        }
      })
      
      // Add timeline events
      api.addTimelineLayer({
        id: 'my-app-events',
        label: 'App Events',
        color: 0x41b883
      })
      
      // Log custom events
      ;(window as any).__logEvent = (label: string, data: any) => {
        api.addTimelineEvent({
          layerId: 'my-app-events',
          event: {
            time: Date.now(),
            title: label,
            data
          }
        })
      }
    }
  )
}

Debugging Utilities

// utils/debug.ts
import { watch, type WatchSource, type Ref } from 'vue'

// Log reactive value changes
export function useDebugWatch<T>(
  source: WatchSource<T>,
  name: string
) {
  if (import.meta.env.DEV) {
    watch(source, (newVal, oldVal) => {
      console.group(`🔄 ${name} changed`)
      console.log('Old:', oldVal)
      console.log('New:', newVal)
      console.groupEnd()
    }, { deep: true })
  }
}

// Component render tracker
export function useRenderTracker(componentName: string) {
  if (import.meta.env.DEV) {
    let renderCount = 0
    
    return () => {
      renderCount++
      console.log(`🎨 ${componentName} rendered (${renderCount} times)`)
    }
  }
  return () => {}
}

// Performance measurement
export function usePerfMark(name: string) {
  const start = () => performance.mark(`${name}-start`)
  const end = () => {
    performance.mark(`${name}-end`)
    performance.measure(name, `${name}-start`, `${name}-end`)
    
    const entries = performance.getEntriesByName(name)
    console.log(`⏱️ ${name}: ${entries[entries.length - 1].duration.toFixed(2)}ms`)
  }
  
  return { start, end }
}

// Debug component in template
export const DebugRef = defineComponent({
  props: {
    data: { required: true },
    label: { type: String, default: 'Debug' }
  },
  setup(props) {
    return () => import.meta.env.DEV 
      ? h('pre', { class: 'debug-output' }, 
          `${props.label}: ${JSON.stringify(props.data, null, 2)}`
        )
      : null
  }
})

Component Performance Profiling

<script setup lang="ts">
import { onRenderTracked, onRenderTriggered } from 'vue'

// Track what dependencies the component has
onRenderTracked((event) => {
  console.log('Render tracked:', event)
  // { effect, target, type, key }
})

// Track what caused a re-render
onRenderTriggered((event) => {
  console.log('Render triggered:', event)
  // { effect, target, type, key, newValue, oldValue }
})
</script>

Error Tracking Integration

// plugins/errorTracking.ts
import * as Sentry from '@sentry/vue'
import type { App, ComponentPublicInstance } from 'vue'
import { router } from '@/router'

export function setupErrorTracking(app: App) {
  Sentry.init({
    app,
    dsn: import.meta.env.VITE_SENTRY_DSN,
    
    integrations: [
      Sentry.browserTracingIntegration({ router }),
      Sentry.replayIntegration({
        maskAllText: false,
        blockAllMedia: false
      })
    ],
    
    // Performance
    tracesSampleRate: import.meta.env.PROD ? 0.1 : 1.0,
    
    // Session Replay
    replaysSessionSampleRate: 0.1,
    replaysOnErrorSampleRate: 1.0,
    
    // Environment
    environment: import.meta.env.MODE,
    
    // Before sending
    beforeSend(event, hint) {
      // Filter or modify events
      if (event.exception) {
        console.error('Sending to Sentry:', event.exception)
      }
      return event
    }
  })
  
  // Custom Vue error handler integration
  const originalErrorHandler = app.config.errorHandler
  
  app.config.errorHandler = (err, instance, info) => {
    Sentry.captureException(err, {
      extra: {
        componentName: instance?.$options?.name,
        info,
        propsData: instance?.$props
      }
    })
    
    // Call original handler
    originalErrorHandler?.(err, instance, info)
  }
}

// Usage: Capture custom errors
export function captureError(error: Error, context?: Record<string, any>) {
  Sentry.captureException(error, { extra: context })
}

// Usage: Add breadcrumbs
export function addBreadcrumb(message: string, data?: Record<string, any>) {
  Sentry.addBreadcrumb({
    message,
    data,
    level: 'info'
  })
}

Quick Reference Card

Task Solution
Local UI state ref() / reactive()
Computed/derived data computed()
Side effects on change watch() / watchEffect()
Shared logic Composables (use*.ts)
Global app state Pinia stores
Server data caching TanStack Query
Avoid prop drilling provide/inject or composables
Form validation VeeValidate + Zod
Routing Vue Router with guards
Testing Vitest + Vue Test Utils
Error tracking Sentry
DevTools Vue DevTools browser extension

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment