Appearance
Vue Watcher Class
A stateful wrapper around Vue's watch() that exposes explicit start() and stop() methods. Provides programmatic control over watcher lifecycle instead of relying solely on component unmount to clean up.
When to use
Use this class when you need to pause and resume a watcher programmatically — for example, disabling auto-validation while resetting form data. It is also useful for managing watchers in class-based reactive objects outside of <script setup> where Vue's automatic cleanup does not apply.
The pattern
Create a Watcher with a source function, a handler, and optional overrides. The watcher starts immediately by default and can be stopped and restarted at any time.
Basic usage
Create a watcher that logs changes to a reactive property. Stop and restart it to control when the handler fires.
javascript
import { reactive } from "vue";
import { Watcher } from "./watcher.js";
const state = reactive({ count: 0 });
const watcher = new Watcher(
() => state.count,
(newVal, oldVal) => {
console.log(`${oldVal} → ${newVal}`);
},
{ immediate: false },
);
state.count++; // logs: "0 → 1"
watcher.stop();
state.count++; // no log
watcher.start();
state.count++; // logs: "2 → 3"Pause during bulk updates
Stop the watcher before bulk mutations to avoid firing the handler for each intermediate state, then restart to get a single handler call for the final state.
javascript
watcher.stop();
state.items = await fetchItems();
state.filters = defaultFilters;
watcher.start();
// Handler fires once for the new state,
// not once per property changeImplementing the Watcher class
The Watcher class stores the source function, handler, and options. start() creates the Vue watcher; stop() calls the returned unwatch handle.
javascript
import assert from "assert";
import { markRaw, watch } from "vue";
export class Watcher {
static get defaults() {
return { deep: true, immediate: true };
}
#unwatch = null;
constructor(source, handler, options = {}) {
// Prevent Vue from wrapping this instance in a reactive
// proxy, which would break access to #private fields.
markRaw(this);
assert(typeof source === "function",
new TypeError("source must be a function"));
assert(typeof handler === "function",
new TypeError("handler must be a function"));
this.#source = source;
this.#handler = handler;
this.options = {
...Watcher.defaults,
...options,
};
this.start();
}
#source;
#handler;
start() {
this.stop();
this.#unwatch = watch(this.#source, this.#handler, this.options);
}
stop() {
this.#unwatch?.();
this.#unwatch = null;
}
}Safe restarts with start()
start() calls stop() first, preventing duplicate watchers. Calling start() multiple times is safe because the previous watcher is always cleaned up before creating a new one.
Deep and immediate defaults
The defaults (deep: true, immediate: true) match the most common use case: watching a reactive object and running the handler immediately with the current value. Override these in the options argument when different behavior is needed.
The #unwatch handle
Vue's watch() returns a function that stops the watcher. The class stores it and calls it in stop(). Setting it to null after calling prevents double-stop issues.
Used by FormModel
The useForm composable creates a Watcher to auto-validate form data on changes. The watcher persists across reset() calls because it's created with ??= (only on first init).
Trade-offs
- Not a composable — This is a plain class, not tied to the component lifecycle. It won't auto-stop on component unmount. If used inside a component, call
watcher.stop()inonUnmounted()or use Vue'sonScopeDispose. - Deep watching overhead —
deep: trueby default means Vue traverses the entire object tree on every change. For large, deeply nested objects this can be expensive. Pass{ deep: false }and watch specific properties when performance matters. - No
watchEffectequivalent — This class wrapswatch(), notwatchEffect(). For auto-tracked dependencies without an explicit source, use Vue'swatchEffectdirectly.