Fix: Invalid Watch Option in Vue.js

Error message:
Invalid watch option: "{key}"
Watchers & Computed 2025-01-25

What Causes This Warning?

This warning occurs when you pass an invalid or misspelled option to Vue’s watch function. Only specific options like immediate, deep, flush, and once are valid.

The Problem

// ❌ Invalid option names
watch(source, callback, {
  immedaite: true,  // Typo!
  depp: true,       // Typo!
  lazy: true,       // Not a valid option
  sync: true        // Not a valid option (use flush: 'sync')
})

The Fix

Use Valid Options

// ✅ Correct option names
watch(source, callback, {
  immediate: true,  // Run callback immediately
  deep: true,       // Deep watch for nested changes
  flush: 'post',    // Timing: 'pre' | 'post' | 'sync'
  once: true        // Only trigger once (Vue 3.4+)
})

Valid Watch Options

OptionTypeDefaultDescription
immediatebooleanfalseRun callback immediately on creation
deepbooleanfalseDeep watch for nested changes
flush'pre' | 'post' | 'sync''pre'Callback timing
oncebooleanfalseOnly trigger once (3.4+)
onTrackfunction-Debug: when dependency is tracked
onTriggerfunction-Debug: when callback is triggered

Common Scenarios

Immediate Execution

// ❌ Wrong
watch(count, handler, { now: true })

// ✅ Correct
watch(count, handler, { immediate: true })

Deep Watching

const user = ref({ name: '', profile: { age: 0 } })

// ❌ Wrong
watch(user, handler, { recursive: true })

// ✅ Correct
watch(user, handler, { deep: true })

// Or watch specific path
watch(() => user.value.profile.age, handler)

Flush Timing

// ❌ Wrong
watch(source, handler, { sync: true })
watch(source, handler, { post: true })

// ✅ Correct
watch(source, handler, { flush: 'sync' })  // Synchronous
watch(source, handler, { flush: 'post' })  // After DOM update
watch(source, handler, { flush: 'pre' })   // Before DOM update (default)

// Shorthand for post flush
watchPostEffect(() => {
  // Runs after DOM updates
})

// Shorthand for sync flush
watchSyncEffect(() => {
  // Runs synchronously
})

One-Time Watch

// ❌ Wrong (Vue 3.4+)
watch(source, handler, { single: true })

// ✅ Correct
watch(source, handler, { once: true })

// Or manually stop
const stop = watch(source, (value) => {
  // Do something
  stop() // Stop watching
})

Options API Watch

export default {
  watch: {
    // ❌ Wrong option in Options API
    count: {
      handler(newVal) { /* ... */ },
      immedaite: true  // Typo!
    },

    // ✅ Correct
    count: {
      handler(newVal, oldVal) {
        console.log('Count changed:', newVal)
      },
      immediate: true,
      deep: true
    }
  }
}

Debugging Options

// ✅ Debug options (development only)
watch(source, callback, {
  onTrack(e) {
    console.log('Tracking:', e)
  },
  onTrigger(e) {
    console.log('Triggered:', e)
  }
})

watchEffect vs watch

// watchEffect - no options for source
watchEffect(() => {
  console.log(count.value)
}, {
  flush: 'post',     // ✅ Valid
  onTrack: (e) => {} // ✅ Valid
})

// watch - has source, supports all options
watch(count, (newVal) => {
  console.log(newVal)
}, {
  immediate: true,   // ✅ Valid
  deep: true,        // ✅ Valid (for refs/reactive)
  flush: 'post',     // ✅ Valid
  once: true         // ✅ Valid (Vue 3.4+)
})

TypeScript

import { watch, WatchOptions } from 'vue'

const options: WatchOptions = {
  immediate: true,
  deep: true,
  flush: 'post'
}

watch(source, callback, options)

Quick Checklist

  • Check spelling: immediate not immedaite
  • Use deep not recursive or depp
  • Use flush: 'sync' not sync: true
  • Use once for one-time watchers (Vue 3.4+)
  • Only flush, onTrack, onTrigger work with watchEffect
  • Use TypeScript for option validation