Skip to content

Computed Properties & Watchers: Reactive Programming Magic! 🎯

You've mastered component communication, now it's time to unlock Vue's reactive programming superpowers! Computed properties and watchers are the secret weapons that make Vue applications intelligent, efficient, and responsive. They help you create derived state and react to changes automatically.

What You'll Master Today

  • Computed properties - smart, cached calculations that update automatically
  • Watchers - responding to data changes with side effects
  • When to use each - choosing the right tool for the job
  • Performance optimization - efficient reactive programming patterns
  • Real-world scenarios - practical applications of reactive programming

The Reactive Programming Landscape

🧮 Computed Properties: Smart Calculations

Think of computed properties as smart formulas that automatically recalculate when their ingredients change:

javascript
// Like Excel formulas that update automatically!
const total = computed(() => price.value * quantity.value)
// When price or quantity changes, total updates automatically

👀 Watchers: Reaction to Change

Think of watchers as observers that do something when they notice a change:

javascript
// Like a security guard watching for specific events
watch(userStatus, (newStatus) => {
  if (newStatus === 'offline') {
    saveUserData() // Do something when status changes
  }
})

Complete Reactive Programming Demo

Let's build a comprehensive e-commerce shopping cart that demonstrates both computed properties and watchers:

vue
<!-- App.vue - Reactive Shopping Cart Demo -->
<template>
  <div id="app">
    <h1>🛒 Reactive Shopping Cart Demo</h1>
    
    <!-- Product Catalog -->
    <div class="product-catalog">
      <h2>📦 Products</h2>
      <div class="products-grid">
        <ProductCard
          v-for="product in products"
          :key="product.id"
          :product="product"
          @add-to-cart="addToCart"
        />
      </div>
    </div>

    <!-- Shopping Cart -->
    <div class="shopping-cart">
      <h2>🛒 Shopping Cart ({{ cartItemCount }} items)</h2>
      
      <!-- Cart Summary - Using Computed Properties -->
      <div class="cart-summary">
        <div class="summary-row">
          <span>Subtotal:</span>
          <span>${{ subtotal.toFixed(2) }}</span>
        </div>
        <div class="summary-row">
          <span>Tax ({{ taxRate * 100 }}%):</span>
          <span>${{ taxAmount.toFixed(2) }}</span>
        </div>
        <div class="summary-row">
          <span>Shipping:</span>
          <span>${{ shippingCost.toFixed(2) }}</span>
        </div>
        <div class="summary-row total">
          <span>Total:</span>
          <span>${{ grandTotal.toFixed(2) }}</span>
        </div>
      </div>

      <!-- Cart Items -->
      <div class="cart-items">
        <CartItem
          v-for="item in cartItems"
          :key="item.id"
          :item="item"
          @update-quantity="updateQuantity"
          @remove-item="removeFromCart"
        />
      </div>

      <!-- Empty Cart Message -->
      <div v-if="cartItems.length === 0" class="empty-cart">
        <p>🛒 Your cart is empty</p>
        <p>Add some products to get started!</p>
      </div>

      <!-- Discount Section -->
      <div class="discount-section">
        <h3>💰 Discount Code</h3>
        <input
          v-model="discountCode"
          placeholder="Enter discount code"
          class="discount-input"
        >
        <div v-if="discountMessage" :class="['discount-message', discountType]">
          {{ discountMessage }}
        </div>
        <div v-if="discountAmount > 0" class="discount-applied">
          Discount Applied: -${{ discountAmount.toFixed(2) }}
        </div>
      </div>

      <!-- User Preferences (Watchers Demo) -->
      <div class="preferences-section">
        <h3>⚙️ Preferences</h3>
        <label>
          <input v-model="autoSave" type="checkbox">
          Auto-save cart
        </label>
        <label>
          <input v-model="notifications" type="checkbox">
          Price change notifications
        </label>
        <div class="last-saved" v-if="lastSaved">
          Last saved: {{ lastSaved }}
        </div>
      </div>

      <!-- Analytics Dashboard -->
      <div class="analytics-section">
        <h3>📊 Shopping Analytics</h3>
        <div class="analytics-grid">
          <div class="metric">
            <div class="metric-value">{{ totalItemsAdded }}</div>
            <div class="metric-label">Items Added</div>
          </div>
          <div class="metric">
            <div class="metric-value">${{ averageItemPrice.toFixed(2) }}</div>
            <div class="metric-label">Avg. Price</div>
          </div>
          <div class="metric">
            <div class="metric-value">{{ uniqueCategories }}</div>
            <div class="metric-label">Categories</div>
          </div>
          <div class="metric">
            <div class="metric-value">{{ shoppingSessionTime }}</div>
            <div class="metric-label">Session Time</div>
          </div>
        </div>
      </div>
    </div>
  </div>
