Key Differences Between watch and watchEffect in Vue 3
Both watch and watchEffect are built on Vue 3's shared reactivity system, differing only in their dependency tracking behavior and usage syntax.
Reactivity Dependency Tracking
To understand the difference between the two APIs, it helps to first cover how Vue 3's reactivity system operates at a high level. Below is a simplified implementation of the ref utility to demonstrate core mechanics:
const ref = (initVal) => {
let internalVal = initVal
return {
get value() {
track(this, 'value') // Register access as a dependency
return internalVal
},
set value(newVal) {
internalVal = newVal
trigger(this, 'value') // Run all registered dependents
}
}
}
When a reactive property is accessed, the track method records the current running effect as a dependency of that property. When the property is updated, trigger executes all effects that depend on the property. All reactive utilities including computed, watch, and watchEffect are implemented on top of this shared effect system.
const orderCount = ref(3)
console.log(orderCount.value) // Calls track() for orderCount.value
orderCount.value = 5 // Calls trigger() for orderCount.value
To collect all dependencies for a given function, you can wrap its execution between tracking start and stop operations:
const orderCount = ref(3)
function printOrderTotal() {
console.log(`Current active orders: ${orderCount.value}`)
}
function gatherDependencies() {
activateTracking()
printOrderTotal() // All reactive values accessed here are recorded
deactivateTracking()
}
This process lets the reactivity system know that printOrderTotal should be rerun whenever orderCount updates.
watch API
The base watch API accepts two distinct arguments: a dependency getter function, and a callback that executes only when the value returned by the getter changes. Shorthand versions that accept a ref or reactive object directly are just syntax sugar over this base format.
watch(
// Dependency getter, runs once on initialization to collect dependencies
() => userProfile.value.age,
// Callback, runs exclusively when the getter's return value updates
(newAge, oldAge) => {
sendAgeVerificationRequest(newAge)
}
)
The dependency getter runs exactly once by default, and no dependency tracking is performed on the callback itself. This means the callback can reference values not declared in the getter without those values triggering runs of the callback.
watchEffect API
watchEffect merges the dependancy getter and callback into a single function. It automatically tracks all reactive values accessed inside its callback, and reruns the entire function whenever any of those dependencies update. Unlike watch, watchEffect executes immediately on initialization, equivalent to passing the { immediate: true } option to watch.
The two snippets below have nearly identical behavior:
const clickCount = ref(0)
watchEffect(() => {
console.log(`Total button clicks: ${clickCount.value}`)
})
// Equivalent watch implementation
watch(
() => clickCount.value,
() => {
console.log(`Total button clicks: ${clickCount.value}`)
},
{ immediate: true }
)
A unique trait of watchEffect is that it re-collects dependencies on every run, so dynamically added dependencies are automatically registered:
const clickCount = ref(0)
const isLoggingEnabled = ref(false)
watchEffect(() => {
if (isLoggingEnabled.value) {
console.log(`Current click total: ${clickCount.value}`)
}
})
// Initial run: isLoggingEnabled is false, only isLoggingEnabled is marked as a dependency
clickCount.value += 1 // No output, clickCount is not yet tracked
isLoggingEnabled.value = true // Triggers effect, outputs "Current click total: 1"
clickCount.value += 1 // clickCount is now tracked, outputs "Current click total: 2"
isLoggingEnabled.value = false // Triggers effect, no output
clickCount.value += 1 // Still triggers effect, even though clickCount is no longer used in the conditional
computed can be viewed as a synchronous variant of watchEffect that includes caching for its return value.
Prefer watch for most use cases, as explicit dependency declaration prevents unintended reruns and reduces the risk of accidentally introducing new dependenceis during code refactoring. Use watchEffect for simple logic where the callback is tightly coupled to all the reactive values it uses, or when you want to avoid manually listing all required dependencies.