0
# Effect Scopes
1
2
Effect scopes provide a way to group and manage reactive effects for organized cleanup and lifecycle management. They enable batch disposal of effects and nested scope hierarchies.
3
4
## Capabilities
5
6
### effectScope()
7
8
Creates an effect scope object that can capture reactive effects created within it for later disposal.
9
10
```typescript { .api }
11
/**
12
* Creates an effect scope for capturing and managing effects
13
* @param detached - Whether to create a detached scope (not connected to parent)
14
* @returns New EffectScope instance
15
*/
16
function effectScope(detached?: boolean): EffectScope;
17
18
class EffectScope {
19
constructor(public detached?: boolean);
20
21
/**
22
* Whether the scope is currently active
23
*/
24
get active(): boolean;
25
26
/**
27
* Pause all effects in this scope
28
*/
29
pause(): void;
30
31
/**
32
* Resume all effects in this scope
33
*/
34
resume(): void;
35
36
/**
37
* Run a function within this scope, capturing any effects created
38
*/
39
run<T>(fn: () => T): T | undefined;
40
41
/**
42
* Stop all effects in this scope and dispose resources
43
*/
44
stop(fromParent?: boolean): void;
45
}
46
```
47
48
**Usage Examples:**
49
50
```typescript
51
import { ref, effect, effectScope } from "@vue/reactivity";
52
53
const count = ref(0);
54
const name = ref("Alice");
55
56
// Create an effect scope
57
const scope = effectScope();
58
59
scope.run(() => {
60
// Effects created within run() are captured by the scope
61
effect(() => {
62
console.log(`Count: ${count.value}`);
63
});
64
65
effect(() => {
66
console.log(`Name: ${name.value}`);
67
});
68
69
// Nested scopes are also captured
70
const nestedScope = effectScope();
71
nestedScope.run(() => {
72
effect(() => {
73
console.log(`Nested: ${count.value} - ${name.value}`);
74
});
75
});
76
});
77
78
// All effects run initially
79
count.value = 1; // Triggers all 3 effects
80
name.value = "Bob"; // Triggers name and nested effects
81
82
// Stop all effects in the scope at once
83
scope.stop();
84
85
count.value = 2; // No effects run (all stopped)
86
name.value = "Charlie"; // No effects run (all stopped)
87
```
88
89
### Detached Scopes
90
91
Create scopes that are independent of parent scopes:
92
93
```typescript
94
import { ref, effect, effectScope } from "@vue/reactivity";
95
96
const count = ref(0);
97
98
const parentScope = effectScope();
99
100
parentScope.run(() => {
101
effect(() => {
102
console.log(`Parent effect: ${count.value}`);
103
});
104
105
// Detached scope won't be stopped when parent stops
106
const detachedScope = effectScope(true); // detached = true
107
108
detachedScope.run(() => {
109
effect(() => {
110
console.log(`Detached effect: ${count.value}`);
111
});
112
});
113
114
return detachedScope; // Keep reference to manage separately
115
});
116
117
count.value = 1; // Both effects run
118
119
// Stop parent scope
120
parentScope.stop();
121
122
count.value = 2; // Only detached effect runs
123
124
// Must stop detached scope separately
125
// detachedScope.stop();
126
```
127
128
### getCurrentScope()
129
130
Get the currently active effect scope:
131
132
```typescript { .api }
133
/**
134
* Returns the current active effect scope if there is one
135
* @returns Current active EffectScope or undefined
136
*/
137
function getCurrentScope(): EffectScope | undefined;
138
```
139
140
**Usage Examples:**
141
142
```typescript
143
import { effectScope, getCurrentScope, effect } from "@vue/reactivity";
144
145
const scope = effectScope();
146
147
scope.run(() => {
148
console.log("Current scope:", getCurrentScope()); // Logs the scope instance
149
150
effect(() => {
151
const currentScope = getCurrentScope();
152
console.log("Effect is running in scope:", currentScope === scope);
153
});
154
});
155
156
// Outside of scope
157
console.log("Outside scope:", getCurrentScope()); // undefined
158
```
159
160
### onScopeDispose()
161
162
Register callbacks that run when the scope is disposed:
163
164
```typescript { .api }
165
/**
166
* Register a dispose callback on the current active effect scope
167
* @param fn - Callback function to run when scope is disposed
168
* @param failSilently - If true, won't warn when no active scope
169
*/
170
function onScopeDispose(fn: () => void, failSilently?: boolean): void;
171
```
172
173
**Usage Examples:**
174
175
```typescript
176
import { ref, effect, effectScope, onScopeDispose } from "@vue/reactivity";
177
178
const count = ref(0);
179
180
const scope = effectScope();
181
182
scope.run(() => {
183
// Register cleanup for the entire scope
184
onScopeDispose(() => {
185
console.log("Scope is being disposed");
186
});
187
188
// Setup resources that need cleanup
189
const timer = setInterval(() => {
190
console.log(`Timer tick: ${count.value}`);
191
}, 1000);
192
193
// Register cleanup for the timer
194
onScopeDispose(() => {
195
clearInterval(timer);
196
console.log("Timer cleaned up");
197
});
198
199
// Effect that uses the count
200
effect(() => {
201
console.log(`Effect: ${count.value}`);
202
});
203
204
// Register effect-specific cleanup
205
onScopeDispose(() => {
206
console.log("Effect cleanup");
207
});
208
});
209
210
// Let it run for a bit
211
setTimeout(() => {
212
scope.stop(); // Triggers all cleanup callbacks
213
}, 5000);
214
```
215
216
### Nested Scopes
217
218
Create hierarchical scope structures for complex applications:
219
220
```typescript
221
import { ref, effect, effectScope, onScopeDispose } from "@vue/reactivity";
222
223
const globalCount = ref(0);
224
225
// Application-level scope
226
const appScope = effectScope();
227
228
appScope.run(() => {
229
console.log("App scope initialized");
230
231
onScopeDispose(() => {
232
console.log("App scope disposed");
233
});
234
235
// Feature-level scope
236
const featureScope = effectScope();
237
238
featureScope.run(() => {
239
console.log("Feature scope initialized");
240
241
onScopeDispose(() => {
242
console.log("Feature scope disposed");
243
});
244
245
// Component-level scope
246
const componentScope = effectScope();
247
248
componentScope.run(() => {
249
console.log("Component scope initialized");
250
251
onScopeDispose(() => {
252
console.log("Component scope disposed");
253
});
254
255
effect(() => {
256
console.log(`Component watching: ${globalCount.value}`);
257
});
258
});
259
260
effect(() => {
261
console.log(`Feature watching: ${globalCount.value}`);
262
});
263
});
264
265
effect(() => {
266
console.log(`App watching: ${globalCount.value}`);
267
});
268
});
269
270
globalCount.value = 1; // All effects run
271
272
// Stopping app scope stops all nested scopes
273
appScope.stop();
274
// Logs:
275
// "Component scope disposed"
276
// "Feature scope disposed"
277
// "App scope disposed"
278
279
globalCount.value = 2; // No effects run
280
```
281
282
### Pause and Resume Scopes
283
284
Control scope execution without destroying effects:
285
286
```typescript
287
import { ref, effect, effectScope } from "@vue/reactivity";
288
289
const count = ref(0);
290
291
const scope = effectScope();
292
293
scope.run(() => {
294
effect(() => {
295
console.log(`Active effect: ${count.value}`);
296
});
297
298
effect(() => {
299
console.log(`Another effect: ${count.value * 2}`);
300
});
301
});
302
303
count.value = 1; // Both effects run
304
305
// Pause all effects in scope
306
scope.pause();
307
count.value = 2; // No effects run (paused)
308
309
// Resume all effects in scope
310
scope.resume();
311
count.value = 3; // Both effects run again
312
313
// Check if scope is active
314
console.log("Scope active:", scope.active); // true
315
316
scope.stop();
317
console.log("Scope active:", scope.active); // false
318
```
319
320
### Advanced Scope Patterns
321
322
#### Conditional Scope Management
323
324
```typescript
325
import { ref, computed, effectScope, effect } from "@vue/reactivity";
326
327
const isFeatureEnabled = ref(false);
328
const count = ref(0);
329
330
let featureScope: EffectScope | null = null;
331
332
// Watch for feature toggle
333
effect(() => {
334
if (isFeatureEnabled.value) {
335
// Create scope when feature is enabled
336
if (!featureScope) {
337
featureScope = effectScope();
338
339
featureScope.run(() => {
340
effect(() => {
341
console.log(`Feature active: ${count.value}`);
342
});
343
344
// More feature-specific effects...
345
});
346
}
347
} else {
348
// Clean up scope when feature is disabled
349
if (featureScope) {
350
featureScope.stop();
351
featureScope = null;
352
}
353
}
354
});
355
356
// Toggle feature
357
isFeatureEnabled.value = true; // Creates and starts feature effects
358
count.value = 1; // Feature effect runs
359
360
isFeatureEnabled.value = false; // Stops and cleans up feature effects
361
count.value = 2; // No feature effect runs
362
```
363
364
#### Resource Management with Scopes
365
366
```typescript
367
import { ref, effectScope, onScopeDispose } from "@vue/reactivity";
368
369
interface Resource {
370
id: string;
371
cleanup(): void;
372
}
373
374
function useResourceManager() {
375
const resources = new Map<string, Resource>();
376
377
const scope = effectScope();
378
379
const addResource = (id: string, resource: Resource) => {
380
resources.set(id, resource);
381
382
// Register cleanup for this resource
383
onScopeDispose(() => {
384
resource.cleanup();
385
resources.delete(id);
386
console.log(`Resource ${id} cleaned up`);
387
});
388
};
389
390
const removeResource = (id: string) => {
391
const resource = resources.get(id);
392
if (resource) {
393
resource.cleanup();
394
resources.delete(id);
395
}
396
};
397
398
const cleanup = () => {
399
scope.stop(); // Triggers cleanup of all resources
400
};
401
402
return scope.run(() => ({
403
addResource,
404
removeResource,
405
cleanup,
406
resourceCount: () => resources.size
407
}))!;
408
}
409
410
// Usage
411
const manager = useResourceManager();
412
413
// Add some resources
414
manager.addResource("timer1", {
415
id: "timer1",
416
cleanup: () => console.log("Timer 1 stopped")
417
});
418
419
manager.addResource("listener", {
420
id: "listener",
421
cleanup: () => console.log("Event listener removed")
422
});
423
424
console.log("Resource count:", manager.resourceCount()); // 2
425
426
// Clean up all resources at once
427
manager.cleanup();
428
// Logs:
429
// "Timer 1 stopped"
430
// "Event listener removed"
431
// "Resource timer1 cleaned up"
432
// "Resource listener cleaned up"
433
```
434
435
## Types
436
437
```typescript { .api }
438
// Core effect scope class
439
class EffectScope {
440
/**
441
* @param detached - If true, this scope's parent scope will not stop it
442
*/
443
constructor(public detached?: boolean);
444
445
/**
446
* Whether the scope is currently active
447
*/
448
get active(): boolean;
449
450
/**
451
* Pause all effects in this scope
452
*/
453
pause(): void;
454
455
/**
456
* Resume all effects in this scope
457
*/
458
resume(): void;
459
460
/**
461
* Run a function within this scope context
462
* @param fn - Function to run in scope
463
* @returns Function result or undefined if scope is inactive
464
*/
465
run<T>(fn: () => T): T | undefined;
466
467
/**
468
* Activate this scope (internal method)
469
*/
470
on(): void;
471
472
/**
473
* Deactivate this scope (internal method)
474
*/
475
off(): void;
476
477
/**
478
* Stop all effects in this scope and dispose resources
479
* @param fromParent - Whether stop was called from parent scope
480
*/
481
stop(fromParent?: boolean): void;
482
}
483
484
// Internal scope state
485
interface EffectScopeState {
486
parent: EffectScope | undefined;
487
scopes: EffectScope[] | undefined;
488
effects: ReactiveEffect[] | undefined;
489
cleanups: (() => void)[] | undefined;
490
index: number;
491
active: boolean;
492
}
493
```
494
495
## Scope Lifecycle
496
497
Understanding the effect scope lifecycle is important for proper resource management:
498
499
1. **Creation**: `effectScope()` creates a new inactive scope
500
2. **Activation**: `scope.run()` activates the scope and captures effects
501
3. **Collection**: All effects and nested scopes created during `run()` are captured
502
4. **Execution**: Effects run normally while scope is active
503
5. **Pause/Resume**: `pause()` and `resume()` control effect execution without disposal
504
6. **Disposal**: `stop()` permanently deactivates scope and cleans up all resources
505
506
```typescript
507
import { effectScope, effect, onScopeDispose } from "@vue/reactivity";
508
509
const scope = effectScope();
510
console.log("1. Scope created, active:", scope.active); // false
511
512
const result = scope.run(() => {
513
console.log("2. Inside run(), active:", scope.active); // true
514
515
effect(() => {
516
console.log("3. Effect created and captured");
517
});
518
519
onScopeDispose(() => {
520
console.log("5. Cleanup executed");
521
});
522
523
return "result";
524
});
525
526
console.log("4. Run completed, result:", result); // "result"
527
528
scope.stop();
529
// Logs: "5. Cleanup executed"
530
console.log("6. After stop, active:", scope.active); // false
531
```