</template>

<script setup>
import { ref, computed, watch, watchEffect, onMounted } from 'vue'
import ProductCard from './components/ProductCard.vue'
import CartItem from './components/CartItem.vue'

// 📦 Raw Data (State)
const products = ref([
  { id: 1, name: 'Laptop Pro', price: 1299.99, category: 'Electronics', image: '💻' },
  { id: 2, name: 'Wireless Mouse', price: 29.99, category: 'Electronics', image: '🖱️' },
  { id: 3, name: 'Coffee Mug', price: 12.99, category: 'Home', image: '☕' },
  { id: 4, name: 'Notebook', price: 8.99, category: 'Office', image: '📓' },
  { id: 5, name: 'Headphones', price: 199.99, category: 'Electronics', image: '🎧' },
  { id: 6, name: 'Plant Pot', price: 24.99, category: 'Home', image: '🪴' }
])

const cartItems = ref([])
const discountCode = ref('')
const taxRate = ref(0.08) // 8% tax
const autoSave = ref(false)
const notifications = ref(true)
const lastSaved = ref('')
const totalItemsAdded = ref(0)
const sessionStartTime = ref(Date.now())

// 🧮 COMPUTED PROPERTIES: Derived State (Cached & Efficient)

// Cart Statistics
const cartItemCount = computed(() => {
  return cartItems.value.reduce((total, item) => total + item.quantity, 0)
})

const subtotal = computed(() => {
  return cartItems.value.reduce((total, item) => {
    return total + (item.price * item.quantity)
  }, 0)
})

const taxAmount = computed(() => {
  return subtotal.value * taxRate.value
})

const shippingCost = computed(() => {
  // Free shipping over $100, otherwise $9.99
  return subtotal.value >= 100 ? 0 : 9.99
})

// Discount Logic (Complex Computed Property)
const discountAmount = computed(() => {
  const code = discountCode.value.toUpperCase()
  switch (code) {
    case 'SAVE10':
      return subtotal.value * 0.1 // 10% off
    case 'SAVE20':
      return subtotal.value * 0.2 // 20% off
    case 'FREESHIP':
      return shippingCost.value // Free shipping
    case 'STUDENT':
      return Math.min(subtotal.value * 0.15, 50) // 15% off, max $50
    default:
      return 0
  }
})

const grandTotal = computed(() => {
  return subtotal.value + taxAmount.value + shippingCost.value - discountAmount.value
})

// Discount Feedback
const discountType = computed(() => {
  return discountAmount.value > 0 ? 'success' : 'error'
})

const discountMessage = computed(() => {
  if (!discountCode.value) return ''
  
  const code = discountCode.value.toUpperCase()
  const validCodes = ['SAVE10', 'SAVE20', 'FREESHIP', 'STUDENT']
  
  if (validCodes.includes(code)) {
    return `✅ Valid discount code! Saving $${discountAmount.value.toFixed(2)}`
  } else {
    return '❌ Invalid discount code'
  }
})

// Analytics Computed Properties
const averageItemPrice = computed(() => {
  if (cartItems.value.length === 0) return 0
  return subtotal.value / cartItemCount.value
})

const uniqueCategories = computed(() => {
  const categories = new Set(
    cartItems.value.map(item => 
      products.value.find(p => p.id === item.id)?.category
    ).filter(Boolean)
  )
  return categories.size
})

const shoppingSessionTime = computed(() => {
  const minutes = Math.floor((Date.now() - sessionStartTime.value) / 60000)
  return `${minutes}m`
})

// 👀 WATCHERS: Side Effects and Reactions

// Watch cart changes for auto-save
watch(cartItems, (newItems) => {
  console.log('Cart changed:', newItems.length, 'items')
  
  if (autoSave.value) {
    saveCartToLocalStorage()
  }
}, { deep: true }) // Deep watch for nested changes

// Watch auto-save preference
watch(autoSave, (isEnabled) => {
  if (isEnabled) {
    saveCartToLocalStorage()
    lastSaved.value = new Date().toLocaleTimeString()
  }
})

// Watch for significant cart value changes
watch(grandTotal, (newTotal, oldTotal) => {
  if (notifications.value && oldTotal > 0) {
    const difference = Math.abs(newTotal - oldTotal)
    if (difference > 50) {
      console.log(`💰 Significant price change: $${difference.toFixed(2)}`)
      // In real app: show notification toast
    }
  }
})

