Introduction to Watchers

+15 Mana ✨

While computed properties handle derived state, watchers let you perform side effects in response to state changes—like fetching data, logging, or modifying the DOM.

When to Use Watchers

Use watchers when you need to:

  • Fetch data when a value changes
  • Perform async operations
  • Log or debug state changes
  • Interact with external systems

Basic watch()

The watch() function takes a source and a callback:

vue
<script setup>
import { ref, watch } from 'vue'

const searchQuery = ref('')

// Watch a single ref
watch(searchQuery, (newValue, oldValue) => {
  console.log(`Search changed from "${oldValue}" to "${newValue}"`)
})
</script>

<template>
  <input v-model="searchQuery" placeholder="Search..." />
</template>

Watching Different Sources

Single Ref

vue
<script setup>
import { ref, watch } from 'vue'

const count = ref(0)

watch(count, (newVal, oldVal) => {
  console.log(`Count: ${oldVal} → ${newVal}`)
})
</script>

Getter Function

For reactive object properties or computed expressions:

vue
<script setup>
import { reactive, watch } from 'vue'

const user = reactive({ name: 'Alice', age: 25 })

// Watch a specific property
watch(
  () => user.name,
  (newName) => {
    console.log(`Name changed to: ${newName}`)
  }
)

// Watch a computed expression
watch(
  () => user.age >= 18,
  (isAdult) => {
    console.log(`User is ${isAdult ? 'an adult' : 'a minor'}`)
  }
)
</script>

Multiple Sources

Watch multiple values at once:

vue
<script setup>
import { ref, watch } from 'vue'

const firstName = ref('John')
const lastName = ref('Doe')

watch(
  [firstName, lastName],
  ([newFirst, newLast], [oldFirst, oldLast]) => {
    console.log(`Name: ${oldFirst} ${oldLast} → ${newFirst} ${newLast}`)
  }
)
</script>

watchEffect()

watchEffect() automatically tracks all reactive dependencies used inside it:

vue
<script setup>
import { ref, watchEffect } from 'vue'

const count = ref(0)
const multiplier = ref(2)

// Automatically tracks count AND multiplier
watchEffect(() => {
  console.log(`Result: ${count.value * multiplier.value}`)
})
// Logs immediately: "Result: 0"
// Logs again whenever count or multiplier changes
</script>

watch() vs watchEffect()

Featurewatch()watchEffect()
Explicit dependenciesYesNo (auto-tracked)
Access to old valueYesNo
Lazy by defaultYesNo (runs immediately)
Best forSpecific state changesMultiple dependencies

Practical Examples

Fetching Data on Change

vue
<script setup>
import { ref, watch } from 'vue'

const userId = ref(1)
const userData = ref(null)
const isLoading = ref(false)

watch(userId, async (newId) => {
  isLoading.value = true
  try {
    const response = await fetch(`/api/users/${newId}`)
    userData.value = await response.json()
  } catch (error) {
    console.error('Failed to fetch user:', error)
  } finally {
    isLoading.value = false
  }
})
</script>

<template>
  <select v-model="userId">
    <option :value="1">User 1</option>
    <option :value="2">User 2</option>
    <option :value="3">User 3</option>
  </select>
  
  <div v-if="isLoading">Loading...</div>
  <div v-else-if="userData">
    <h2>{{ userData.name }}</h2>
    <p>{{ userData.email }}</p>
  </div>
</template>
vue
<script setup>
import { ref, watch } from 'vue'

const searchQuery = ref('')
const searchResults = ref([])

let debounceTimeout

watch(searchQuery, (query) => {
  // Clear previous timeout
  clearTimeout(debounceTimeout)
  
  if (!query) {
    searchResults.value = []
    return
  }
  
  // Wait 300ms before searching
  debounceTimeout = setTimeout(async () => {
    const response = await fetch(`/api/search?q=${query}`)
    searchResults.value = await response.json()
  }, 300)
})
</script>

Persisting to LocalStorage

vue
<script setup>
import { ref, watch } from 'vue'

// Load initial value from localStorage
const theme = ref(localStorage.getItem('theme') || 'light')

// Save to localStorage whenever it changes
watch(theme, (newTheme) => {
  localStorage.setItem('theme', newTheme)
  document.documentElement.setAttribute('data-theme', newTheme)
})
</script>

<template>
  <select v-model="theme">
    <option value="light">Light</option>
    <option value="dark">Dark</option>
    <option value="system">System</option>
  </select>
</template>

Watcher Options

immediate: Run on Creation

vue
<script setup>
import { ref, watch } from 'vue'

const userId = ref(1)

watch(
  userId,
  async (id) => {
    await fetchUser(id)
  },
  { immediate: true }  // Fetch immediately, not just on change
)
</script>

deep: Watch Nested Changes

vue
<script setup>
import { ref, watch } from 'vue'

const settings = ref({
  notifications: {
    email: true,
    push: false
  }
})

watch(
  settings,
  (newSettings) => {
    console.log('Settings changed:', newSettings)
  },
  { deep: true }  // Detect nested property changes
)
</script>
✓ Completed