Hone logo
Hone
Problems

Implement Vue's shallowRef Functionality in TypeScript

This challenge requires you to recreate the core functionality of Vue's shallowRef composable. Understanding shallowRef is crucial for optimizing performance in large applications by controlling how Vue tracks reactivity. Your task is to implement a TypeScript function that behaves like shallowRef, creating a ref that only tracks its top-level property for reactivity.

Problem Description

You need to implement a TypeScript function named shallowRef that mimics the behavior of Vue's built-in shallowRef. This function should accept an initial value and return a ref object. The key characteristic of a shallowRef is that its reactivity is "shallow." This means that if the initial value is an object or an array, shallowRef will only track changes to the ref's .value property itself, not to the properties within that object or array.

Key Requirements:

  1. Function Signature: The function must be named shallowRef and accept one argument: initialValue of any type. It should return a ref object.
  2. Ref Object Structure: The returned ref object must have at least a .value property, which holds the current value. It should also have a mechanism to trigger updates when .value is reassigned.
  3. Shallow Reactivity:
    • If the initialValue is a primitive type (string, number, boolean, null, undefined, symbol, bigint), reactivity should work as expected, similar to Vue's ref.
    • If the initialValue is an object or array, changes to the properties within that object or array should not trigger reactivity updates on their own. Only reassigning the entire .value property should trigger an update.
  4. Reactivity Triggering: The implementation should include a way to signal to a "rendering" mechanism (which we'll simulate) that a change has occurred, specifically when .value is reassigned.

Expected Behavior:

  • Creating a shallowRef with a primitive value should allow changes to that primitive to be reactive.
  • Creating a shallowRef with an object:
    • Reassigning the entire .value property to a new object or primitive should trigger an update.
    • Mutating properties of the object assigned to .value should not trigger an update.
  • Creating a shallowRef with an array:
    • Reassigning the entire .value property to a new array or primitive should trigger an update.
    • Mutating the array (e.g., pushing elements, changing elements by index) should not trigger an update.

Edge Cases:

  • Handling null and undefined as initial values.
  • Handling non-plain objects (e.g., Dates, RegExps) – these should be treated like any other object.

Examples

Example 1: Primitive Value

// Simulate a dependency tracking mechanism
let effectCallback: (() => void) | null = null;
function trackEffect(callback: () => void) {
  effectCallback = callback;
}
function triggerUpdate() {
  if (effectCallback) {
    effectCallback();
  }
}

// --- Your shallowRef implementation would be here ---

const count = shallowRef(0);

// Simulate an effect that depends on 'count.value'
trackEffect(() => {
  console.log('Count updated:', count.value);
});

console.log('Initial count:', count.value); // Output: Initial count: 0

count.value = 10; // Should trigger an update
// Expected output: Count updated: 10
console.log('After reassignment:', count.value); // Output: After reassignment: 10

count.value++; // Should trigger an update
// Expected output: Count updated: 11
console.log('After increment:', count.value); // Output: After increment: 11

Explanation: Primitive values are tracked at the .value level. Reassigning count.value to a new number directly triggers the simulated triggerUpdate and thus the effectCallback.

Example 2: Object Value (Shallow Tracking)

// Use the same trackEffect and triggerUpdate functions from Example 1

const person = shallowRef({ name: 'Alice', age: 30 });

// Simulate an effect that depends on 'person.value'
trackEffect(() => {
  console.log('Person updated:', person.value);
});

console.log('Initial person:', person.value); // Output: Initial person: { name: 'Alice', age: 30 }

// Mutate a property within the object - THIS SHOULD NOT TRIGGER an update
person.value.age = 31;
console.log('After mutation:', person.value); // Output: After mutation: { name: 'Alice', age: 31 }
// Expected output: (No "Person updated" log here)

// Reassign the entire .value property - THIS SHOULD TRIGGER an update
person.value = { name: 'Bob', age: 25 };
// Expected output: Person updated: { name: 'Bob', age: 25 }
console.log('After reassignment:', person.value); // Output: After reassignment: { name: 'Bob', age: 25 }

Explanation: When person is a shallowRef holding an object, mutating person.value.age does not trigger an update because shallowRef only observes changes to the person.value reference itself. Reassigning person.value to a new object, however, does trigger an update.

Example 3: Array Value (Shallow Tracking)

// Use the same trackEffect and triggerUpdate functions from Example 1

const items = shallowRef([1, 2, 3]);

// Simulate an effect that depends on 'items.value'
trackEffect(() => {
  console.log('Items updated:', items.value);
});

console.log('Initial items:', items.value); // Output: Initial items: [1, 2, 3]

// Mutate the array - THIS SHOULD NOT TRIGGER an update
items.value.push(4);
console.log('After push:', items.value); // Output: After push: [1, 2, 3, 4]
// Expected output: (No "Items updated" log here)

// Change an element by index - THIS SHOULD NOT TRIGGER an update
items.value[0] = 99;
console.log('After index change:', items.value); // Output: After index change: [99, 2, 3, 4]
// Expected output: (No "Items updated" log here)

// Reassign the entire .value property - THIS SHOULD TRIGGER an update
items.value = [5, 6];
// Expected output: Items updated: [5, 6]
console.log('After reassignment:', items.value); // Output: After reassignment: [5, 6]

Explanation: Similar to objects, mutating an array held by a shallowRef (e.g., push, index assignment) does not trigger reactivity. Only reassigning items.value to a completely new array or a different value will trigger an update.

Constraints

  • The shallowRef implementation must be written in TypeScript.
  • The solution should focus on the core shallowRef logic. You do not need to implement a full reactivity system from scratch. You can assume a simplified mechanism for tracking and triggering updates (as shown in the examples).
  • The returned ref object should have a .value property.
  • The implementation should be efficient.

Notes

  • Consider how you will distinguish between primitive values and object/array values when determining reactivity behavior.
  • The core challenge is to prevent Vue's deep reactivity from kicking in for objects/arrays passed to shallowRef. You are essentially creating a "one-level deep" ref.
  • Think about how Vue typically handles dependency tracking and triggering updates for refs. You can simulate this with simple callback functions as demonstrated in the examples.
  • The goal is to create a ref that, when its .value property is reassigned, notifies its dependents. Changes within the object/array referenced by .value should not.
Loading editor...
typescript