// Watch discount code for analytics
watch(discountCode, (newCode, oldCode) => {
  if (newCode && newCode !== oldCode) {
    console.log('📊 Analytics: User tried discount code:', newCode)
    // In real app: track user behavior
  }
})

// Complex watcher: Track user engagement
watch(
  [cartItemCount, subtotal],
  ([newCount, newSubtotal], [oldCount, oldSubtotal]) => {
    // Track when user adds expensive items
    if (newCount > oldCount && newSubtotal - oldSubtotal > 100) {
      console.log('🎯 User added high-value item!')
      // In real app: trigger upsell recommendations
    }
  }
)

// 🚀 WATCHEFFECT: Automatic Dependency Tracking

// Automatically save when any cart-related data changes
watchEffect(() => {
  // This automatically tracks: cartItems, autoSave, discountCode
  if (cartItems.value.length > 0 && autoSave.value) {
    const cartData = {
      items: cartItems.value,
      discount: discountCode.value,
      timestamp: Date.now()
    }
    localStorage.setItem('shoppingCart', JSON.stringify(cartData))
    console.log('💾 Auto-saved cart data')
  }
})

// Automatically update document title with cart info
watchEffect(() => {
  document.title = cartItems.value.length > 0 
    ? `Shopping Cart (${cartItemCount.value}) - $${grandTotal.value.toFixed(2)}`
    : 'Shopping Cart - Empty'
})

// 🛠️ Methods (Regular Functions)
const addToCart = (product) => {
  const existingItem = cartItems.value.find(item => item.id === product.id)
  
  if (existingItem) {
    existingItem.quantity += 1
  } else {
    cartItems.value.push({
      id: product.id,
      name: product.name,
      price: product.price,
      quantity: 1
    })
  }
  
  totalItemsAdded.value += 1
}

const updateQuantity = (itemId, newQuantity) => {
  if (newQuantity <= 0) {
    removeFromCart(itemId)
    return
  }
  
  const item = cartItems.value.find(item => item.id === itemId)
  if (item) {
    item.quantity = newQuantity
  }
}

const removeFromCart = (itemId) => {
  cartItems.value = cartItems.value.filter(item => item.id !== itemId)
}

const saveCartToLocalStorage = () => {
  const cartData = {
    items: cartItems.value,
    discount: discountCode.value,
    timestamp: Date.now()
  }
  localStorage.setItem('shoppingCart', JSON.stringify(cartData))
  lastSaved.value = new Date().toLocaleTimeString()
}

const loadCartFromLocalStorage = () => {
  const saved = localStorage.getItem('shoppingCart')
  if (saved) {
    const cartData = JSON.parse(saved)
    cartItems.value = cartData.items || []
    discountCode.value = cartData.discount || ''
  }
}

// 🏁 Lifecycle
onMounted(() => {
  loadCartFromLocalStorage()
  console.log('🚀 Shopping cart app mounted!')
})
</script>

<style>
#app {
  max-width: 1200px;
  margin: 0 auto;
  padding: 20px;
  font-family: Arial, sans-serif;
}

h1 {
  text-align: center;
  color: #2c3e50;
  margin-bottom: 30px;
}

.product-catalog {
  margin-bottom: 40px;
}

.products-grid {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
  gap: 20px;
  margin-bottom: 30px;
}

.shopping-cart {
  background: #f8f9fa;
  padding: 20px;
  border-radius: 12px;
  border: 2px solid #e9ecef;
}

.cart-summary {
  background: white;
  padding: 20px;
  border-radius: 8px;
  margin-bottom: 20px;
  box-shadow: 0 2px 10px rgba(0,0,0,0.1);
}

.summary-row {
  display: flex;
  justify-content: space-between;
  margin-bottom: 10px;
  padding: 8px 0;
}

.summary-row.total {
  border-top: 2px solid #007bff;
  font-weight: bold;
  font-size: 1.2em;
  color: #007bff;
}

.cart-items {
  margin-bottom: 20px;
}

.empty-cart {
  text-align: center;
  padding: 40px;
  color: #6c757d;
  background: white;
  border-radius: 8px;
  border: 2px dashed #dee2e6;
}

.discount-section {
  background: white;
  padding: 20px;
  border-radius: 8px;
  margin-bottom: 20px;
}

.discount-input {
  width: 100%;
  padding: 12px;
  border: 2px solid #ced4da;
  border-radius: 4px;
  font-size: 16px;
  margin-bottom: 10px;
}

.discount-message {
  padding: 10px;
  border-radius: 4px;
  margin-bottom: 10px;
}

