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:
-
createSignalFunction: Implement a functioncreateSignal<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.
-
createEffectFunction: Implement a functioncreateEffect(callback: () => void)that registers a side effect (e.g., a UI update). Thecallbackfunction will be executed initially and whenever any of the signals it reads are updated. -
Dependency Tracking: Your system must automatically track which signals are read within an effect's callback.
-
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
createSignalandcreateEffectfunctions 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
createEffectrepresents 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.