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:
-
signal(initialValue)Function:- Accepts an initial value of any type.
- Returns an object with
get()andset()methods. get(): Returns the current value of the signal.set(newValue): Updates the signal's value and triggers notifications to all subscribers.
-
useSignal(signal)Hook:- Accepts a signal object created by your
signalfunction. - Returns the current value of the signal.
- Ensures that any component using this hook re-renders when the signal's value changes.
- Accepts a signal object created by your
-
Reactivity: Components using
useSignalshould only re-render when the specific signal they are subscribed to changes. -
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:
countSignal.set(1)is called.- Both
CounterDisplaycomponents, subscribed tocountSignal, will re-render. - The app will display
Count: 1in 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:
userSignal.set(...)is called, updating theageproperty of the object.- If
UserNameDisplayis only subscribed to theuserSignalitself (and not just thenameproperty), it should re-render if the signal implementation correctly detects object changes. - 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
signalanduseSignalimplementation 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
useStateanduseEffecthooks manage state and subscriptions. You'll need a similar mechanism to track which components are "listening" to which signals. - Consider using a
Setto store the callbacks (or component update functions) for each signal to efficiently add, remove, and iterate through subscribers. - The
useSignalhook will likely need to involveReact.useStateor 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.