.discount-message.success {
  background: #d4edda;
  color: #155724;
  border: 1px solid #c3e6cb;
}

.discount-message.error {
  background: #f8d7da;
  color: #721c24;
  border: 1px solid #f5c6cb;
}

.discount-applied {
  background: #d1ecf1;
  color: #0c5460;
  padding: 10px;
  border-radius: 4px;
  font-weight: bold;
}

.preferences-section {
  background: white;
  padding: 20px;
  border-radius: 8px;
  margin-bottom: 20px;
}

.preferences-section label {
  display: block;
  margin: 10px 0;
  cursor: pointer;
}

.preferences-section input[type="checkbox"] {
  margin-right: 8px;
}

.last-saved {
  margin-top: 10px;
  font-style: italic;
  color: #6c757d;
}

.analytics-section {
  background: white;
  padding: 20px;
  border-radius: 8px;
}

.analytics-grid {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(120px, 1fr));
  gap: 15px;
  margin-top: 15px;
}

.metric {
  text-align: center;
  padding: 15px;
  background: #f8f9fa;
  border-radius: 8px;
  border: 1px solid #e9ecef;
}

.metric-value {
  font-size: 1.8em;
  font-weight: bold;
  color: #007bff;
  margin-bottom: 5px;
}

.metric-label {
  font-size: 0.9em;
  color: #6c757d;
}

@media (max-width: 768px) {
  .products-grid {
    grid-template-columns: 1fr;
  }
  
  .analytics-grid {
    grid-template-columns: repeat(2, 1fr);
  }
}
</style>

Now let's create the child components:

ProductCard.vue

vue
<template>
  <div class="product-card">
    <div class="product-image">{{ product.image }}</div>
    <h3>{{ product.name }}</h3>
    <p class="product-category">{{ product.category }}</p>
    <div class="product-price">${{ product.price.toFixed(2) }}</div>
    <button @click="addToCart" class="add-button">
      Add to Cart
    </button>
  </div>
</template>

<script setup>
const props = defineProps({
  product: {
    type: Object,
    required: true
  }
})

const emit = defineEmits(['add-to-cart'])

const addToCart = () => {
  emit('add-to-cart', props.product)
}
</script>

<style scoped>
.product-card {
  background: white;
  border-radius: 8px;
  padding: 20px;
  text-align: center;
  box-shadow: 0 2px 10px rgba(0,0,0,0.1);
  transition: transform 0.2s;
}

.product-card:hover {
  transform: translateY(-4px);
}

.product-image {
  font-size: 3em;
  margin-bottom: 15px;
}

.product-card h3 {
  margin: 10px 0;
  color: #2c3e50;
}

.product-category {
  color: #6c757d;
  font-size: 0.9em;
  margin-bottom: 10px;
}

.product-price {
  font-size: 1.3em;
  font-weight: bold;
  color: #28a745;
  margin-bottom: 15px;
}

.add-button {
  background: #007bff;
  color: white;
  border: none;
  padding: 12px 24px;
  border-radius: 4px;
  cursor: pointer;
  font-weight: bold;
  transition: background-color 0.2s;
  width: 100%;
}

.add-button:hover {
  background: #0056b3;
}
</style>

CartItem.vue

vue
<template>
  <div class="cart-item">
    <div class="item-info">
      <h4>{{ item.name }}</h4>
      <div class="item-price">${{ item.price.toFixed(2) }} each</div>
    </div>
    
    <div class="quantity-controls">
      <button @click="decreaseQuantity" :disabled="item.quantity <= 1">-</button>
      <span class="quantity">{{ item.quantity }}</span>
      <button @click="increaseQuantity">+</button>
    </div>
    
    <div class="item-total">
      ${{ (item.price * item.quantity).toFixed(2) }}
    </div>
    
    <button @click="removeItem" class="remove-button">🗑️</button>
  </div>
</template>

<script setup>
const props = defineProps({
  item: {
    type: Object,
    required: true
  }
})

const emit = defineEmits(['update-quantity', 'remove-item'])

const increaseQuantity = () => {
  emit('update-quantity', props.item.id, props.item.quantity + 1)
}

const decreaseQuantity = () => {
  emit('update-quantity', props.item.id, props.item.quantity - 1)
}

const removeItem = () => {
  emit('remove-item', props.item.id)
}
</script>

<style scoped>
.cart-item {
  display: flex;
  align-items: center;
  background: white;
  padding: 15px;
  border-radius: 8px;
  margin-bottom: 10px;
  box-shadow: 0 1px 5px rgba(0,0,0,0.1);
}

.item-info {
  flex: 1;
}

