Hone logo
Hone
Problems

Implementing a React Signals System

Build a lightweight, performant, and declarative state management system in React using TypeScript. This system should allow components to subscribe to and react to changes in shared state, similar to how built-in React hooks work but with a custom, more primitive approach. This is a fundamental pattern for building scalable and efficient applications.

Problem Description

Your task is to implement a simple signals system in React with TypeScript. A "signal" is a reactive primitive that holds a value and notifies any subscribers when its value changes. You need to create the core signal function and a React hook useSignal that allows components to consume signals and re-render automatically when the signal's value changes.

Key Requirements:

  1. signal(initialValue) Function:

    • Accepts an initial value of any type.
    • Returns an object with get() and set() methods.
    • get(): Returns the current value of the signal.
    • set(newValue): Updates the signal's value and triggers notifications to all subscribers.
  2. useSignal(signal) Hook:

    • Accepts a signal object created by your signal function.
    • Returns the current value of the signal.
    • Ensures that any component using this hook re-renders when the signal's value changes.
  3. Reactivity: Components using useSignal should only re-render when the specific signal they are subscribed to changes.

  4. TypeScript Support: The implementation must be fully typed using TypeScript.

Expected Behavior:

When a signal's value is updated using set(), all components that are currently displaying or reacting to that signal's value via useSignal() should re-render to reflect the new value.

Edge Cases:

  • Multiple Consumers: A single signal can be used by multiple components. All should update.
  • Unsubscribing: While not explicitly required for this basic implementation, consider how a more robust system would handle unsubscribing to prevent memory leaks (though for this challenge, we can assume components using the hook will be unmounted cleanly).
  • Complex Data Structures: The signal should correctly handle updates to objects, arrays, and primitive types.

Examples

Example 1:

import React from 'react';
// Assume signal and useSignal are implemented as described

// --- Signal Creation ---
const countSignal = signal(0);

// --- Component using the signal ---
function CounterDisplay() {
  const count = useSignal(countSignal);
  return <div>Count: {count}</div>;
}

// --- Component that updates the signal ---
function CounterButton() {
  const increment = () => {
    countSignal.set(countSignal.get() + 1);
  };
  return <button onClick={increment}>Increment</button>;
}

// --- App structure (conceptual) ---
function App() {
  return (
    <div>
      <CounterDisplay />
      <CounterButton />
      <CounterDisplay /> {/* Another display component */}
    </div>
  );
}

Output:

Initially, the app will render Count: 0 in both CounterDisplay instances. When the "Increment" button is clicked:

  1. countSignal.set(1) is called.
  2. Both CounterDisplay components, subscribed to countSignal, will re-render.
  3. The app will display Count: 1 in both instances.

Explanation: The countSignal holds a primitive number. useSignal subscribes CounterDisplay to countSignal. When CounterButton calls countSignal.set(), the signal notifies its subscribers, causing CounterDisplay instances to update and re-render with the new value.

Example 2:

import React from 'react';
// Assume signal and useSignal are implemented

// --- Signal Creation ---
const userSignal = signal({ name: 'Alice', age: 30 });

// --- Component displaying user name ---
function UserNameDisplay() {
  // We only need the name, but useSignal gives the whole object
  const user = useSignal(userSignal);
  return <div>User Name: {user.name}</div>;
}

// --- Component to update user age ---
function UserAgeUpdater() {
  const updateAge = () => {
    // Crucially, we need to update the object immutably to trigger reactivity if done shallowly
    // A robust signal might handle deep equality checks or require explicit mutation
    const currentUser = userSignal.get();
    userSignal.set({ ...currentUser, age: currentUser.age + 1 });
  };
  return <button onClick={updateAge}>Increase Age</button>;
}

// --- App structure (conceptual) ---
function App() {
  return (
    <div>
      <UserNameDisplay />
      <UserAgeUpdater />
    </div>
  );
}

Output:

Initially, the app displays User Name: Alice. When "Increase Age" is clicked:

  1. userSignal.set(...) is called, updating the age property of the object.
  2. If UserNameDisplay is only subscribed to the userSignal itself (and not just the name property), it should re-render if the signal implementation correctly detects object changes.
  3. The app will still display User Name: Alice.

Explanation: This example highlights the need for the signal to correctly detect changes, even within complex data structures. A naive implementation might only trigger updates if the reference of the object changes. For this challenge, assume that updating any part of the object returned by signal constitutes a "change" that should trigger re-renders. A more advanced system might offer fine-grained subscriptions.

Constraints

  • The signal and useSignal implementation should be self-contained within a single file or module.
  • No external state management libraries (like Redux, Zustand, Jotai) are allowed.
  • The solution must use TypeScript and leverage its type-safety features.
  • Performance is important; avoid unnecessary re-renders or computations. The system should be efficient enough for a moderate number of signals and subscribers.

Notes

  • Think about how React's useState and useEffect hooks manage state and subscriptions. You'll need a similar mechanism to track which components are "listening" to which signals.
  • Consider using a Set to store the callbacks (or component update functions) for each signal to efficiently add, remove, and iterate through subscribers.
  • The useSignal hook will likely need to involve React.useState or a similar mechanism internally to force re-renders when the signal updates.
  • This challenge focuses on the core reactivity primitive. Advanced features like derived signals (signals computed from other signals) are out of scope for this basic implementation.
Loading editor...
typescript