0
# Effects System
1
2
The effects system provides automatic dependency tracking and side effect execution. Effects automatically re-run when their reactive dependencies change, making it easy to create responsive applications.
3
4
## Capabilities
5
6
### effect()
7
8
Creates a reactive effect that automatically tracks dependencies and re-runs when they change. Returns a runner function that can manually trigger the effect.
9
10
```typescript { .api }
11
/**
12
* Creates a reactive effect that tracks dependencies and re-runs on changes
13
* @param fn - The effect function to run
14
* @param options - Configuration options
15
* @returns A runner function that can manually trigger the effect
16
*/
17
function effect<T = any>(
18
fn: () => T,
19
options?: ReactiveEffectOptions
20
): ReactiveEffectRunner<T>;
21
22
interface ReactiveEffectRunner<T = any> {
23
(): T;
24
effect: ReactiveEffect;
25
}
26
27
interface ReactiveEffectOptions extends DebuggerOptions {
28
scheduler?: EffectScheduler;
29
allowRecurse?: boolean;
30
onStop?: () => void;
31
}
32
33
type EffectScheduler = (fn: () => void) => void;
34
```
35
36
**Usage Examples:**
37
38
```typescript
39
import { ref, reactive, effect } from "@vue/reactivity";
40
41
// Basic effect
42
const count = ref(0);
43
44
const runner = effect(() => {
45
console.log(`Count is: ${count.value}`);
46
});
47
// Immediately logs: "Count is: 0"
48
49
count.value = 1; // Logs: "Count is: 1"
50
count.value = 2; // Logs: "Count is: 2"
51
52
// Manual execution
53
runner(); // Logs: "Count is: 2"
54
55
// Effect with reactive object
56
const state = reactive({ message: "Hello", count: 0 });
57
58
effect(() => {
59
console.log(`${state.message} - Count: ${state.count}`);
60
});
61
// Logs: "Hello - Count: 0"
62
63
state.message = "Hi"; // Logs: "Hi - Count: 0"
64
state.count = 5; // Logs: "Hi - Count: 5"
65
66
// Effect with return value
67
const user = ref({ name: "Alice", age: 25 });
68
69
const logUser = effect(() => {
70
const currentUser = user.value;
71
console.log(`User: ${currentUser.name}, Age: ${currentUser.age}`);
72
return currentUser;
73
});
74
75
user.value = { name: "Bob", age: 30 }; // Logs and returns new user
76
```
77
78
### Effect with Custom Scheduler
79
80
Control when effects run by providing a custom scheduler:
81
82
```typescript
83
import { ref, effect } from "@vue/reactivity";
84
85
const count = ref(0);
86
const updates: (() => void)[] = [];
87
88
// Effect with custom scheduler
89
effect(
90
() => {
91
console.log(`Count: ${count.value}`);
92
},
93
{
94
scheduler: (fn) => {
95
// Batch updates instead of running immediately
96
updates.push(fn);
97
}
98
}
99
);
100
101
// Changes are scheduled, not executed immediately
102
count.value = 1; // No immediate log
103
count.value = 2; // No immediate log
104
count.value = 3; // No immediate log
105
106
// Execute batched updates
107
updates.forEach(fn => fn());
108
// Logs: "Count: 3" (only once, with final value)
109
110
// Async scheduler example
111
const asyncUpdates: (() => void)[] = [];
112
113
effect(
114
() => {
115
console.log(`Async count: ${count.value}`);
116
},
117
{
118
scheduler: (fn) => {
119
asyncUpdates.push(fn);
120
Promise.resolve().then(() => {
121
const updates = asyncUpdates.splice(0);
122
updates.forEach(update => update());
123
});
124
}
125
}
126
);
127
```
128
129
### stop()
130
131
Stops a reactive effect, preventing it from running when dependencies change.
132
133
```typescript { .api }
134
/**
135
* Stops a reactive effect
136
* @param runner - The effect runner returned by effect()
137
*/
138
function stop(runner: ReactiveEffectRunner): void;
139
```
140
141
**Usage Examples:**
142
143
```typescript
144
import { ref, effect, stop } from "@vue/reactivity";
145
146
const count = ref(0);
147
148
const runner = effect(() => {
149
console.log(`Count: ${count.value}`);
150
});
151
// Logs: "Count: 0"
152
153
count.value = 1; // Logs: "Count: 1"
154
155
// Stop the effect
156
stop(runner);
157
158
count.value = 2; // No log (effect stopped)
159
count.value = 3; // No log (effect stopped)
160
161
// Stopped effects can still be manually executed
162
runner(); // Logs: "Count: 3"
163
```
164
165
### Tracking Control
166
167
Control dependency tracking behavior with tracking functions:
168
169
```typescript { .api }
170
/**
171
* Temporarily pauses dependency tracking
172
*/
173
function pauseTracking(): void;
174
175
/**
176
* Re-enables effect tracking (if it was paused)
177
*/
178
function enableTracking(): void;
179
180
/**
181
* Resets the previous global effect tracking state
182
*/
183
function resetTracking(): void;
184
```
185
186
**Usage Examples:**
187
188
```typescript
189
import { ref, effect, pauseTracking, enableTracking, resetTracking } from "@vue/reactivity";
190
191
const count = ref(0);
192
const name = ref("Alice");
193
194
effect(() => {
195
console.log(`Count: ${count.value}`);
196
197
// Pause tracking temporarily
198
pauseTracking();
199
200
// This access won't be tracked
201
console.log(`Name: ${name.value}`);
202
203
// Re-enable tracking
204
enableTracking();
205
});
206
207
count.value = 1; // Logs both count and name
208
name.value = "Bob"; // Does NOT trigger effect (wasn't tracked)
209
210
// Reset tracking state
211
resetTracking();
212
```
213
214
### onEffectCleanup()
215
216
Register cleanup functions that run before an effect re-runs or when it's stopped.
217
218
```typescript { .api }
219
/**
220
* Registers a cleanup function for the current active effect
221
* @param fn - Cleanup function to register
222
* @param failSilently - If true, won't warn when no active effect
223
*/
224
function onEffectCleanup(fn: () => void, failSilently?: boolean): void;
225
```
226
227
**Usage Examples:**
228
229
```typescript
230
import { ref, effect, onEffectCleanup, stop } from "@vue/reactivity";
231
232
const url = ref("https://api.example.com/users");
233
234
const runner = effect(() => {
235
const controller = new AbortController();
236
237
// Register cleanup to cancel request
238
onEffectCleanup(() => {
239
controller.abort();
240
console.log("Request cancelled");
241
});
242
243
// Make API request
244
fetch(url.value, { signal: controller.signal })
245
.then(response => response.json())
246
.then(data => console.log("Data:", data))
247
.catch(error => {
248
if (error.name !== "AbortError") {
249
console.error("Fetch error:", error);
250
}
251
});
252
});
253
254
// Changing URL cancels previous request and starts new one
255
url.value = "https://api.example.com/posts"; // Logs: "Request cancelled"
256
257
// Stopping effect also runs cleanup
258
stop(runner); // Logs: "Request cancelled"
259
260
// Timer cleanup example
261
const interval = ref(1000);
262
263
effect(() => {
264
const timer = setInterval(() => {
265
console.log(`Timer tick (${interval.value}ms)`);
266
}, interval.value);
267
268
onEffectCleanup(() => {
269
clearInterval(timer);
270
console.log("Timer cleared");
271
});
272
});
273
274
interval.value = 500; // Clears old timer, starts new one
275
```
276
277
### ReactiveEffect Class
278
279
The low-level class that powers the effect system. Usually not used directly but available for advanced use cases.
280
281
```typescript { .api }
282
/**
283
* Low-level reactive effect class
284
*/
285
class ReactiveEffect<T = any> implements Subscriber, ReactiveEffectOptions {
286
constructor(public fn: () => T);
287
288
/**
289
* Pause the effect (stop tracking dependencies)
290
*/
291
pause(): void;
292
293
/**
294
* Resume the effect (start tracking dependencies)
295
*/
296
resume(): void;
297
298
/**
299
* Run the effect function and track dependencies
300
*/
301
run(): T;
302
303
/**
304
* Stop the effect permanently
305
*/
306
stop(): void;
307
308
/**
309
* Manually trigger the effect
310
*/
311
trigger(): void;
312
313
/**
314
* Run the effect only if it's dirty (dependencies changed)
315
*/
316
runIfDirty(): void;
317
318
/**
319
* Check if the effect needs to re-run
320
*/
321
get dirty(): boolean;
322
}
323
```
324
325
**Usage Examples:**
326
327
```typescript
328
import { ref, ReactiveEffect } from "@vue/reactivity";
329
330
const count = ref(0);
331
332
// Create effect manually
333
const effectInstance = new ReactiveEffect(() => {
334
console.log(`Manual effect: ${count.value}`);
335
});
336
337
// Must manually run to start tracking
338
effectInstance.run(); // Logs: "Manual effect: 0"
339
340
count.value = 1; // Triggers effect: "Manual effect: 1"
341
342
// Pause and resume
343
effectInstance.pause();
344
count.value = 2; // No log (paused)
345
346
effectInstance.resume();
347
count.value = 3; // Logs: "Manual effect: 3"
348
349
// Check if dirty
350
console.log(effectInstance.dirty); // false (just ran)
351
count.value = 4;
352
console.log(effectInstance.dirty); // true (needs to re-run)
353
354
effectInstance.runIfDirty(); // Logs: "Manual effect: 4"
355
```
356
357
### Effect with Allow Recurse
358
359
Control whether effects can trigger themselves recursively:
360
361
```typescript
362
import { ref, effect } from "@vue/reactivity";
363
364
const count = ref(0);
365
366
// Effect that modifies its own dependency
367
effect(
368
() => {
369
console.log(`Count: ${count.value}`);
370
371
if (count.value < 5) {
372
count.value++; // This would normally cause infinite recursion
373
}
374
},
375
{
376
allowRecurse: true // Allow recursive triggering
377
}
378
);
379
380
// This will log:
381
// "Count: 0"
382
// "Count: 1"
383
// "Count: 2"
384
// "Count: 3"
385
// "Count: 4"
386
// "Count: 5"
387
```
388
389
### Effect Debugging
390
391
Use debug options to understand effect behavior:
392
393
```typescript
394
import { ref, effect } from "@vue/reactivity";
395
396
const count = ref(0);
397
const name = ref("Alice");
398
399
effect(
400
() => {
401
console.log(`${name.value}: ${count.value}`);
402
},
403
{
404
onTrack: (event) => {
405
console.log("Tracked:", event.key, "on", event.target);
406
},
407
onTrigger: (event) => {
408
console.log("Triggered by:", event.key, "->", event.newValue);
409
}
410
}
411
);
412
413
// Logs tracking of both dependencies
414
count.value = 1; // Logs trigger event and effect
415
name.value = "Bob"; // Logs trigger event and effect
416
```
417
418
## Types
419
420
```typescript { .api }
421
// Core effect types
422
interface ReactiveEffectRunner<T = any> {
423
(): T;
424
effect: ReactiveEffect;
425
}
426
427
interface ReactiveEffectOptions extends DebuggerOptions {
428
scheduler?: EffectScheduler;
429
allowRecurse?: boolean;
430
onStop?: () => void;
431
}
432
433
type EffectScheduler = (fn: () => void) => void;
434
435
// Effect flags for internal state management
436
enum EffectFlags {
437
ACTIVE = 1 << 0, // Effect is active
438
RUNNING = 1 << 1, // Effect is currently running
439
TRACKING = 1 << 2, // Effect is tracking dependencies
440
NOTIFIED = 1 << 3, // Effect has been notified of changes
441
DIRTY = 1 << 4, // Effect needs to re-run
442
ALLOW_RECURSE = 1 << 5, // Effect can trigger itself
443
PAUSED = 1 << 6, // Effect is paused
444
EVALUATED = 1 << 7 // Effect has been evaluated
445
}
446
447
// Debug interfaces
448
interface DebuggerOptions {
449
onTrack?: (event: DebuggerEvent) => void;
450
onTrigger?: (event: DebuggerEvent) => void;
451
}
452
453
interface DebuggerEvent {
454
effect: ReactiveEffect;
455
target: object;
456
type: TrackOpTypes | TriggerOpTypes;
457
key: any;
458
newValue?: any;
459
oldValue?: any;
460
oldTarget?: Map<any, any> | Set<any>;
461
}
462
463
interface DebuggerEventExtraInfo {
464
target: object;
465
type: TrackOpTypes | TriggerOpTypes;
466
key: any;
467
newValue?: any;
468
oldValue?: any;
469
oldTarget?: Map<any, any> | Set<any>;
470
}
471
472
// Subscriber interface (internal)
473
interface Subscriber {
474
deps: Link[];
475
flags: EffectFlags;
476
notify(): void;
477
}
478
```
479
480
## Advanced Patterns
481
482
### Conditional Effects
483
484
Create effects that only track certain dependencies based on conditions:
485
486
```typescript
487
import { ref, effect, pauseTracking, enableTracking } from "@vue/reactivity";
488
489
const mode = ref<"simple" | "advanced">("simple");
490
const basicCount = ref(0);
491
const advancedCount = ref(0);
492
493
effect(() => {
494
console.log(`Mode: ${mode.value}`); // Always tracked
495
496
if (mode.value === "simple") {
497
console.log(`Basic: ${basicCount.value}`); // Tracked only in simple mode
498
499
pauseTracking();
500
console.log(`Advanced (not tracked): ${advancedCount.value}`);
501
enableTracking();
502
} else {
503
pauseTracking();
504
console.log(`Basic (not tracked): ${basicCount.value}`);
505
enableTracking();
506
507
console.log(`Advanced: ${advancedCount.value}`); // Tracked only in advanced mode
508
}
509
});
510
511
mode.value = "advanced"; // Switches tracking
512
```
513
514
### Effect Composition
515
516
Combine multiple effects for complex reactive logic:
517
518
```typescript
519
import { ref, effect, computed } from "@vue/reactivity";
520
521
const items = ref<{ id: number; completed: boolean }[]>([]);
522
const filter = ref<"all" | "completed" | "pending">("all");
523
524
// Base effect for logging changes
525
effect(() => {
526
console.log(`Items updated: ${items.value.length} total`);
527
});
528
529
// Computed for filtered items
530
const filteredItems = computed(() => {
531
switch (filter.value) {
532
case "completed":
533
return items.value.filter(item => item.completed);
534
case "pending":
535
return items.value.filter(item => !item.completed);
536
default:
537
return items.value;
538
}
539
});
540
541
// Effect that depends on filtered items
542
effect(() => {
543
console.log(`Showing ${filteredItems.value.length} items (${filter.value})`);
544
});
545
546
// Updates trigger both effects appropriately
547
items.value = [
548
{ id: 1, completed: false },
549
{ id: 2, completed: true }
550
];
551
552
filter.value = "completed"; // Only triggers filter effect
553
```