.item-info h4 {
  margin: 0 0 5px 0;
  color: #2c3e50;
}

.item-price {
  color: #6c757d;
  font-size: 0.9em;
}

.quantity-controls {
  display: flex;
  align-items: center;
  gap: 10px;
  margin: 0 20px;
}

.quantity-controls button {
  width: 30px;
  height: 30px;
  border: 1px solid #ced4da;
  background: white;
  cursor: pointer;
  border-radius: 4px;
  font-weight: bold;
}

.quantity-controls button:hover:not(:disabled) {
  background: #f8f9fa;
}

.quantity-controls button:disabled {
  opacity: 0.5;
  cursor: not-allowed;
}

.quantity {
  min-width: 30px;
  text-align: center;
  font-weight: bold;
}

.item-total {
  font-weight: bold;
  color: #28a745;
  min-width: 80px;
  text-align: right;
}

.remove-button {
  background: none;
  border: none;
  cursor: pointer;
  font-size: 1.2em;
  padding: 5px;
  margin-left: 15px;
  opacity: 0.6;
  transition: opacity 0.2s;
}

.remove-button:hover {
  opacity: 1;
}
</style>

Understanding Computed Properties vs Watchers

🧮 Computed Properties: When to Use

Perfect for:

  • Derived state that depends on other reactive data
  • Calculations that should be cached
  • Formatting data for display
  • Filtering or sorting lists
javascript
// ✅ Great use cases for computed properties
const fullName = computed(() => `${firstName.value} ${lastName.value}`)
const filteredProducts = computed(() => products.value.filter(p => p.inStock))
const totalPrice = computed(() => cartItems.value.reduce((sum, item) => sum + item.price, 0))
const formattedDate = computed(() => new Date(timestamp.value).toLocaleDateString())

Why computed properties are amazing:

  • Cached - only recalculates when dependencies change
  • Declarative - clearly express what the value should be
  • Automatic - Vue tracks dependencies for you
  • Efficient - no unnecessary recalculations

👀 Watchers: When to Use

Perfect for:

  • Side effects in response to data changes
  • API calls when data changes
  • Local storage updates
  • External library integration
javascript
// ✅ Great use cases for watchers
watch(userId, async (newId) => {
  // Fetch user data when ID changes
  userData.value = await fetchUser(newId)
})

watch(cartItems, (newItems) => {
  // Save to localStorage when cart changes
  localStorage.setItem('cart', JSON.stringify(newItems))
}, { deep: true })

watch(searchQuery, (newQuery) => {
  // Debounced search API call
  debounceSearch(newQuery)
})

Deep Dive: Different Types of Watchers

1. Basic Watch

javascript
// Watch a single ref
const count = ref(0)
watch(count, (newValue, oldValue) => {
  console.log(`Count changed from ${oldValue} to ${newValue}`)
})

2. Watch Multiple Sources

javascript
// Watch multiple things at once
watch([firstName, lastName], ([newFirst, newLast], [oldFirst, oldLast]) => {
  console.log(`Name changed: ${newFirst} ${newLast}`)
})

3. Watch with Getter Function

javascript
// Watch a computed value or complex expression
watch(
  () => user.value.profile.name,
  (newName) => {
    console.log('Username changed:', newName)
  }
)

4. Deep Watch

javascript
// Watch for nested object changes
const userProfile = ref({
  personal: { name: 'John', age: 30 },
  preferences: { theme: 'dark', notifications: true }
})

watch(userProfile, (newProfile) => {
  saveProfile(newProfile)
}, { deep: true }) // Watches ALL nested properties

5. Immediate Watch

javascript
// Run immediately AND on changes
watch(
  apiUrl,
  (url) => {
    fetchData(url)
  },
  { immediate: true } // Runs right away, then on changes
)

6. Once Watch (Vue 3.4+)

javascript
// Run only once when the value changes
watch(
  initializationComplete,
  () => {
    setupComplexFeature()
  },
  { once: true } // Only runs the first time it changes
)

WatchEffect: Automatic Dependency Tracking

WatchEffect is like a super-smart watcher that automatically figures out what it should watch:

javascript
// 🤖 Automatic dependency tracking
watchEffect(() => {
  // Vue automatically tracks: userId, preferences, apiEndpoint
  if (userId.value && preferences.value.autoSync) {
    syncData(apiEndpoint.value, userId.value)
  }
})

When to use watchEffect:

  • Multiple dependencies that might change
  • Complex dependency relationships
  • You want automatic dependency tracking

When to use watch:

  • Explicit control over what triggers the watcher
  • Access to old and new values
  • Conditional watching logic

Advanced Watcher Patterns

