Vue.js's standalone reactivity system providing reactive references, objects, computed values, effects, and watchers with fine-grained dependency tracking.
—
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.
Creates an effect scope object that can capture reactive effects created within it for later disposal.
/**
* Creates an effect scope for capturing and managing effects
* @param detached - Whether to create a detached scope (not connected to parent)
* @returns New EffectScope instance
*/
function effectScope(detached?: boolean): EffectScope;
class EffectScope {
constructor(public detached?: boolean);
/**
* Whether the scope is currently active
*/
get active(): boolean;
/**
* Pause all effects in this scope
*/
pause(): void;
/**
* Resume all effects in this scope
*/
resume(): void;
/**
* Run a function within this scope, capturing any effects created
*/
run<T>(fn: () => T): T | undefined;
/**
* Stop all effects in this scope and dispose resources
*/
stop(fromParent?: boolean): void;
}Usage Examples:
import { ref, effect, effectScope } from "@vue/reactivity";
const count = ref(0);
const name = ref("Alice");
// Create an effect scope
const scope = effectScope();
scope.run(() => {
// Effects created within run() are captured by the scope
effect(() => {
console.log(`Count: ${count.value}`);
});
effect(() => {
console.log(`Name: ${name.value}`);
});
// Nested scopes are also captured
const nestedScope = effectScope();
nestedScope.run(() => {
effect(() => {
console.log(`Nested: ${count.value} - ${name.value}`);
});
});
});
// All effects run initially
count.value = 1; // Triggers all 3 effects
name.value = "Bob"; // Triggers name and nested effects
// Stop all effects in the scope at once
scope.stop();
count.value = 2; // No effects run (all stopped)
name.value = "Charlie"; // No effects run (all stopped)Create scopes that are independent of parent scopes:
import { ref, effect, effectScope } from "@vue/reactivity";
const count = ref(0);
const parentScope = effectScope();
parentScope.run(() => {
effect(() => {
console.log(`Parent effect: ${count.value}`);
});
// Detached scope won't be stopped when parent stops
const detachedScope = effectScope(true); // detached = true
detachedScope.run(() => {
effect(() => {
console.log(`Detached effect: ${count.value}`);
});
});
return detachedScope; // Keep reference to manage separately
});
count.value = 1; // Both effects run
// Stop parent scope
parentScope.stop();
count.value = 2; // Only detached effect runs
// Must stop detached scope separately
// detachedScope.stop();Get the currently active effect scope:
/**
* Returns the current active effect scope if there is one
* @returns Current active EffectScope or undefined
*/
function getCurrentScope(): EffectScope | undefined;Usage Examples:
import { effectScope, getCurrentScope, effect } from "@vue/reactivity";
const scope = effectScope();
scope.run(() => {
console.log("Current scope:", getCurrentScope()); // Logs the scope instance
effect(() => {
const currentScope = getCurrentScope();
console.log("Effect is running in scope:", currentScope === scope);
});
});
// Outside of scope
console.log("Outside scope:", getCurrentScope()); // undefinedRegister callbacks that run when the scope is disposed:
/**
* Register a dispose callback on the current active effect scope
* @param fn - Callback function to run when scope is disposed
* @param failSilently - If true, won't warn when no active scope
*/
function onScopeDispose(fn: () => void, failSilently?: boolean): void;Usage Examples:
import { ref, effect, effectScope, onScopeDispose } from "@vue/reactivity";
const count = ref(0);
const scope = effectScope();
scope.run(() => {
// Register cleanup for the entire scope
onScopeDispose(() => {
console.log("Scope is being disposed");
});
// Setup resources that need cleanup
const timer = setInterval(() => {
console.log(`Timer tick: ${count.value}`);
}, 1000);
// Register cleanup for the timer
onScopeDispose(() => {
clearInterval(timer);
console.log("Timer cleaned up");
});
// Effect that uses the count
effect(() => {
console.log(`Effect: ${count.value}`);
});
// Register effect-specific cleanup
onScopeDispose(() => {
console.log("Effect cleanup");
});
});
// Let it run for a bit
setTimeout(() => {
scope.stop(); // Triggers all cleanup callbacks
}, 5000);Create hierarchical scope structures for complex applications:
import { ref, effect, effectScope, onScopeDispose } from "@vue/reactivity";
const globalCount = ref(0);
// Application-level scope
const appScope = effectScope();
appScope.run(() => {
console.log("App scope initialized");
onScopeDispose(() => {
console.log("App scope disposed");
});
// Feature-level scope
const featureScope = effectScope();
featureScope.run(() => {
console.log("Feature scope initialized");
onScopeDispose(() => {
console.log("Feature scope disposed");
});
// Component-level scope
const componentScope = effectScope();
componentScope.run(() => {
console.log("Component scope initialized");
onScopeDispose(() => {
console.log("Component scope disposed");
});
effect(() => {
console.log(`Component watching: ${globalCount.value}`);
});
});
effect(() => {
console.log(`Feature watching: ${globalCount.value}`);
});
});
effect(() => {
console.log(`App watching: ${globalCount.value}`);
});
});
globalCount.value = 1; // All effects run
// Stopping app scope stops all nested scopes
appScope.stop();
// Logs:
// "Component scope disposed"
// "Feature scope disposed"
// "App scope disposed"
globalCount.value = 2; // No effects runControl scope execution without destroying effects:
import { ref, effect, effectScope } from "@vue/reactivity";
const count = ref(0);
const scope = effectScope();
scope.run(() => {
effect(() => {
console.log(`Active effect: ${count.value}`);
});
effect(() => {
console.log(`Another effect: ${count.value * 2}`);
});
});
count.value = 1; // Both effects run
// Pause all effects in scope
scope.pause();
count.value = 2; // No effects run (paused)
// Resume all effects in scope
scope.resume();
count.value = 3; // Both effects run again
// Check if scope is active
console.log("Scope active:", scope.active); // true
scope.stop();
console.log("Scope active:", scope.active); // falseimport { ref, computed, effectScope, effect } from "@vue/reactivity";
const isFeatureEnabled = ref(false);
const count = ref(0);
let featureScope: EffectScope | null = null;
// Watch for feature toggle
effect(() => {
if (isFeatureEnabled.value) {
// Create scope when feature is enabled
if (!featureScope) {
featureScope = effectScope();
featureScope.run(() => {
effect(() => {
console.log(`Feature active: ${count.value}`);
});
// More feature-specific effects...
});
}
} else {
// Clean up scope when feature is disabled
if (featureScope) {
featureScope.stop();
featureScope = null;
}
}
});
// Toggle feature
isFeatureEnabled.value = true; // Creates and starts feature effects
count.value = 1; // Feature effect runs
isFeatureEnabled.value = false; // Stops and cleans up feature effects
count.value = 2; // No feature effect runsimport { ref, effectScope, onScopeDispose } from "@vue/reactivity";
interface Resource {
id: string;
cleanup(): void;
}
function useResourceManager() {
const resources = new Map<string, Resource>();
const scope = effectScope();
const addResource = (id: string, resource: Resource) => {
resources.set(id, resource);
// Register cleanup for this resource
onScopeDispose(() => {
resource.cleanup();
resources.delete(id);
console.log(`Resource ${id} cleaned up`);
});
};
const removeResource = (id: string) => {
const resource = resources.get(id);
if (resource) {
resource.cleanup();
resources.delete(id);
}
};
const cleanup = () => {
scope.stop(); // Triggers cleanup of all resources
};
return scope.run(() => ({
addResource,
removeResource,
cleanup,
resourceCount: () => resources.size
}))!;
}
// Usage
const manager = useResourceManager();
// Add some resources
manager.addResource("timer1", {
id: "timer1",
cleanup: () => console.log("Timer 1 stopped")
});
manager.addResource("listener", {
id: "listener",
cleanup: () => console.log("Event listener removed")
});
console.log("Resource count:", manager.resourceCount()); // 2
// Clean up all resources at once
manager.cleanup();
// Logs:
// "Timer 1 stopped"
// "Event listener removed"
// "Resource timer1 cleaned up"
// "Resource listener cleaned up"// Core effect scope class
class EffectScope {
/**
* @param detached - If true, this scope's parent scope will not stop it
*/
constructor(public detached?: boolean);
/**
* Whether the scope is currently active
*/
get active(): boolean;
/**
* Pause all effects in this scope
*/
pause(): void;
/**
* Resume all effects in this scope
*/
resume(): void;
/**
* Run a function within this scope context
* @param fn - Function to run in scope
* @returns Function result or undefined if scope is inactive
*/
run<T>(fn: () => T): T | undefined;
/**
* Activate this scope (internal method)
*/
on(): void;
/**
* Deactivate this scope (internal method)
*/
off(): void;
/**
* Stop all effects in this scope and dispose resources
* @param fromParent - Whether stop was called from parent scope
*/
stop(fromParent?: boolean): void;
}
// Internal scope state
interface EffectScopeState {
parent: EffectScope | undefined;
scopes: EffectScope[] | undefined;
effects: ReactiveEffect[] | undefined;
cleanups: (() => void)[] | undefined;
index: number;
active: boolean;
}Understanding the effect scope lifecycle is important for proper resource management:
effectScope() creates a new inactive scopescope.run() activates the scope and captures effectsrun() are capturedpause() and resume() control effect execution without disposalstop() permanently deactivates scope and cleans up all resourcesimport { effectScope, effect, onScopeDispose } from "@vue/reactivity";
const scope = effectScope();
console.log("1. Scope created, active:", scope.active); // false
const result = scope.run(() => {
console.log("2. Inside run(), active:", scope.active); // true
effect(() => {
console.log("3. Effect created and captured");
});
onScopeDispose(() => {
console.log("5. Cleanup executed");
});
return "result";
});
console.log("4. Run completed, result:", result); // "result"
scope.stop();
// Logs: "5. Cleanup executed"
console.log("6. After stop, active:", scope.active); // falseInstall with Tessl CLI
npx tessl i tessl/npm-vue--reactivity