Back to News & Insights
JavaScript October 12, 2026 · 12 min read

Vue watch vs watchEffect: The Complete Guide

Vue's watch() and watchEffect() look interchangeable but track dependencies, run timing, and cleanup differently. The exact rules, verified against Vue 3.5.

Vue watch vs watchEffect: The Complete Guide

You type "vue" into a search box, one letter at a time. Four requests go out — for v, vu, vue, and the deleted-and-retyped vue again — and the one for v happens to come back last. The screen ends up showing results for a single letter, on top of a watch() callback that looks completely correct. Nothing in that callback is wrong. The watcher just never learned that a newer run had already made it obsolete.

This is the article that fixes that, and along the way answers the question every Vue developer eventually asks: when do I reach for watch(), when do I reach for watchEffect(), and why does my callback keep firing at the wrong moment?

By the end of this article you'll be able to: Explain the real difference between watch() and watchEffect() — not "one is explicit, one is automatic," but what that difference costs you Pick the correct flush timing (pre, post, sync) for a watcher that needs to read the DOM Use onWatcherCleanup to cancel a stale async response before it overwrites a newer one Stop a watcher manually, and know when Vue stops it for you Recognize the specific ways both APIs quietly track (or fail to track) the wrong things

You should already be comfortable with ref(), reactive(), and computed() in the Composition API. If "why does destructuring a reactive() object lose reactivity" isn't yet second nature, read Vue Reactivity Explained: ref vs reactive first — this article assumes that mental model and builds on it.

This is written against Vue 3.5.x (verified September 2026). Version-specific notes (the once option, onWatcherCleanup) are called out where they apply.

Table of contents The problem: a watcher with no memory The mental model: pull vs. push tracking Stage 1: the smallest watch() Stage 2: the same job with watchEffect() Stage 3: multiple sources, and watching inside an object Stage 4: immediate, deep, and once Stage 5: flush timing — pre, post, sync Stage 6: cleanup and the race condition, fixed Stopping a watcher Edge cases and gotchas Best practices FAQ Cheat sheet Key takeaways

Here's the search box, written the way it's tempting to write it the first time:

Every keystroke fires a new request, and requests do not resolve in the order they were sent. If the request for v is slow and the request for vue is fast, vue's results land first and v's results land after, silently overwriting them. The callback did exactly what it was written to do; it just never asked "am I still the most recent run?"

That question is exactly what a watcher's cleanup mechanism answers. Getting there first requires being precise about what watch() and watchEffect() actually are.

The mental model: watch() is told what to watch. watchEffect() discovers what to watch by running once and recording every reactive value it reads. Everything else — old/new values, laziness, immediacy — falls out of that one distinction. watch(source, callback) takes an explicit source (a ref, a getter, a reactive() object, or an array of them) and a callback. It does not run on creation by default. It runs only when the named source changes, and it hands the callback both the new and the old value. watchEffect(fn) takes a single function with no separate source. It runs immediately, once, synchronously (well — see flush timing below) to figure out what it read, and re-runs itself in full whenever any of those dependencies change. There is no "old value" concept, because there's no fixed source to diff against — the whole function reruns.

Put differently: watch is a subscription to a specific address. watchEffect is a subscription to whatever addresses this function happens to visit today — and it can visit different addresses on different runs, exactly like a computed() getter does. The dependency-tracking mechanism (a run records every reactive value it reads) is shared across computed, watch's getter-form sources, and watchEffect alike.

Key concept: the callback did not run when watch was declared — only after count changed. That's the "lazy by default" behavior, and it's the first thing that trips people up coming from watchEffect.

Key concept: watchEffect ran once immediately, on creation, before anything changed — because it has to run once just to discover that count is a dependency. watch never needed to run to know its source; you named it.

If the job is "sync this state to something external, right now and on every future change" — a document title, a localStorage key, an analytics call — watchEffect is usually the shorter answer. Need the old value, or need to stay silent until one specific thing changes? Reach for watch.

watch accepts an array of sources, and hands the callback matching arrays of new/old values:

Watching a single property of a reactive() object requires a getter function, not the bare property — because reactive().someProp is just a plain value the instant you read it out of the object, with no reference back to the proxy:

This is the same "reactivity is a subscription made at read time" rule from the ref vs reactive article — watch needs something it can re-read, and a getter is how you hand it that.

Three options on watch change when and how often it fires (watchEffect accepts only flush, covered next, plus the dev-only onTrack/onTrigger — it runs on creation and tracks everything it reads, so immediate, deep and once don't apply):

Want to discuss this further?

Book a free strategy call with our team to see how these insights apply to your specific business goals.

Book a consultation