Vue.js's standalone reactivity system providing reactive references, objects, computed values, effects, and watchers with fine-grained dependency tracking.
—
Watchers provide a powerful way to observe reactive data changes and execute callbacks with access to both old and new values. They offer more control than effects for handling specific data changes.
Watches one or more reactive data sources and invokes a callback function when the sources change.
/**
* Watch reactive data sources and invoke callback on changes
* @param source - The reactive source(s) to watch
* @param cb - Callback function called when source changes
* @param options - Configuration options
* @returns WatchHandle with control methods
*/
function watch<T>(
source: WatchSource<T> | WatchSource<T>[] | WatchEffect | object,
cb?: WatchCallback<T> | null,
options?: WatchOptions
): WatchHandle;
type WatchSource<T = any> = Ref<T, any> | ComputedRef<T> | (() => T);
type WatchCallback<V = any, OV = any> = (
value: V,
oldValue: OV,
onCleanup: OnCleanup
) => any;
type OnCleanup = (cleanupFn: () => void) => void;
interface WatchHandle extends WatchStopHandle {
pause: () => void;
resume: () => void;
stop: () => void;
}Usage Examples:
import { ref, reactive, watch } from "@vue/reactivity";
// Watch a single ref
const count = ref(0);
const stopWatcher = watch(count, (newValue, oldValue) => {
console.log(`Count changed from ${oldValue} to ${newValue}`);
});
count.value = 1; // Logs: "Count changed from 0 to 1"
count.value = 2; // Logs: "Count changed from 1 to 2"
// Stop watching
stopWatcher.stop();
count.value = 5; // No log (watcher stopped)
// Watch reactive object property
const user = reactive({ name: "Alice", age: 25 });
watch(
() => user.name,
(newName, oldName) => {
console.log(`User name changed from ${oldName} to ${newName}`);
}
);
user.name = "Bob"; // Logs: "User name changed from Alice to Bob"
// Watch entire reactive object
watch(
user,
(newUser, oldUser) => {
console.log("User object changed:", newUser);
},
{ deep: true }
);
user.age = 26; // Triggers watcher with deep optionWatch multiple reactive sources simultaneously:
import { ref, watch } from "@vue/reactivity";
const firstName = ref("John");
const lastName = ref("Doe");
// Watch multiple sources
watch(
[firstName, lastName],
([newFirst, newLast], [oldFirst, oldLast]) => {
console.log(
`Name changed from "${oldFirst} ${oldLast}" to "${newFirst} ${newLast}"`
);
}
);
firstName.value = "Jane";
// Logs: 'Name changed from "John Doe" to "Jane Doe"'
lastName.value = "Smith";
// Logs: 'Name changed from "Jane Doe" to "Jane Smith"'
// Watch multiple with different source types
const count = ref(0);
const doubled = computed(() => count.value * 2);
const message = ref("Hello");
watch(
[count, doubled, message],
([newCount, newDoubled, newMessage], [oldCount, oldDoubled, oldMessage]) => {
console.log({
count: { old: oldCount, new: newCount },
doubled: { old: oldDoubled, new: newDoubled },
message: { old: oldMessage, new: newMessage }
});
}
);Configure watcher behavior with various options:
interface WatchOptions<Immediate = boolean> extends DebuggerOptions {
immediate?: Immediate;
deep?: boolean | number;
once?: boolean;
scheduler?: WatchScheduler;
onWarn?: (msg: string, ...args: any[]) => void;
}
type WatchScheduler = (fn: () => void, isFirstRun: boolean) => void;Usage Examples:
import { ref, reactive, watch } from "@vue/reactivity";
const count = ref(0);
const user = reactive({ profile: { name: "Alice" } });
// Immediate execution
watch(
count,
(newValue, oldValue) => {
console.log(`Count: ${newValue} (was ${oldValue})`);
},
{ immediate: true }
);
// Immediately logs: "Count: 0 (was undefined)"
// Deep watching
watch(
user,
(newUser, oldUser) => {
console.log("Deep change detected in user");
},
{ deep: true }
);
user.profile.name = "Bob"; // Triggers watcher due to deep option
// Watch only once
watch(
count,
(newValue) => {
console.log(`First change to: ${newValue}`);
},
{ once: true }
);
count.value = 1; // Logs and then watcher is automatically stopped
count.value = 2; // No log (watcher stopped after first trigger)
// Custom scheduler
const updates: (() => void)[] = [];
watch(
count,
(newValue) => {
console.log(`Scheduled update: ${newValue}`);
},
{
scheduler: (fn) => {
updates.push(fn);
// Execute updates in next tick
Promise.resolve().then(() => {
const currentUpdates = updates.splice(0);
currentUpdates.forEach(update => update());
});
}
}
);Handle cleanup for async operations or subscriptions:
import { ref, watch } from "@vue/reactivity";
const userId = ref(1);
watch(
userId,
async (newId, oldId, onCleanup) => {
// Setup abort controller for fetch
const controller = new AbortController();
// Register cleanup function
onCleanup(() => {
controller.abort();
console.log(`Cancelled request for user ${newId}`);
});
try {
const response = await fetch(`/api/users/${newId}`, {
signal: controller.signal
});
const user = await response.json();
console.log("Loaded user:", user);
} catch (error) {
if (error.name !== "AbortError") {
console.error("Failed to load user:", error);
}
}
}
);
// Changing userId cancels previous request
userId.value = 2; // Logs: "Cancelled request for user 2"
userId.value = 3; // Logs: "Cancelled request for user 3"
// Timer cleanup example
const interval = ref(1000);
watch(interval, (newInterval, oldInterval, onCleanup) => {
const timer = setInterval(() => {
console.log(`Timer tick (${newInterval}ms)`);
}, newInterval);
onCleanup(() => {
clearInterval(timer);
console.log(`Cleared ${newInterval}ms timer`);
});
});Get the current active watcher context:
/**
* Returns the current active watcher effect if there is one
* @returns Current active watcher or undefined
*/
function getCurrentWatcher(): ReactiveEffect<any> | undefined;Usage Examples:
import { ref, watch, getCurrentWatcher } from "@vue/reactivity";
const count = ref(0);
watch(count, () => {
const currentWatcher = getCurrentWatcher();
if (currentWatcher) {
console.log("Inside watcher context");
console.log("Watcher flags:", currentWatcher.flags);
}
});
count.value = 1; // Logs watcher context infoRegister cleanup functions from within watchers:
/**
* Register cleanup callback on the current active watcher
* @param cleanupFn - Cleanup function to register
* @param failSilently - If true, won't warn when no active watcher
* @param owner - The effect to attach cleanup to
*/
function onWatcherCleanup(
cleanupFn: () => void,
failSilently?: boolean,
owner?: ReactiveEffect | undefined
): void;Usage Examples:
import { ref, watch, onWatcherCleanup } from "@vue/reactivity";
const searchTerm = ref("");
watch(searchTerm, (term) => {
if (!term) return;
const controller = new AbortController();
// Register cleanup using onWatcherCleanup
onWatcherCleanup(() => {
controller.abort();
console.log(`Search cancelled for: ${term}`);
});
// Perform search
fetch(`/api/search?q=${term}`, { signal: controller.signal })
.then(response => response.json())
.then(results => console.log("Results:", results))
.catch(error => {
if (error.name !== "AbortError") {
console.error("Search failed:", error);
}
});
});Deeply traverse an object for dependency tracking:
/**
* Deeply traverses an object for dependency tracking
* @param value - The value to traverse
* @param depth - Maximum traversal depth
* @param seen - Map to avoid circular references
* @returns The traversed value
*/
function traverse(
value: unknown,
depth?: number,
seen?: Map<unknown, number>
): unknown;Usage Examples:
import { reactive, watch, traverse } from "@vue/reactivity";
const deepObject = reactive({
level1: {
level2: {
level3: {
value: "deep"
}
}
}
});
// Custom deep watching with traverse
watch(
() => traverse(deepObject, 2), // Limit depth to 2 levels
() => {
console.log("Object changed (depth 2)");
}
);
deepObject.level1.level2.value = "changed"; // Triggers watcher
deepObject.level1.level2.level3.value = "deep change"; // Doesn't trigger (depth > 2)Handle errors in watchers with proper error codes:
enum WatchErrorCodes {
WATCH_GETTER = 2,
WATCH_CALLBACK,
WATCH_CLEANUP
}Usage Examples:
import { ref, watch } from "@vue/reactivity";
const count = ref(0);
// Error handling in watcher callback
watch(
count,
(newValue) => {
try {
if (newValue < 0) {
throw new Error("Count cannot be negative");
}
console.log("Valid count:", newValue);
} catch (error) {
console.error("Watcher error:", error.message);
}
}
);
count.value = 5; // Logs: "Valid count: 5"
count.value = -1; // Logs: "Watcher error: Count cannot be negative"
// Error handling in getter
watch(
() => {
if (count.value > 100) {
throw new Error("Count too high");
}
return count.value;
},
(newValue) => {
console.log("Count within limits:", newValue);
},
{
onWarn: (msg, ...args) => {
console.warn("Watch warning:", msg, ...args);
}
}
);import { ref, watch } from "@vue/reactivity";
const searchTerm = ref("");
function debouncedWatch<T>(
source: () => T,
callback: (value: T) => void,
delay: number = 300
) {
let timeoutId: number;
return watch(
source,
(newValue) => {
clearTimeout(timeoutId);
timeoutId = setTimeout(() => {
callback(newValue);
}, delay);
}
);
}
// Debounced search
debouncedWatch(
() => searchTerm.value,
(term) => {
console.log("Searching for:", term);
// Perform search...
},
500
);
searchTerm.value = "vue"; // Wait 500ms
searchTerm.value = "vuejs"; // Cancels previous, wait 500msimport { ref, watch, computed } from "@vue/reactivity";
const isEnabled = ref(false);
const count = ref(0);
// Watch that can be enabled/disabled
const conditionalWatcher = computed(() => {
if (!isEnabled.value) return null;
return count.value;
});
watch(conditionalWatcher, (newValue, oldValue) => {
if (newValue !== null) {
console.log(`Count changed to: ${newValue}`);
}
});
count.value = 1; // No log (not enabled)
isEnabled.value = true;
count.value = 2; // Logs: "Count changed to: 2"
isEnabled.value = false;
count.value = 3; // No log (disabled)// Core watch types
type WatchSource<T = any> = Ref<T, any> | ComputedRef<T> | (() => T);
type WatchCallback<V = any, OV = any> = (
value: V,
oldValue: OV,
onCleanup: OnCleanup
) => any;
type OnCleanup = (cleanupFn: () => void) => void;
type WatchEffect = (onCleanup: OnCleanup) => void;
// Watch handles
interface WatchStopHandle {
(): void;
}
interface WatchHandle extends WatchStopHandle {
pause: () => void;
resume: () => void;
stop: () => void;
}
// Watch options
interface WatchOptions<Immediate = boolean> extends DebuggerOptions {
immediate?: Immediate;
deep?: boolean | number;
once?: boolean;
scheduler?: WatchScheduler;
onWarn?: (msg: string, ...args: any[]) => void;
}
type WatchScheduler = (fn: () => void, isFirstRun: boolean) => void;
// Error codes
enum WatchErrorCodes {
WATCH_GETTER = 2,
WATCH_CALLBACK,
WATCH_CLEANUP
}
// Internal types
interface BaseWatchOptions<Immediate = boolean> extends DebuggerOptions {
immediate?: Immediate;
deep?: boolean | number;
once?: boolean;
scheduler?: WatchScheduler;
augmentJob?: (job: SchedulerJob) => void;
call?: (
fn: Function,
type: string,
args?: unknown[]
) => void;
}
interface SchedulerJob extends Function {
id?: number;
pre?: boolean;
active?: boolean;
computed?: boolean;
allowRecurse?: boolean;
ownerInstance?: any;
}Install with Tessl CLI
npx tessl i tessl/npm-vue--reactivity