Hone logo
Hone
Problems

React Fine-Grained Reactivity Simulation

This challenge focuses on understanding and implementing a core concept of modern reactivity patterns in UI frameworks: fine-grained reactivity. Instead of re-rendering entire components when state changes, fine-grained reactivity aims to update only the specific parts of the UI that depend on the changed state, leading to significant performance improvements. You will simulate this by building a minimal reactivity system.

Problem Description

Your task is to create a system that mimics fine-grained reactivity within a React-like environment using TypeScript. You will need to implement a mechanism for tracking dependencies between state values and the UI elements that render them. When a state value changes, only the parts of the UI that explicitly use that value should be re-rendered or updated.

Key Requirements:

  1. createSignal Function: Implement a function createSignal<T>(initialValue: T) that creates a reactive state variable. This function should return a tuple:

    • A getter function to read the current value of the signal.
    • A setter function to update the value of the signal.
  2. createEffect Function: Implement a function createEffect(callback: () => void) that registers a side effect (e.g., a UI update). The callback function will be executed initially and whenever any of the signals it reads are updated.

  3. Dependency Tracking: Your system must automatically track which signals are read within an effect's callback.

  4. Update Mechanism: When a signal's value is updated via its setter, all registered effects that depend on that signal should be re-executed.

Expected Behavior:

When a signal's value changes, only the effects that directly read that signal should run. Effects that do not read the changed signal should not be re-executed.

Edge Cases to Consider:

  • Signals that are never read by any effect.
  • Effects that read multiple signals.
  • Effects that read signals that are updated within the effect itself (recursion/cycles should be handled gracefully, likely by executing the effect once or a limited number of times per update cycle).
  • Multiple signals being updated in a single synchronous operation.

Examples

Example 1:

// Assume your implementation is available as 'signalLib'
const [count, setCount] = signalLib.createSignal(0);

signalLib.createEffect(() => {
  console.log("Count is:", count());
});

setCount(1); // Should log "Count is: 1"

Explanation: An effect is created that logs the value of count. When count is updated to 1, the effect re-runs and logs the new value.

Example 2:

// Assume your implementation is available as 'signalLib'
const [count, setCount] = signalLib.createSignal(0);
const [name, setName] = signalLib.createSignal("Alice");

signalLib.createEffect(() => {
  console.log("Count is:", count());
});

const nameEffect = signalLib.createEffect(() => {
  console.log("Name is:", name());
});

setCount(5); // Should log "Count is: 5"
// nameEffect should NOT run here.

setName("Bob"); // Should log "Name is: Bob"
// The first effect (logging count) should NOT run here.

Explanation: Two signals, count and name, are created. Two separate effects are registered. When count changes, only the effect that reads count re-runs. When name changes, only the effect that reads name re-runs.

Example 3:

// Assume your implementation is available as 'signalLib'
const [a, setA] = signalLib.createSignal(1);
const [b, setB] = signalLib.createSignal(2);

signalLib.createEffect(() => {
  console.log("Sum:", a() + b());
});

setA(10); // Should log "Sum: 12" (runs once with new 'a' and old 'b')
setB(20); // Should log "Sum: 30" (runs once with new 'b' and new 'a')

Explanation: An effect depends on both a and b. When a is updated, the effect re-runs, logging the sum with the new a and the current b. When b is subsequently updated, the effect re-runs again, logging the sum with the latest values of both a and b.

Constraints

  • Your implementation should be written purely in TypeScript.
  • The createSignal and createEffect functions should be the only public API.
  • The system should efficiently handle a large number of signals and effects (though actual performance testing is not required for this challenge, design with scalability in mind).
  • Avoid using any external libraries that provide reactivity (e.g., MobX, Zustand, Valtio, RxJS). You are building the core logic yourself.

Notes

  • Think about how to store the dependencies between signals and effects. A common pattern is to use a registry or a directed graph.
  • Consider how to trigger updates. When a signal's setter is called, you need a mechanism to notify all dependent effects.
  • The "callback" for createEffect represents the rendering logic or any side effect that consumes reactive state. You don't need to implement actual DOM manipulation, just simulate the execution of the callback.
  • For the edge case of an effect reading a signal that it also updates, a simple approach is to ensure the effect runs only once per update cycle to prevent infinite loops.
Loading editor...
typescript