Hone logo
Hone
Problems

Reactive Primitives in React: A Custom State Management System

This challenge focuses on building fundamental reactive primitives within React, mimicking the core functionality of state management libraries like Zustand or Jotai. You'll create a simple, yet powerful, system for managing and deriving state, demonstrating a deeper understanding of React's reactivity and how to build upon it. This exercise is valuable for understanding the underlying mechanisms of state management and building custom solutions tailored to specific needs.

Problem Description

Your task is to implement three core reactive primitives: createSignal, createMemo, and createEffect. These primitives will form the foundation of a lightweight state management system.

  • createSignal(initialValue: T): { value: T; set: (newValue: T | ((prevValue: T) => T)) => void; }: This function creates a signal. A signal holds a single value of type T and provides a value getter and a set function to update the value. The set function can accept either a new value directly or a function that receives the previous value and returns the new value. Changes to the signal's value should trigger re-renders in components that use it.

  • createMemo(compute: (get: (signal: Signal<any>) => any) => T): T: This function creates a memoized value. It takes a computation function compute as an argument. This function receives a get function, which allows access to the values of signals. The compute function should be executed only when its dependencies (signals accessed through get) change. The result of the computation is cached and returned as the memoized value.

  • createEffect(compute: (get: (signal: Signal<any>) => any) => void): void: This function creates an effect. It takes a computation function compute as an argument, similar to createMemo. This function receives a get function to access signal values. The compute function should be executed whenever any of its dependencies (signals accessed through get) change. Effects are typically used for side effects like logging, DOM mutations, or API calls.

Key Requirements:

  • Reactivity: Changes to signals should trigger updates in memoized values and effects that depend on them.
  • Dependency Tracking: createMemo and createEffect must correctly track dependencies on signals.
  • Efficient Updates: createMemo should only re-compute when its dependencies change.
  • TypeScript Safety: The code should be well-typed and leverage TypeScript's features to ensure type safety.
  • No External Libraries: You should not use any external state management libraries (Redux, Zustand, Jotai, etc.). You are building your own primitives.

Expected Behavior:

  • Signals should hold and update their values correctly.
  • Memoized values should be computed only when their dependencies change.
  • Effects should be executed whenever their dependencies change.
  • Components using these primitives should re-render when the underlying state changes.

Examples

Example 1:

// Assume createSignal, createMemo, and createEffect are defined

const count = createSignal(0);

const doubleCount = createMemo(() => count.value * 2);

createEffect(() => {
  console.log("Double count:", doubleCount);
});

count.set(5); // Output: Double count: 10
count.set(10); // Output: Double count: 20

Explanation: count is initialized to 0. doubleCount is memoized and depends on count. The effect logs doubleCount. When count is set to 5, doubleCount is recomputed to 10, and the effect is executed. When count is set to 10, doubleCount is recomputed to 20, and the effect is executed again.

Example 2:

// Assume createSignal, createMemo, and createEffect are defined

const name = createSignal("Alice");
const age = createSignal(30);

const greeting = createMemo(() => `Hello, ${name.value}! You are ${age.value} years old.`);

createEffect(() => {
  console.log(greeting);
});

name.set("Bob"); // Output: Hello, Bob! You are 30 years old.
age.set(31);   // Output: Hello, Bob! You are 31 years old.

Explanation: name and age are signals. greeting is a memoized value that depends on both name and age. The effect logs greeting. When name is set to "Bob", greeting is recomputed. When age is set to 31, greeting is recomputed again.

Example 3: (Edge Case - Nested Dependencies)

// Assume createSignal, createMemo, and createEffect are defined

const a = createSignal(1);
const b = createSignal(2);

const c = createMemo(() => a.value + b.value);

const d = createMemo(() => c.value * 2);

createEffect(() => {
  console.log("d:", d);
});

a.set(3); // Output: d: 10
b.set(4); // Output: d: 14

Explanation: c depends on a and b. d depends on c. When a changes, c is recomputed, which in turn triggers the recomputation of d and the execution of the effect. When b changes, c is recomputed, which in turn triggers the recomputation of d and the execution of the effect.

Constraints

  • Maximum Signal Count: The system should be able to handle at least 100 signals concurrently without significant performance degradation.
  • Dependency Cycle Prevention: While not strictly required for this challenge, consider how you might prevent circular dependencies between signals, memoized values, and effects. (Hint: This is a complex topic, so a simple acknowledgement of the issue is sufficient).
  • Performance: The re-computation of memoized values and execution of effects should be as efficient as possible, minimizing unnecessary work.
  • TypeScript: Strict adherence to TypeScript types is required.

Notes

  • Think about how to track dependencies between signals, memoized values, and effects. A simple approach might involve storing a list of signals that each memoized value and effect depends on.
  • Consider using a Map or Set to efficiently store and manage signals and their dependencies.
  • The get function passed to createMemo and createEffect should provide a way to access the current value of a signal.
  • This is a simplified implementation. Real-world state management libraries often include features like batching updates, devtools integration, and more sophisticated dependency tracking. Focus on the core reactive primitives for this challenge.
  • The createEffect function does not need to return anything. Its purpose is solely to execute side effects.
Loading editor...
typescript