Reactive Effects in React with TypeScript
Building reactive effects in React allows components to respond to changes in data or state in a predictable and manageable way, beyond the standard useEffect hook. This challenge asks you to implement a simplified effect system that allows components to register and execute effects based on specific dependencies, mimicking a more controlled and potentially optimized approach to side effects. This is useful for scenarios where you need fine-grained control over when effects run or want to avoid unnecessary re-renders.
Problem Description
You are tasked with creating a basic effect system for React components using TypeScript. The system should allow components to register effects that are executed when specific dependencies change. The effect system should manage a list of registered effects and efficiently trigger only those effects whose dependencies have been updated.
What needs to be achieved:
- Create a function
createEffectSystemthat returns an object with two methods:registerEffectandrerunEffects. registerEffectshould accept an effect function and an array of dependencies. The effect function should be a function that takes no arguments. The dependencies array should contain values that, when changed, trigger the effect.rerunEffectsshould iterate through all registered effects and execute those whose dependencies have changed since the last execution. Dependency changes are determined by strict equality (===).- The effect system should maintain a record of the last seen values for each dependency.
Key Requirements:
- The effect system must be implemented using TypeScript.
- The effect system should be efficient, only re-running effects when necessary.
- The effect system should handle multiple effects with different dependencies.
- The effect system should not cause infinite loops.
Expected Behavior:
- When
registerEffectis called, the effect function and its dependencies are stored. - When
rerunEffectsis called, the effect system compares the current values of the dependencies with the previously stored values. - If any dependency has changed (using
===), the corresponding effect function is executed. - After an effect function is executed, the effect system updates the stored values of its dependencies.
Edge Cases to Consider:
- What happens when an effect is registered with an empty dependency array? (Should run every time
rerunEffectsis called) - What happens when an effect is registered with dependencies that are objects or arrays? (Strict equality
===will be used, so changes to properties of objects/elements of arrays will not trigger the effect unless the object/array itself is replaced.) - What happens if the effect function throws an error? (The effect system should continue to function and not crash.)
Examples
Example 1:
Input:
const effectSystem = createEffectSystem();
const effect1 = () => console.log("Effect 1 ran");
const effect2 = () => console.log("Effect 2 ran");
effectSystem.registerEffect(effect1, [1, 2]);
effectSystem.registerEffect(effect2, [3, 4]);
rerunEffects(effectSystem, { 1: 1, 2: 2, 3: 3, 4: 4 }); // Initial run
rerunEffects(effectSystem, { 1: 1, 2: 2, 3: 3, 4: 4 }); // No change, no effects run
rerunEffects(effectSystem, { 1: 1, 2: 3, 3: 3, 4: 4 }); // Effect 1 runs
rerunEffects(effectSystem, { 1: 1, 2: 3, 3: 4, 4: 4 }); // Effect 1 and Effect 2 run
Output:
Effect 1 ran
Effect 1 ran
Effect 1 ran
Effect 1 ran
Effect 2 ran
Explanation: The first `rerunEffects` call runs both effects because the dependencies are new. Subsequent calls only run effects whose dependencies have changed.
Example 2:
Input:
const effectSystem = createEffectSystem();
const effect1 = () => console.log("Effect 1 ran");
effectSystem.registerEffect(effect1, []); // No dependencies
rerunEffects(effectSystem, { a: 1, b: 2 });
rerunEffects(effectSystem, { a: 3, b: 4 });
Output:
Effect 1 ran
Effect 1 ran
Explanation: Because effect1 has no dependencies, it runs every time `rerunEffects` is called.
Example 3: (Edge Case - Object Dependency)
Input:
const effectSystem = createEffectSystem();
const effect1 = () => console.log("Effect 1 ran");
const obj = { value: 1 };
effectSystem.registerEffect(effect1, [obj]);
rerunEffects(effectSystem, { value: 1 }); // Initial run
obj.value = 2;
rerunEffects(effectSystem, { value: 1 }); // No change, effect doesn't run
const newObj = { value: 1 };
rerunEffects(effectSystem, { value: 1 }); // Effect runs because obj is a new object
Output:
Effect 1 ran
Explanation: Modifying the `value` property of `obj` does *not* trigger the effect because `===` compares object references, not their contents. Replacing `obj` with a new object *does* trigger the effect.
Constraints
- The
createEffectSystemfunction should return an object with only theregisterEffectandrerunEffectsmethods. - The
registerEffectfunction should accept a function and an array. - The
rerunEffectsfunction should accept the effect system object and a plain JavaScript object representing the current values of the dependencies. - The effect system should be implemented without using any external libraries.
- The effect system should be reasonably performant for a small number of effects (e.g., less than 100). Optimization for extremely large numbers of effects is not required.
Notes
- Think about how to store the registered effects and their dependencies. A simple data structure like an array of objects might be suitable.
- Consider how to efficiently compare dependencies. Strict equality (
===) is sufficient for this challenge. - The
rerunEffectsfunction should not modify the input object. - This is a simplified effect system. Real-world effect systems often have more features, such as automatic cleanup, memoization, and more sophisticated dependency tracking.
- Focus on correctness and clarity of code. Good TypeScript practices (type safety, clear naming) are encouraged.