Skip to content
← All questions
Intermediate

What's the difference between watch and watchEffect?

ReactivityComposition API

Both run code in response to reactive changes, but they differ in how you tell Vue what to watch and when they first run.

watch: you specify what to observe

watch takes an explicit source (a ref, a getter, or an array of sources) and only runs when that specific source changes. It gives you both the old and new values.

ts
import { ref, watch } from 'vue'

const count = ref(0)

watch(count, (newVal, oldVal) => {
  console.log(`Changed from ${oldVal} to ${newVal}`)
})

// Watch multiple sources
const firstName = ref('Ana')
const lastName = ref('García')

watch([firstName, lastName], ([newFirst, newLast], [oldFirst, oldLast]) => {
  console.log(`Name changed to ${newFirst} ${newLast}`)
})

watch is lazy by default — it doesn't run until the source actually changes. Add { immediate: true } if you need it to run once immediately.

watchEffect: Vue figures out the dependencies

watchEffect runs your callback immediately and automatically tracks every reactive value you read inside it. When any of those values change, it re-runs.

ts
import { ref, watchEffect } from 'vue'

const query = ref('')
const page = ref(1)

watchEffect(async () => {
  // Vue sees you reading query.value and page.value
  // It will re-run this whenever either changes
  const results = await fetch(`/api/search?q=${query.value}&page=${page.value}`)
  // ...
})

You don't list dependencies anywhere — Vue detects them at runtime by observing which reactive values your code actually reads.

When to use which

Use watch when:

  • You need the previous value (e.g., comparing old vs new to decide what to do)
  • You want to watch something specific and ignore other reactive data in the callback
  • You want lazy execution (don't run until the first change)
  • You need { deep: true } to watch nested object changes
ts
watch(route, (newRoute, oldRoute) => {
  if (newRoute.path !== oldRoute.path) {
    analytics.track('page_view', { path: newRoute.path })
  }
})

Use watchEffect when:

  • You have a simple side effect that depends on reactive data and should run right away
  • You don't care about the previous value
  • You want dependencies tracked automatically (less code, fewer mistakes)
ts
watchEffect(() => {
  document.title = `${count.value} items — MyApp`
})

The auto-tracking caveat

watchEffect only tracks dependencies that are read synchronously during execution. If you read a reactive value after an await, Vue won't track it:

ts
watchEffect(async () => {
  // ✅ tracked — read before await
  const url = `/api/users?role=${role.value}`

  const data = await fetch(url)

  // ❌ NOT tracked — read after await
  console.log(filter.value)
})

If you hit this, either read all reactive values before the first await, or switch to watch with explicit sources.

See also: What's the difference between computed and watch? · How does Vue batch DOM updates?

References

Released under the MIT License.