1. Cleanup Functions

javascript
// Clean up side effects when watcher re-runs
watch(searchQuery, (query, oldQuery, onCleanup) => {
  const controller = new AbortController()
  
  fetch(`/api/search?q=${query}`, {
    signal: controller.signal
  }).then(handleResults)
  
  // Cleanup function - runs before next execution
  onCleanup(() => {
    controller.abort() // Cancel previous request
  })
})

2. Flush Timing Control

javascript
// Control WHEN the watcher runs
watch(data, callback, {
  flush: 'post' // Run after DOM updates
})

watch(data, callback, {
  flush: 'sync' // Run synchronously (be careful!)
})

3. Conditional Watching

javascript
// Only watch when certain conditions are met
watchEffect(() => {
  if (isAuthenticated.value && userData.value) {
    // Only run when user is authenticated AND we have data
    updateUserPreferences(userData.value)
  }
})

Performance Optimization Tips

1. Choose the Right Tool

javascript
// ✅ Good: Use computed for derived state
const expensiveCalculation = computed(() => {
  return heavyComputation(data.value) // Cached!
})

// ❌ Avoid: Using watchers for simple calculations
watch(data, (newData) => {
  result.value = heavyComputation(newData) // Runs every time!
})

2. Debounce Expensive Operations

javascript
import { debounce } from 'lodash-es'

const debouncedSearch = debounce((query) => {
  searchAPI(query)
}, 300)

watch(searchQuery, debouncedSearch)

3. Be Careful with Deep Watching

javascript
// ✅ Good: Watch specific properties
watch(() => user.value.preferences.theme, (newTheme) => {
  updateTheme(newTheme)
})

// ⚠️ Use carefully: Deep watching large objects
watch(entireUserObject, callback, { deep: true }) // Can be expensive!

4. Stop Watchers When Needed

javascript
// Stop watchers to prevent memory leaks
const stopWatcher = watch(someRef, callback)

// Later, when no longer needed
stopWatcher()

Debugging Computed Properties and Watchers

1. Computed Property Debugging

javascript
const expensiveComputed = computed(() => {
  console.log('🧮 Computing expensive value...') // Tracks when it runs
  return heavyCalculation(data.value)
})

2. Watcher Debugging

javascript
watch(
  () => user.value.status,
  (newStatus, oldStatus) => {
    console.log('👀 Watcher fired:', { newStatus, oldStatus })
    console.trace('Called from:') // Show call stack
  },
  {
    onTrack(e) {
      console.log('🎯 Dependency tracked:', e)
    },
    onTrigger(e) {
      console.log('🔥 Watcher triggered by:', e)
    }
  }
)

3. Dependency Tracking Issues

javascript
// ❌ Problem: Accessing reactive data outside reactive context
const user = ref({ name: 'John', age: 30 })

// This won't be reactive
const userName = user.value.name

// ✅ Solution: Use computed or access inside reactive context
const userName = computed(() => user.value.name)

Real-World Patterns and Examples

1. Search with Debouncing

javascript
const searchQuery = ref('')
const searchResults = ref([])
const isSearching = ref(false)

// Debounced search function
const performSearch = debounce(async (query) => {
  if (!query.trim()) {
    searchResults.value = []
    return
  }
  
  isSearching.value = true
  try {
    const results = await searchAPI(query)
    searchResults.value = results
  } catch (error) {
    console.error('Search failed:', error)
    searchResults.value = []
  } finally {
    isSearching.value = false
  }
}, 300)

// Watch for search query changes
watch(searchQuery, performSearch)

// Computed property for search state
const hasSearchResults = computed(() => searchResults.value.length > 0)
const showEmptyState = computed(() => 
  searchQuery.value.trim() && !isSearching.value && !hasSearchResults.value
)

2. Form Validation

javascript
const formData = ref({
  email: '',
  password: '',
  confirmPassword: ''
})

// Computed validation rules
const emailValid = computed(() => {
  const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/
  return emailRegex.test(formData.value.email)
})

const passwordValid = computed(() => {
  return formData.value.password.length >= 8
})

const passwordsMatch = computed(() => {
  return formData.value.password === formData.value.confirmPassword
})

const formValid = computed(() => {
  return emailValid.value && passwordValid.value && passwordsMatch.value
})

// Watch for validation changes
watch(formValid, (isValid) => {
  if (isValid) {
    console.log('✅ Form is now valid!')
  }
})

// Watch for password mismatch
watch([() => formData.value.password, () => formData.value.confirmPassword], 
  ([password, confirm]) => {
    if (password && confirm && password !== confirm) {
      console.log('⚠️ Passwords do not match')
    }
  }
)

