CtrlK
BlogDocsLog inGet started
Tessl Logo

tessl/npm-vue--reactivity

Vue.js's standalone reactivity system providing reactive references, objects, computed values, effects, and watchers with fine-grained dependency tracking.

Pending
Overview
Eval results
Files

effect-scopes.mddocs/

Effect Scopes

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.

Capabilities

effectScope()

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)

Detached Scopes

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();

getCurrentScope()

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()); // undefined

onScopeDispose()

Register 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);

Nested Scopes

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 run

Pause and Resume Scopes

Control 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); // false

Advanced Scope Patterns

Conditional Scope Management

import { 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 runs

Resource Management with Scopes

import { 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"

Types

// 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;
}

Scope Lifecycle

Understanding the effect scope lifecycle is important for proper resource management:

  1. Creation: effectScope() creates a new inactive scope
  2. Activation: scope.run() activates the scope and captures effects
  3. Collection: All effects and nested scopes created during run() are captured
  4. Execution: Effects run normally while scope is active
  5. Pause/Resume: pause() and resume() control effect execution without disposal
  6. Disposal: stop() permanently deactivates scope and cleans up all resources
import { 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); // false

Install with Tessl CLI

npx tessl i tessl/npm-vue--reactivity

docs

computed.md

effect-scopes.md

effects.md

index.md

reactive-objects.md

refs.md

utilities.md

watchers.md

tile.json