0
# Watchers
1
2
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.
3
4
## Capabilities
5
6
### watch()
7
8
Watches one or more reactive data sources and invokes a callback function when the sources change.
9
10
```typescript { .api }
11
/**
12
* Watch reactive data sources and invoke callback on changes
13
* @param source - The reactive source(s) to watch
14
* @param cb - Callback function called when source changes
15
* @param options - Configuration options
16
* @returns WatchHandle with control methods
17
*/
18
function watch<T>(
19
source: WatchSource<T> | WatchSource<T>[] | WatchEffect | object,
20
cb?: WatchCallback<T> | null,
21
options?: WatchOptions
22
): WatchHandle;
23
24
type WatchSource<T = any> = Ref<T, any> | ComputedRef<T> | (() => T);
25
26
type WatchCallback<V = any, OV = any> = (
27
value: V,
28
oldValue: OV,
29
onCleanup: OnCleanup
30
) => any;
31
32
type OnCleanup = (cleanupFn: () => void) => void;
33
34
interface WatchHandle extends WatchStopHandle {
35
pause: () => void;
36
resume: () => void;
37
stop: () => void;
38
}
39
```
40
41
**Usage Examples:**
42
43
```typescript
44
import { ref, reactive, watch } from "@vue/reactivity";
45
46
// Watch a single ref
47
const count = ref(0);
48
49
const stopWatcher = watch(count, (newValue, oldValue) => {
50
console.log(`Count changed from ${oldValue} to ${newValue}`);
51
});
52
53
count.value = 1; // Logs: "Count changed from 0 to 1"
54
count.value = 2; // Logs: "Count changed from 1 to 2"
55
56
// Stop watching
57
stopWatcher.stop();
58
count.value = 5; // No log (watcher stopped)
59
60
// Watch reactive object property
61
const user = reactive({ name: "Alice", age: 25 });
62
63
watch(
64
() => user.name,
65
(newName, oldName) => {
66
console.log(`User name changed from ${oldName} to ${newName}`);
67
}
68
);
69
70
user.name = "Bob"; // Logs: "User name changed from Alice to Bob"
71
72
// Watch entire reactive object
73
watch(
74
user,
75
(newUser, oldUser) => {
76
console.log("User object changed:", newUser);
77
},
78
{ deep: true }
79
);
80
81
user.age = 26; // Triggers watcher with deep option
82
```
83
84
### Watch Multiple Sources
85
86
Watch multiple reactive sources simultaneously:
87
88
```typescript
89
import { ref, watch } from "@vue/reactivity";
90
91
const firstName = ref("John");
92
const lastName = ref("Doe");
93
94
// Watch multiple sources
95
watch(
96
[firstName, lastName],
97
([newFirst, newLast], [oldFirst, oldLast]) => {
98
console.log(
99
`Name changed from "${oldFirst} ${oldLast}" to "${newFirst} ${newLast}"`
100
);
101
}
102
);
103
104
firstName.value = "Jane";
105
// Logs: 'Name changed from "John Doe" to "Jane Doe"'
106
107
lastName.value = "Smith";
108
// Logs: 'Name changed from "Jane Doe" to "Jane Smith"'
109
110
// Watch multiple with different source types
111
const count = ref(0);
112
const doubled = computed(() => count.value * 2);
113
const message = ref("Hello");
114
115
watch(
116
[count, doubled, message],
117
([newCount, newDoubled, newMessage], [oldCount, oldDoubled, oldMessage]) => {
118
console.log({
119
count: { old: oldCount, new: newCount },
120
doubled: { old: oldDoubled, new: newDoubled },
121
message: { old: oldMessage, new: newMessage }
122
});
123
}
124
);
125
```
126
127
### Watch Options
128
129
Configure watcher behavior with various options:
130
131
```typescript { .api }
132
interface WatchOptions<Immediate = boolean> extends DebuggerOptions {
133
immediate?: Immediate;
134
deep?: boolean | number;
135
once?: boolean;
136
scheduler?: WatchScheduler;
137
onWarn?: (msg: string, ...args: any[]) => void;
138
}
139
140
type WatchScheduler = (fn: () => void, isFirstRun: boolean) => void;
141
```
142
143
**Usage Examples:**
144
145
```typescript
146
import { ref, reactive, watch } from "@vue/reactivity";
147
148
const count = ref(0);
149
const user = reactive({ profile: { name: "Alice" } });
150
151
// Immediate execution
152
watch(
153
count,
154
(newValue, oldValue) => {
155
console.log(`Count: ${newValue} (was ${oldValue})`);
156
},
157
{ immediate: true }
158
);
159
// Immediately logs: "Count: 0 (was undefined)"
160
161
// Deep watching
162
watch(
163
user,
164
(newUser, oldUser) => {
165
console.log("Deep change detected in user");
166
},
167
{ deep: true }
168
);
169
170
user.profile.name = "Bob"; // Triggers watcher due to deep option
171
172
// Watch only once
173
watch(
174
count,
175
(newValue) => {
176
console.log(`First change to: ${newValue}`);
177
},
178
{ once: true }
179
);
180
181
count.value = 1; // Logs and then watcher is automatically stopped
182
count.value = 2; // No log (watcher stopped after first trigger)
183
184
// Custom scheduler
185
const updates: (() => void)[] = [];
186
187
watch(
188
count,
189
(newValue) => {
190
console.log(`Scheduled update: ${newValue}`);
191
},
192
{
193
scheduler: (fn) => {
194
updates.push(fn);
195
// Execute updates in next tick
196
Promise.resolve().then(() => {
197
const currentUpdates = updates.splice(0);
198
currentUpdates.forEach(update => update());
199
});
200
}
201
}
202
);
203
```
204
205
### Watch with Cleanup
206
207
Handle cleanup for async operations or subscriptions:
208
209
```typescript
210
import { ref, watch } from "@vue/reactivity";
211
212
const userId = ref(1);
213
214
watch(
215
userId,
216
async (newId, oldId, onCleanup) => {
217
// Setup abort controller for fetch
218
const controller = new AbortController();
219
220
// Register cleanup function
221
onCleanup(() => {
222
controller.abort();
223
console.log(`Cancelled request for user ${newId}`);
224
});
225
226
try {
227
const response = await fetch(`/api/users/${newId}`, {
228
signal: controller.signal
229
});
230
const user = await response.json();
231
console.log("Loaded user:", user);
232
} catch (error) {
233
if (error.name !== "AbortError") {
234
console.error("Failed to load user:", error);
235
}
236
}
237
}
238
);
239
240
// Changing userId cancels previous request
241
userId.value = 2; // Logs: "Cancelled request for user 2"
242
userId.value = 3; // Logs: "Cancelled request for user 3"
243
244
// Timer cleanup example
245
const interval = ref(1000);
246
247
watch(interval, (newInterval, oldInterval, onCleanup) => {
248
const timer = setInterval(() => {
249
console.log(`Timer tick (${newInterval}ms)`);
250
}, newInterval);
251
252
onCleanup(() => {
253
clearInterval(timer);
254
console.log(`Cleared ${newInterval}ms timer`);
255
});
256
});
257
```
258
259
### getCurrentWatcher()
260
261
Get the current active watcher context:
262
263
```typescript { .api }
264
/**
265
* Returns the current active watcher effect if there is one
266
* @returns Current active watcher or undefined
267
*/
268
function getCurrentWatcher(): ReactiveEffect<any> | undefined;
269
```
270
271
**Usage Examples:**
272
273
```typescript
274
import { ref, watch, getCurrentWatcher } from "@vue/reactivity";
275
276
const count = ref(0);
277
278
watch(count, () => {
279
const currentWatcher = getCurrentWatcher();
280
if (currentWatcher) {
281
console.log("Inside watcher context");
282
console.log("Watcher flags:", currentWatcher.flags);
283
}
284
});
285
286
count.value = 1; // Logs watcher context info
287
```
288
289
### onWatcherCleanup()
290
291
Register cleanup functions from within watchers:
292
293
```typescript { .api }
294
/**
295
* Register cleanup callback on the current active watcher
296
* @param cleanupFn - Cleanup function to register
297
* @param failSilently - If true, won't warn when no active watcher
298
* @param owner - The effect to attach cleanup to
299
*/
300
function onWatcherCleanup(
301
cleanupFn: () => void,
302
failSilently?: boolean,
303
owner?: ReactiveEffect | undefined
304
): void;
305
```
306
307
**Usage Examples:**
308
309
```typescript
310
import { ref, watch, onWatcherCleanup } from "@vue/reactivity";
311
312
const searchTerm = ref("");
313
314
watch(searchTerm, (term) => {
315
if (!term) return;
316
317
const controller = new AbortController();
318
319
// Register cleanup using onWatcherCleanup
320
onWatcherCleanup(() => {
321
controller.abort();
322
console.log(`Search cancelled for: ${term}`);
323
});
324
325
// Perform search
326
fetch(`/api/search?q=${term}`, { signal: controller.signal })
327
.then(response => response.json())
328
.then(results => console.log("Results:", results))
329
.catch(error => {
330
if (error.name !== "AbortError") {
331
console.error("Search failed:", error);
332
}
333
});
334
});
335
```
336
337
### traverse()
338
339
Deeply traverse an object for dependency tracking:
340
341
```typescript { .api }
342
/**
343
* Deeply traverses an object for dependency tracking
344
* @param value - The value to traverse
345
* @param depth - Maximum traversal depth
346
* @param seen - Map to avoid circular references
347
* @returns The traversed value
348
*/
349
function traverse(
350
value: unknown,
351
depth?: number,
352
seen?: Map<unknown, number>
353
): unknown;
354
```
355
356
**Usage Examples:**
357
358
```typescript
359
import { reactive, watch, traverse } from "@vue/reactivity";
360
361
const deepObject = reactive({
362
level1: {
363
level2: {
364
level3: {
365
value: "deep"
366
}
367
}
368
}
369
});
370
371
// Custom deep watching with traverse
372
watch(
373
() => traverse(deepObject, 2), // Limit depth to 2 levels
374
() => {
375
console.log("Object changed (depth 2)");
376
}
377
);
378
379
deepObject.level1.level2.value = "changed"; // Triggers watcher
380
deepObject.level1.level2.level3.value = "deep change"; // Doesn't trigger (depth > 2)
381
```
382
383
### Watch Error Handling
384
385
Handle errors in watchers with proper error codes:
386
387
```typescript { .api }
388
enum WatchErrorCodes {
389
WATCH_GETTER = 2,
390
WATCH_CALLBACK,
391
WATCH_CLEANUP
392
}
393
```
394
395
**Usage Examples:**
396
397
```typescript
398
import { ref, watch } from "@vue/reactivity";
399
400
const count = ref(0);
401
402
// Error handling in watcher callback
403
watch(
404
count,
405
(newValue) => {
406
try {
407
if (newValue < 0) {
408
throw new Error("Count cannot be negative");
409
}
410
console.log("Valid count:", newValue);
411
} catch (error) {
412
console.error("Watcher error:", error.message);
413
}
414
}
415
);
416
417
count.value = 5; // Logs: "Valid count: 5"
418
count.value = -1; // Logs: "Watcher error: Count cannot be negative"
419
420
// Error handling in getter
421
watch(
422
() => {
423
if (count.value > 100) {
424
throw new Error("Count too high");
425
}
426
return count.value;
427
},
428
(newValue) => {
429
console.log("Count within limits:", newValue);
430
},
431
{
432
onWarn: (msg, ...args) => {
433
console.warn("Watch warning:", msg, ...args);
434
}
435
}
436
);
437
```
438
439
### Advanced Watch Patterns
440
441
#### Debounced Watcher
442
443
```typescript
444
import { ref, watch } from "@vue/reactivity";
445
446
const searchTerm = ref("");
447
448
function debouncedWatch<T>(
449
source: () => T,
450
callback: (value: T) => void,
451
delay: number = 300
452
) {
453
let timeoutId: number;
454
455
return watch(
456
source,
457
(newValue) => {
458
clearTimeout(timeoutId);
459
timeoutId = setTimeout(() => {
460
callback(newValue);
461
}, delay);
462
}
463
);
464
}
465
466
// Debounced search
467
debouncedWatch(
468
() => searchTerm.value,
469
(term) => {
470
console.log("Searching for:", term);
471
// Perform search...
472
},
473
500
474
);
475
476
searchTerm.value = "vue"; // Wait 500ms
477
searchTerm.value = "vuejs"; // Cancels previous, wait 500ms
478
```
479
480
#### Conditional Watcher
481
482
```typescript
483
import { ref, watch, computed } from "@vue/reactivity";
484
485
const isEnabled = ref(false);
486
const count = ref(0);
487
488
// Watch that can be enabled/disabled
489
const conditionalWatcher = computed(() => {
490
if (!isEnabled.value) return null;
491
return count.value;
492
});
493
494
watch(conditionalWatcher, (newValue, oldValue) => {
495
if (newValue !== null) {
496
console.log(`Count changed to: ${newValue}`);
497
}
498
});
499
500
count.value = 1; // No log (not enabled)
501
isEnabled.value = true;
502
count.value = 2; // Logs: "Count changed to: 2"
503
isEnabled.value = false;
504
count.value = 3; // No log (disabled)
505
```
506
507
## Types
508
509
```typescript { .api }
510
// Core watch types
511
type WatchSource<T = any> = Ref<T, any> | ComputedRef<T> | (() => T);
512
513
type WatchCallback<V = any, OV = any> = (
514
value: V,
515
oldValue: OV,
516
onCleanup: OnCleanup
517
) => any;
518
519
type OnCleanup = (cleanupFn: () => void) => void;
520
521
type WatchEffect = (onCleanup: OnCleanup) => void;
522
523
// Watch handles
524
interface WatchStopHandle {
525
(): void;
526
}
527
528
interface WatchHandle extends WatchStopHandle {
529
pause: () => void;
530
resume: () => void;
531
stop: () => void;
532
}
533
534
// Watch options
535
interface WatchOptions<Immediate = boolean> extends DebuggerOptions {
536
immediate?: Immediate;
537
deep?: boolean | number;
538
once?: boolean;
539
scheduler?: WatchScheduler;
540
onWarn?: (msg: string, ...args: any[]) => void;
541
}
542
543
type WatchScheduler = (fn: () => void, isFirstRun: boolean) => void;
544
545
// Error codes
546
enum WatchErrorCodes {
547
WATCH_GETTER = 2,
548
WATCH_CALLBACK,
549
WATCH_CLEANUP
550
}
551
552
// Internal types
553
interface BaseWatchOptions<Immediate = boolean> extends DebuggerOptions {
554
immediate?: Immediate;
555
deep?: boolean | number;
556
once?: boolean;
557
scheduler?: WatchScheduler;
558
augmentJob?: (job: SchedulerJob) => void;
559
call?: (
560
fn: Function,
561
type: string,
562
args?: unknown[]
563
) => void;
564
}
565
566
interface SchedulerJob extends Function {
567
id?: number;
568
pre?: boolean;
569
active?: boolean;
570
computed?: boolean;
571
allowRecurse?: boolean;
572
ownerInstance?: any;
573
}
574
```