3. Data Synchronization

javascript
const localData = ref([])
const lastSyncTime = ref(null)
const syncStatus = ref('idle') // 'idle', 'syncing', 'success', 'error'

// Auto-sync when data changes
watch(localData, (newData) => {
  if (newData.length > 0) {
    syncToServer()
  }
}, { deep: true })

// Computed sync indicators
const needsSync = computed(() => {
  if (!lastSyncTime.value) return true
  const timeSinceSync = Date.now() - lastSyncTime.value
  return timeSinceSync > 300000 // 5 minutes
})

const syncStatusMessage = computed(() => {
  switch (syncStatus.value) {
    case 'syncing': return '🔄 Syncing...'
    case 'success': return '✅ Synced'
    case 'error': return '❌ Sync failed'
    default: return needsSync.value ? '⏳ Needs sync' : '✅ Up to date'
  }
})

const syncToServer = async () => {
  syncStatus.value = 'syncing'
  try {
    await apiSync(localData.value)
    lastSyncTime.value = Date.now()
    syncStatus.value = 'success'
  } catch (error) {
    syncStatus.value = 'error'
    console.error('Sync failed:', error)
  }
}

4. Theme Management

javascript
const theme = ref('light')
const userPreferences = ref({
  autoTheme: true,
  preferredTheme: 'light'
})

// Computed theme based on preferences and system
const effectiveTheme = computed(() => {
  if (!userPreferences.value.autoTheme) {
    return userPreferences.value.preferredTheme
  }
  
  // Auto-detect system theme
  if (window.matchMedia('(prefers-color-scheme: dark)').matches) {
    return 'dark'
  }
  
  return 'light'
})

// Watch theme changes and apply to DOM
watch(effectiveTheme, (newTheme) => {
  document.documentElement.setAttribute('data-theme', newTheme)
  localStorage.setItem('theme', newTheme)
}, { immediate: true })

// Watch system theme changes
watchEffect(() => {
  if (userPreferences.value.autoTheme) {
    const mediaQuery = window.matchMedia('(prefers-color-scheme: dark)')
    const handleChange = () => {
      // This will trigger the effectiveTheme computed property
      // which will then trigger the theme watcher
    }
    mediaQuery.addListener(handleChange)
    
    // Cleanup
    return () => mediaQuery.removeListener(handleChange)
  }
})

5. Shopping Cart Persistence

javascript
const cartItems = ref([])
const cartSettings = ref({
  autoSave: true,
  saveInterval: 30000 // 30 seconds
})

// Computed cart summary
const cartSummary = computed(() => ({
  itemCount: cartItems.value.reduce((sum, item) => sum + item.quantity, 0),
  totalValue: cartItems.value.reduce((sum, item) => sum + (item.price * item.quantity), 0),
  lastUpdated: Date.now()
}))

// Watch cart changes for auto-save
watch(cartItems, (newItems) => {
  if (cartSettings.value.autoSave) {
    saveCart()
  }
}, { deep: true })

// Periodic save watcher
watchEffect((onCleanup) => {
  if (cartSettings.value.autoSave && cartItems.value.length > 0) {
    const interval = setInterval(() => {
      saveCart()
    }, cartSettings.value.saveInterval)
    
    onCleanup(() => clearInterval(interval))
  }
})

const saveCart = () => {
  const cartData = {
    items: cartItems.value,
    summary: cartSummary.value,
    timestamp: Date.now()
  }
  localStorage.setItem('shoppingCart', JSON.stringify(cartData))
}

Common Mistakes and How to Avoid Them

1. Infinite Update Loops

javascript
// ❌ BAD: This creates an infinite loop!
const count = ref(0)
watch(count, () => {
  count.value++ // This triggers the watcher again!
})

// ✅ GOOD: Use conditions to prevent loops
watch(count, (newCount) => {
  if (newCount < 10) {
    count.value++ // Safe with condition
  }
})

2. Watching Wrong Things

javascript
// ❌ BAD: Watching computed property that depends on the same data
const doubled = computed(() => count.value * 2)
watch(doubled, () => {
  // This is redundant! Just watch count directly
})

// ✅ GOOD: Watch the source data
watch(count, (newCount) => {
  // React to count changes
})

3. Not Cleaning Up Side Effects

javascript
// ❌ BAD: Memory leaks from intervals/subscriptions
watch(isActive, (active) => {
  if (active) {
    setInterval(() => {
      // This interval never gets cleared!
    }, 1000)
  }
})

