Hone logo
Hone
Problems

Mastering Vue 3 Effect Scopes: A Reactive Cleanup Challenge

Vue 3's Composition API introduces effectScope for managing reactive effects. This challenge will test your understanding of creating, nesting, and properly cleaning up effect scopes, a crucial skill for preventing memory leaks and ensuring predictable behavior in complex Vue applications.

Problem Description

Your task is to implement a mechanism that utilizes Vue 3's effectScope to manage a set of reactive computations. You'll need to:

  1. Create a parent effectScope: This scope will encapsulate multiple child effects.
  2. Define and register child effects: These will be reactive computations (e.g., using computed or watch) that depend on some reactive state.
  3. Implement cleanup: Ensure that when the parent scope is disposed, all its child effects are also automatically cleaned up, preventing any lingering reactive dependencies.
  4. Demonstrate nesting (optional but recommended): Show how to create nested effect scopes within the parent scope, ensuring that disposing the parent also cleans up nested scopes.

The goal is to build a small, isolated system that highlights the lifecycle and management capabilities of effectScope.

Examples

Example 1: Basic Scope Creation and Cleanup

Imagine a simple scenario where you want to track a computed value and clean it up when it's no longer needed.

import { ref, computed, effectScope } from 'vue';

// --- Your implementation here ---
function createManagedReactiveCounter() {
  const count = ref(0);
  const increment = () => { count.value++; };

  const scope = effectScope();

  scope.run(() => {
    const doubleCount = computed(() => count.value * 2);
    console.log(`Initial doubleCount: ${doubleCount.value}`); // Logs 0

    // Simulate some interaction
    increment();
    console.log(`After increment, doubleCount: ${doubleCount.value}`); // Logs 2

    // A watcher to demonstrate another effect
    const stopWatcher = watch(count, (newVal) => {
      console.log(`Count changed to: ${newVal}`);
    });

    return {
      doubleCount,
      stopWatcher,
      increment
    };
  });

  return {
    scope,
    counterState: scope.run(() => ref(0)) // Another simple state managed by scope
  };
}

// --- Usage ---
const { scope, counterState } = createManagedReactiveCounter();

console.log('Counter state:', counterState.value); // Logs 0

// Simulate some time passing, then cleanup
setTimeout(() => {
  console.log('Disposing scope...');
  scope.stop();
  console.log('Scope disposed.');

  // Attempting to access reactive properties after stop should yield no updates or errors
  counterState.value = 10; // This won't trigger a reactive update if the effect was properly stopped
  console.log('Counter state after dispose:', counterState.value); // Still logs 0 (or initial value if not set again)
}, 100);

Expected Output (approximately, timing might vary slightly):

Initial doubleCount: 0
After increment, doubleCount: 2
Count changed to: 1
Counter state: 0
Disposing scope...
Scope disposed.
Counter state after dispose: 10

Explanation:

The createManagedReactiveCounter function sets up a parent effectScope. Inside scope.run(), a computed property (doubleCount) and a watch effect are created. When scope.stop() is called, both doubleCount and the watcher are automatically disposed of. Accessing counterState directly after scope.stop() might still allow setting its value, but any effects that were reacting to it within the disposed scope will no longer function.

Example 2: Nested Effect Scopes

This example demonstrates how nested scopes are also cleaned up when the parent is stopped.

import { ref, computed, effectScope, watch } from 'vue';

function createNestedScopes() {
  const baseValue = ref(5);

  const parentScope = effectScope();

  parentScope.run(() => {
    const parentComputed = computed(() => baseValue.value * 10);
    console.log('Parent computed:', parentComputed.value); // Logs 50

    const childScope = effectScope();

    childScope.run(() => {
      const childComputed = computed(() => parentComputed.value + 5);
      console.log('Child computed:', childComputed.value); // Logs 55

      watch(childComputed, (newVal) => {
        console.log(`Child computed changed to: ${newVal}`);
      });
    });

    // Return child scope for potential manual control, though parent stop handles cleanup
    return { childScope, parentComputed };
  });

  return { parentScope, baseValue };
}

// --- Usage ---
const { parentScope, baseValue } = createNestedScopes();

console.log('--- Initial state ---');
// Accessing values to ensure they've been computed
parentScope.run(() => {}); // Reruns the effects within parentScope to get logs if not already triggered

console.log('--- Updating baseValue ---');
baseValue.value = 10; // Should trigger updates in parentComputed and childComputed

console.log('--- Disposing parent scope ---');
parentScope.stop();
console.log('Parent scope disposed.');

console.log('--- Attempting to update baseValue after dispose ---');
baseValue.value = 15; // This change should not trigger any more logs from the disposed effects.

Expected Output (approximately):

Parent computed: 50
Child computed: 55
--- Initial state ---
--- Updating baseValue ---
Child computed changed to: 105
--- Disposing parent scope ---
Parent scope disposed.
--- Attempting to update baseValue after dispose ---

Explanation:

When parentScope.stop() is called, not only are the effects directly within parentScope.run() stopped, but childScope and all its effects are also automatically disposed of. Updates to baseValue after parentScope.stop() do not trigger any further logged output from the previously active effects.

Constraints

  • You must use Vue 3's effectScope API.
  • Your solution should be written in TypeScript.
  • The primary focus is on demonstrating correct scope creation, effect registration, and cleanup.
  • Performance is not a critical bottleneck for this challenge, but avoid intentionally inefficient patterns.
  • Assume a Vue 3 environment is available (you can simulate this for testing purposes if needed).

Notes

  • Remember that effectScope.run() executes a function within the scope. Any reactive effects created inside this function (like ref, computed, watch) will be automatically associated with that scope.
  • When scope.stop() is called, all effects associated with that scope are deactivated.
  • Consider how you might structure your code to return the effectScope instance so it can be stopped externally.
  • Think about what happens if you have effects that depend on other reactive sources outside their immediate scope. effectScope manages the lifecycle of the effect itself, not necessarily the lifecycle of the data it depends on.
Loading editor...
typescript