Hone logo
Hone
Problems

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 createEffectSystem that returns an object with two methods: registerEffect and rerunEffects.
  • registerEffect should 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.
  • rerunEffects should 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 registerEffect is called, the effect function and its dependencies are stored.
  • When rerunEffects is 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 rerunEffects is 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 createEffectSystem function should return an object with only the registerEffect and rerunEffects methods.
  • The registerEffect function should accept a function and an array.
  • The rerunEffects function 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 rerunEffects function 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.
Loading editor...
typescript