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:
- Function Signature: The function must be named
shallowRefand accept one argument:initialValueof any type. It should return a ref object. - Ref Object Structure: The returned ref object must have at least a
.valueproperty, which holds the current value. It should also have a mechanism to trigger updates when.valueis reassigned. - Shallow Reactivity:
- If the
initialValueis a primitive type (string, number, boolean, null, undefined, symbol, bigint), reactivity should work as expected, similar to Vue'sref. - If the
initialValueis 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.valueproperty should trigger an update.
- If the
- 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
.valueis reassigned.
Expected Behavior:
- Creating a
shallowRefwith a primitive value should allow changes to that primitive to be reactive. - Creating a
shallowRefwith an object:- Reassigning the entire
.valueproperty to a new object or primitive should trigger an update. - Mutating properties of the object assigned to
.valueshould not trigger an update.
- Reassigning the entire
- Creating a
shallowRefwith an array:- Reassigning the entire
.valueproperty 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.
- Reassigning the entire
Edge Cases:
- Handling
nullandundefinedas 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
shallowRefimplementation must be written in TypeScript. - The solution should focus on the core
shallowReflogic. 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
.valueproperty. - 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
.valueproperty is reassigned, notifies its dependents. Changes within the object/array referenced by.valueshould not.