Vue watchEffect vs watch: Understanding the Difference in Vue 3 Composition API

watchEffect runs immediately and tracks any reactive dependencies accessed inside its callback automatically, while watch requires explicit source declaration and provides access to previous and new values.

When working with the Vue 3 Composition API in the vuejs/core repository, understanding the distinction between watchEffect and watch is essential for building reactive applications. Both functions reside in the reactivity system but serve fundamentally different purposes regarding dependency tracking, callback signatures, and execution timing.

Core Differences Between watchEffect and watch

The primary distinction lies in how each function tracks reactive dependencies and what information they provide to your callbacks.

Aspect watchEffect watch
Signature watchEffect(effect, options?) watch(source, callback?, options?)
Dependency Tracking Implicitly tracks any reactive values accessed inside the effect function Explicitly watches declared sources (ref, computed, getter, or array)
Callback Arguments Receives only onCleanup function Receives (newValue, oldValue, onCleanup)
Options Limited to flush ('pre' | 'post' | 'sync') Full WatchOptions including immediate, deep, once, flush
Execution Runs immediately and re-runs when any accessed dependency changes Runs only when specified source changes (or immediately with immediate: true)

Signature and Source Tracking

In packages/runtime-core/src/apiWatch.ts, watchEffect delegates to the internal doWatch function with a null source parameter, indicating it should operate as a "simple effect" without explicit source tracking【watchEffect implementation†https://github.com/vuejs/core/blob/main/packages/runtime-core/src/apiWatch.ts#L55-L61】.

Conversely, watch requires you to provide a source—whether a reactive object, ref, computed property, or getter function. This explicit declaration allows the watcher to precisely track only the specified dependencies rather than any reactive value touched during execution.

Callback Arguments and Options

watchEffect provides minimal context—only an onCleanup function for registering cleanup logic before re-execution. The watch function, however, supplies both newValue and oldValue to your callback, enabling comparison logic, animations, or conditional updates based on value transitions.

The options diverge significantly as well. While watchEffect only accepts flush timing options, watch supports immediate (run on initialization), deep (recursive watching of nested objects), and once (run only once when the source changes)【WatchOptions†https://github.com/vuejs/core/blob/main/packages/runtime-core/src/apiWatch.ts#L49-L53】.

How watchEffect Works Under the Hood

The implementation in packages/reactivity/src/watch.ts handles the core logic for both functions. When you call watchEffect, the system creates a ReactiveEffect that executes your provided function immediately. Because no source is specified, the effect tracks every reactive dependency accessed during execution automatically.

import { ref, watchEffect } from 'vue'

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

watchEffect((onCleanup) => {
  // Automatically tracks both count and multiplier
  console.log('Result:', count.value * multiplier.value)
  
  // Setup cleanup for previous runs
  const controller = new AbortController()
  onCleanup(() => controller.abort())
  
  // Fetch data using controller.signal...
})

// Changing either value triggers the effect
count.value = 5
multiplier.value = 3

This "autorun" style behavior makes watchEffect ideal for side effects that simply need to stay synchronized with reactive state without caring about which specific property changed or what the previous value was.

How watch Works Under the Hood

The watch function provides granular control by requiring explicit source declaration. In packages/reactivity/src/watch.ts, the implementation resolves your source into a getter function, then compares new and old values when the reactive effect triggers【watch implementation†https://github.com/vuejs/core/blob/main/packages/reactivity/src/watch.ts#L20-L30】.

Watching a Single Source

import { ref, watch } from 'vue'

const name = ref('Alice')

watch(name, (newVal, oldVal) => {
  console.log(`Name changed from ${oldVal} to ${newVal}`)
}, { immediate: true })  // Runs immediately with undefined as oldVal

Deep Watching Complex Objects

When watching reactive objects or nested properties, use the deep option to traverse nested dependencies:

import { reactive, watch } from 'vue'

const settings = reactive({ 
  theme: { dark: false },
  notifications: { email: true }
})

watch(
  () => settings.theme,
  (newVal, oldVal) => {
    console.log('Theme updated:', newVal)
  },
  { deep: true }  // Watches nested properties inside theme
)

Watching Multiple Sources

import { ref, watch } from 'vue'

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

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

When to Use watchEffect vs watch

Choosing between these functions depends on your specific requirements for dependency tracking and callback information.

Use watchEffect when:

  • You need a "fire and forget" side effect that automatically tracks all reactive dependencies
  • You don't need access to the previous value
  • You're performing simple synchronization tasks like updating the DOM, logging, or fetching data based on multiple reactive values
  • You want the effect to run immediately without waiting for the first change

Use watch when:

  • You need to watch specific sources rather than any accessed reactive value
  • You require access to oldValue and newValue for comparison or transition logic
  • You need advanced options like deep (recursive watching), immediate (run on mount), or once (single execution)
  • You want to avoid unnecessary re-runs when unrelated reactive state changes within the same scope

Summary

  • watchEffect provides automatic dependency tracking by running an effect function immediately and re-running whenever any reactive value accessed inside changes, but offers no access to previous values.
  • watch requires explicit source declaration and provides (newValue, oldValue) to the callback, supporting advanced options like deep, immediate, and once for granular control over reactive observation.
  • Both functions delegate to the internal doWatch implementation in packages/runtime-core/src/apiWatch.ts and packages/reactivity/src/watch.ts, sharing the same underlying ReactiveEffect architecture but differing in source resolution and callback invocation patterns.

Frequently Asked Questions

Can I access the previous value inside watchEffect?

No, watchEffect does not provide access to previous values. The callback receives only an onCleanup function for registering cleanup logic. If you need to compare new and old values, use watch instead, which passes (newValue, oldValue) as arguments to your callback.

Why does watchEffect run immediately while watch waits for changes?

By design, watchEffect executes immediately to establish its reactive dependencies and perform initial side effects. In contrast, watch only runs when its explicitly declared source changes, unless you specify immediate: true in the options object. This behavior difference reflects their distinct use cases: watchEffect for immediate synchronization and watch for change-specific reactions.

When should I use flush: 'post' with these watchers?

Use flush: 'post' when your watcher needs to access the DOM after Vue has updated it, or when you want to defer the watcher callback until after the component has rendered. This is particularly useful in watchEffect when performing DOM measurements or animations that depend on the final rendered state. Without this option, both watch and watchEffect run in the 'pre' flush mode by default, executing before the component updates.

Can I stop a watcher manually?

Yes, both watch and watchEffect return a stop function that you can call to terminate the watcher and clean up its dependencies. For example: const stop = watchEffect(() => { ... }) followed by stop() when you no longer need the effect. This is essential for preventing memory leaks in dynamic components or when watchers are created conditionally.

Have a question about this repo?

These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →