Building a Simple Reactive Data Store in JavaScript
This challenge asks you to implement a fundamental component of modern JavaScript applications: a reactive data store. A reactive system allows for automatic updates to the user interface or other parts of your application when the underlying data changes. This is crucial for creating dynamic and responsive user experiences.
Problem Description
You need to create a JavaScript class, ReactiveStore, that manages a piece of data and notifies any registered "subscribers" whenever that data is updated. The ReactiveStore should allow you to:
- Initialize with an initial value.
- Get the current value.
- Set a new value, triggering notifications.
- Subscribe to changes, providing a callback function that will be executed whenever the data is updated. Subscribers should also receive the new value.
- Unsubscribe from changes, removing a previously registered callback.
Key Requirements:
- The
ReactiveStoreclass should be generic enough to hold any type of data (primitives, objects, arrays, etc.). - When
setis called with the same value as the current value, no subscribers should be notified. - When
setis called with a new value, all currently subscribed callbacks should be invoked with the new value as an argument. - A subscriber can unsubscribe itself, or another subscriber can be removed by the store owner.
- Subscribers should be able to unsubscribe multiple times without errors.
Expected Behavior:
- Creating a store:
const myStore = new ReactiveStore(initialValue); - Getting the value:
myStore.get(); - Setting a new value:
myStore.set(newValue);(triggers subscribers) - Subscribing:
const unsubscribe = myStore.subscribe(callbackFunction); - Unsubscribing:
unsubscribe();ormyStore.unsubscribe(callbackFunction);
Edge Cases:
- Setting the same value multiple times consecutively.
- Subscribing with a function that immediately unsubscribes itself.
- Unsubscribing a non-existent subscriber.
- Handling initial values that are
nullorundefined.
Examples
Example 1: Basic Get, Set, and Subscribe
const countStore = new ReactiveStore(0);
let lastCount = -1;
const countSubscriber = (newValue) => {
lastCount = newValue;
console.log(`Count updated to: ${newValue}`);
};
const unsubscribeCount = countStore.subscribe(countSubscriber);
console.log("Initial count:", countStore.get()); // Output: Initial count: 0
countStore.set(10);
// Output: Count updated to: 10
console.log("Current count after set:", countStore.get()); // Output: Current count after set: 10
countStore.set(10); // Setting the same value
// No output from subscriber
countStore.set(25);
// Output: Count updated to: 25
console.log("Current count after another set:", countStore.get()); // Output: Current count after another set: 25
unsubscribeCount(); // Unsubscribe the listener
countStore.set(50);
// No output from subscriber
console.log("Current count after unsubscribe:", countStore.get()); // Output: Current count after unsubscribe: 50
Example 2: Object data and direct unsubscribe
const userStore = new ReactiveStore({ name: "Alice", age: 30 });
const userLogger = (newUser) => {
console.log(`User changed: ${newUser.name}, Age: ${newUser.age}`);
};
userStore.subscribe(userLogger);
console.log("Initial user:", userStore.get()); // Output: Initial user: { name: "Alice", age: 30 }
userStore.set({ name: "Bob", age: 31 });
// Output: User changed: Bob, Age: 31
console.log("Current user:", userStore.get()); // Output: Current user: { name: "Bob", age: 31 }
// Let's say Bob's age changes
userStore.set({ name: "Bob", age: 32 });
// Output: User changed: Bob, Age: 32
console.log("Current user:", userStore.get()); // Output: Current user: { name: "Bob", age: 32 }
// Unsubscribe directly using the store method
userStore.unsubscribe(userLogger);
userStore.set({ name: "Charlie", age: 35 });
// No output from subscriber
console.log("Current user:", userStore.get()); // Output: Current user: { name: "Charlie", age: 35 }
Example 3: Multiple subscribers and self-unsubscribing
const themeStore = new ReactiveStore("light");
let themeLog1 = "";
const unsubscribe1 = themeStore.subscribe((newTheme) => {
themeLog1 += `Listener 1 received: ${newTheme}\n`;
if (newTheme === "dark") {
console.log("Listener 1 unsubscribing itself!");
unsubscribe1(); // Listener 1 unsubscribes itself
}
});
let themeLog2 = "";
const listener2 = (newTheme) => {
themeLog2 += `Listener 2 received: ${newTheme}\n`;
};
themeStore.subscribe(listener2);
themeStore.set("light");
// Listener 1 received: light
// Listener 2 received: light
themeStore.set("dark");
// Listener 1 received: dark
// Listener 2 received: dark
// Listener 1 unsubscribing itself!
themeStore.set("blue");
// Listener 2 received: blue
// Note: Listener 1 is no longer active.
console.log("--- Theme Logs ---");
console.log(themeLog1);
console.log(themeLog2);
/*
Expected output for logs:
--- Theme Logs ---
Listener 1 received: light
Listener 1 received: dark
Listener 2 received: light
Listener 2 received: dark
Listener 2 received: blue
*/
Constraints
- The
ReactiveStoreclass must be implemented purely in JavaScript. No external libraries or frameworks are allowed. - The implementation should be efficient; the number of subscribers could potentially be large, so operations like
subscribeandunsubscribeshould have a reasonable time complexity. - The
setmethod should perform a shallow comparison when checking if the value has changed. For primitive types, this is a direct comparison. For objects and arrays,newValue === currentValuewill suffice for this challenge.
Notes
- Consider how you will store the list of subscribers. An array or a Set would be good candidates.
- When handling subscriptions and unsubscriptions, ensure that the list of subscribers is managed correctly to avoid issues like trying to call an unsubscribed listener.
- The
subscribemethod should return a function that allows the subscriber to easily unsubscribe themselves.