// ✅ GOOD: Proper cleanup
watch(isActive, (active, oldActive, onCleanup) => {
  if (active) {
    const interval = setInterval(() => {
      // Do something
    }, 1000)
    
    onCleanup(() => {
      clearInterval(interval)
    })
  }
})

4. Overusing Deep Watching

javascript
// ❌ BAD: Deep watching everything
const appState = ref({
  user: { /* lots of data */ },
  settings: { /* lots of data */ },
  cache: { /* lots of data */ }
})

watch(appState, () => {
  // Expensive! Watches ALL nested properties
}, { deep: true })

// ✅ GOOD: Watch specific properties
watch(() => appState.value.user.preferences, (prefs) => {
  // Only watches what you need
})

Testing Computed Properties and Watchers

1. Testing Computed Properties

javascript
import { ref, computed } from 'vue'

// Test computed property
test('calculates total price correctly', () => {
  const items = ref([
    { price: 10, quantity: 2 },
    { price: 15, quantity: 1 }
  ])
  
  const total = computed(() => 
    items.value.reduce((sum, item) => sum + (item.price * item.quantity), 0)
  )
  
  expect(total.value).toBe(35) // 20 + 15
  
  // Test reactivity
  items.value.push({ price: 5, quantity: 3 })
  expect(total.value).toBe(50) // 35 + 15
})

2. Testing Watchers

javascript
import { ref, watch, nextTick } from 'vue'

test('watcher fires when value changes', async () => {
  const count = ref(0)
  const mockCallback = jest.fn()
  
  watch(count, mockCallback)
  
  count.value = 5
  await nextTick() // Wait for watcher to fire
  
  expect(mockCallback).toHaveBeenCalledWith(5, 0)
})

Performance Monitoring

1. Computed Property Performance

javascript
const expensiveComputed = computed(() => {
  const start = performance.now()
  const result = heavyCalculation(data.value)
  const end = performance.now()
  
  if (end - start > 10) { // Log slow computations
    console.warn(`Slow computed property: ${end - start}ms`)
  }
  
  return result
})

2. Watcher Performance

javascript
watch(data, (newData, oldData) => {
  const start = performance.now()
  
  processData(newData)
  
  const end = performance.now()
  if (end - start > 50) {
    console.warn(`Slow watcher: ${end - start}ms`)
  }
})

What You've Mastered! 🏆

  • Computed properties - efficient, cached derived state
  • Watchers - reactive side effects and data synchronization
  • WatchEffect - automatic dependency tracking
  • Performance optimization - choosing the right tool for the job
  • Advanced patterns - real-world reactive programming
  • Debugging techniques - troubleshooting reactive code
  • Best practices - avoiding common pitfalls and mistakes

Key Principles to Remember

1. Computed vs Watchers Decision Matrix

Use Computed When:Use Watchers When:
Deriving values from other dataPerforming side effects
Formatting data for displayMaking API calls
Filtering/sorting listsUpdating localStorage
Simple calculationsComplex async operations
Values that should be cachedTriggering external actions

2. The Reactive Programming Mindset

  • Think declaratively - describe what should happen, not how
  • Minimize side effects - keep computed properties pure
  • Use the right tool - computed for values, watchers for effects
  • Clean up properly - prevent memory leaks

3. Performance Best Practices

  • Prefer computed properties for derived state
  • Debounce expensive watchers to avoid excessive calls
  • Be selective with deep watching - only when necessary
  • Monitor performance - log slow operations in development

What's Next?

In the next project, you'll learn about lifecycle hooks - understanding when components are created, mounted, updated, and destroyed. This will complete your understanding of the Vue component lifecycle and help you know exactly when to perform different operations!

Quick Reference

Essential Patterns

javascript
// Computed Properties (Cached, Derived State)
const total = computed(() => items.value.reduce((sum, item) => sum + item.price, 0))

// Basic Watcher (Side Effects)
watch(userId, (newId) => fetchUserData(newId))

// Deep Watcher (Nested Objects)
watch(formData, saveForm, { deep: true })

// Immediate Watcher (Run on Mount)
watch(apiUrl, fetchData, { immediate: true })

// Multiple Sources
watch([first, second], ([newFirst, newSecond]) => {})

// WatchEffect (Automatic Dependencies)
watchEffect(() => {
  if (user.value && settings.value.autoSync) {
    syncData()
  }
})

Common Use Cases

  • Shopping cart totals → Computed properties
  • Search results filtering → Computed properties
  • API calls on data change → Watchers
  • Form validation → Computed properties
  • Auto-save functionality → Watchers
  • Theme switching → Watchers with localStorage

You now have the reactive programming superpowers to build intelligent, efficient Vue applications! 🌟