Hone logo
Hone
Problems

Implementing a Custom Ref in Vue.js (TypeScript)

Vue.js provides the ref function to create reactive state. However, sometimes you need more control over how that state is accessed and modified. This challenge involves implementing a custom ref that adds specific behavior, such as logging or validation, whenever its value is read or written.

Problem Description

Your task is to create a custom ref function in Vue.js using TypeScript. This custom ref should wrap Vue's built-in ref and allow you to define custom logic that executes on get (when the value is accessed) and set (when the value is changed).

Key Requirements:

  1. Custom Getter: The custom ref should accept an optional get function. This function will be called every time the ref's value is accessed. It should receive the current raw value and return the value to be exposed.
  2. Custom Setter: The custom ref should accept an optional set function. This function will be called every time the ref's value is attempted to be changed. It should receive the new raw value and the current raw value, and it should be responsible for actually updating the internal value.
  3. Integration with Vue's Reactivity: The custom ref must correctly integrate with Vue's reactivity system, ensuring that components re-render when the ref's value changes.
  4. TypeScript Support: The implementation should be fully typed in TypeScript.

Expected Behavior:

When you use your custom ref, accessing its .value property will trigger your custom get logic. Assigning a new value to .value will trigger your custom set logic.

Edge Cases to Consider:

  • What happens if only a get function is provided, but no set function?
  • What happens if only a set function is provided, but no get function?
  • What happens if neither get nor set functions are provided? (It should behave like a standard ref).
  • Ensure type safety when dealing with different data types passed to the ref.

Examples

Example 1: Logging Ref

Let's imagine a ref that logs every time its value is accessed or changed.

Input (Conceptual - how it would be used):

import { ref, customRef } from 'vue';

function useCustomRefWithLogging<T>(initialValue: T) {
  return customRef<T>((track, trigger, stop) => {
    let value = initialValue;

    return {
      get() {
        console.log(`Getting value: ${value}`);
        track(); // Tell Vue to track this ref for reactivity
        return value;
      },
      set(newValue) {
        console.log(`Setting value from ${value} to ${newValue}`);
        value = newValue;
        trigger(); // Tell Vue to trigger updates for this ref
      }
    };
  });
}

// Usage in a Vue component:
const message = useCustomRefWithLogging('Hello');

// When accessed:
console.log(message.value); // Output: Getting value: Hello, then 'Hello'

// When changed:
message.value = 'World'; // Output: Setting value from Hello to World

Output (Console Logs):

Getting value: Hello
Setting value from Hello to World

Explanation:

The useCustomRefWithLogging creates a ref that logs to the console before returning the value in get and before updating the internal value in set. Vue's track and trigger are essential for maintaining reactivity.

Example 2: Validation Ref

A ref that only allows numbers greater than 0.

Input (Conceptual - how it would be used):

import { ref, customRef } from 'vue';

function usePositiveNumberRef(initialValue: number) {
  return customRef<number>((track, trigger) => {
    let value = initialValue;

    return {
      get() {
        track();
        return value;
      },
      set(newValue) {
        if (typeof newValue === 'number' && newValue > 0) {
          console.log(`Valid input: ${newValue}`);
          value = newValue;
          trigger();
        } else {
          console.warn(`Invalid input: ${newValue}. Must be a positive number.`);
        }
      }
    };
  });
}

// Usage in a Vue component:
const count = usePositiveNumberRef(10);

// When accessed:
console.log(count.value); // Output: 10

// When changed with valid input:
count.value = 20; // Output: Valid input: 20
console.log(count.value); // Output: 20

// When changed with invalid input:
count.value = -5; // Output: Invalid input: -5. Must be a positive number.
console.log(count.value); // Output: 20 (value remains unchanged)

count.value = 0; // Output: Invalid input: 0. Must be a positive number.
console.log(count.value); // Output: 20 (value remains unchanged)

Output (Console Logs):

10
Valid input: 20
20
Invalid input: -5. Must be a positive number.
Invalid input: 0. Must be a positive number.

Explanation:

This custom ref intercepts writes to .value. If the new value is a positive number, it updates the internal state and triggers a re-render. Otherwise, it logs a warning and prevents the update.

Constraints

  • The custom ref implementation must be a standalone function that takes an initial value and optional get/set handlers.
  • The solution must use Vue's customRef API.
  • The solution must be written in TypeScript.
  • The custom ref should support any primitive type and objects.

Notes

  • Vue's customRef gives you access to track, trigger, and stop functions.
    • track(): Call this inside the get function to register the dependency.
    • trigger(): Call this inside the set function to notify Vue that the value has changed.
    • stop(): Use this for cleanup if your custom ref has any listeners or subscriptions that need to be removed when the component unmounts. For this challenge, you likely won't need stop.
  • You will need to return an object with get and set properties from the factory function passed to customRef.
  • Consider how you'll handle the type of the initialValue and the return type of your custom ref.
  • Think about how to pass the get and set handlers into your custom ref function without them interfering with each other or the core reactivity. The customRef factory function is designed to return the get and set handlers for the ref itself, so your custom ref function will need to create these handlers using the provided get/set logic.
Loading editor...
typescript