Hone logo
Hone
Problems

Building a Vue-Inspired Reactivity System in TypeScript

Vue.js is renowned for its elegant and efficient reactivity system, which automatically updates the UI when the underlying data changes. This challenge aims to replicate the core functionality of such a system using TypeScript, allowing you to gain a deeper understanding of how this powerful feature works under the hood.

Problem Description

Your task is to create a simplified reactivity system in TypeScript that mimics the fundamental behavior of Vue's reactivity. This system should allow you to define reactive data properties and automatically trigger callbacks (effects) whenever these properties change.

Key Requirements:

  1. reactive function: Create a function reactive(obj) that takes a plain JavaScript object and returns a reactive proxy of that object.
  2. track function: Implement a mechanism to track which effects are reading which properties.
  3. trigger function: Implement a mechanism to trigger effects when a property is written to.
  4. effect function: Create a function effect(callback) that takes a callback function. This callback should be executed immediately, and any reactive properties accessed within it should be "tracked." The callback should then be re-executed whenever any of the tracked properties change.
  5. Handling nested objects: The reactive function should recursively make nested objects reactive.
  6. Handling array modifications: The system should also support reactivity for array operations (e.g., push, pop, splice).

Expected Behavior:

When a reactive property is accessed within an effect's callback, that access should be recorded. When that same reactive property is later modified, the effect's callback should be re-executed.

Edge Cases:

  • Modifying a property to its current value should not trigger an effect re-execution.
  • Accessing properties that don't exist on the initial object should be handled gracefully (though for this challenge, assume initial properties exist or are added later).
  • Ensure that nested objects within the reactive object are also reactive.

Examples

Example 1: Basic Property Update

// Assume reactive, track, trigger, effect are defined

const state = reactive({ count: 0 });

effect(() => {
  console.log(`Count is: ${state.count}`);
});

state.count++; // This should trigger the effect, logging "Count is: 1"
state.count++; // This should trigger the effect again, logging "Count is: 2"

Output:

Count is: 0
Count is: 1
Count is: 2

Explanation:

The initial effect runs, logging Count is: 0. When state.count is incremented, the trigger mechanism detects the change and re-executes the effect's callback, logging the new value.

Example 2: Nested Object and Multiple Properties

// Assume reactive, track, trigger, effect are defined

const user = reactive({
  name: "Alice",
  address: {
    city: "Wonderland",
    zip: "12345"
  }
});

effect(() => {
  console.log(`User: ${user.name}, City: ${user.address.city}`);
});

user.name = "Bob"; // Triggers effect
user.address.city = "Oz"; // Triggers effect
user.address.zip = "67890"; // Does NOT trigger effect (zip is not tracked in the current effect)

Output:

User: Alice, City: Wonderland
User: Bob, City: Wonderland
User: Bob, City: Oz

Explanation:

The initial effect logs "User: Alice, City: Wonderland". When user.name changes, the effect re-runs. When user.address.city changes, the effect re-runs again. user.address.zip changes, but since it wasn't accessed in the effect's callback, no re-execution occurs.

Example 3: Array Modification

// Assume reactive, track, trigger, effect are defined

const list = reactive({ items: ["a", "b"] });

effect(() => {
  console.log(`Items: ${list.items.join(", ")} (length: ${list.items.length})`);
});

list.items.push("c"); // Triggers effect
list.items.pop(); // Triggers effect

Output:

Items: a, b (length: 2)
Items: a, b, c (length: 3)
Items: a, b (length: 2)

Explanation:

The initial effect logs the starting list. Pushing "c" makes the list reactive, triggering the effect. Popping "c" again makes the list reactive, triggering the effect once more.

Constraints

  • The reactive, track, trigger, and effect functions must be implemented in TypeScript.
  • The system should handle primitive types (strings, numbers, booleans) and nested objects for reactivity.
  • Array modifications like push, pop, splice, length changes should be supported.
  • The performance should be reasonably efficient for typical use cases, avoiding unnecessary re-executions of effects.

Notes

  • You will likely need to use Proxy objects to intercept property access and modification.
  • Consider how to store and manage the dependencies between effects and reactive properties. A common pattern involves using a Map to store dependencies.
  • Think about how to handle the initial run of an effect and subsequent updates.
  • For array reactivity, you might need to intercept specific array methods.
Loading editor...
typescript