React Pub/Sub Event Bus
This challenge involves building a simple, in-memory publish-subscribe (pub/sub) event bus using React hooks and TypeScript. This pattern is invaluable for decoupling components, allowing them to communicate without direct dependencies, which is particularly useful in complex React applications for managing global state or coordinating actions across unrelated parts of the UI.
Problem Description
You need to create a reusable set of React hooks that implement a pub/sub mechanism. This system will allow any component to "publish" an event with associated data, and other components that have "subscribed" to that event type will be notified and receive the data.
Key Requirements:
- Event Bus Service: Create a central service (e.g., a class or a singleton object) that manages event subscriptions and publications.
- Subscription Hook (
useSubscribe): A custom React hook that allows a component to subscribe to a specific event type. When the event is published, the hook should trigger a callback function provided by the component, passing the event data. The hook must also handle unsubscribing when the component unmounts to prevent memory leaks. - Publication Function (
usePublishor a direct function): A mechanism (either a hook or a direct function exposed by the event bus service) to publish an event. This function should take the event type and optional data as arguments. - Type Safety: The entire system must be strongly typed using TypeScript. Event types should be distinct, and event data should be strongly typed based on the event type.
- Multiple Subscribers: The event bus should support multiple components subscribing to the same event type. All subscribers should be notified when an event is published.
- Unsubscription: It's crucial to ensure subscribers are properly unsubscribed when they are no longer needed (e.g., when a component unmounts).
Expected Behavior:
- A component calls
useSubscribewith an event type and a callback. - Another component calls
publishwith the same event type and some data. - The callback in the first component is executed with the data.
- If the first component unmounts, it should no longer receive notifications for that event.
Edge Cases:
- Publishing an event to which no component is subscribed.
- Unsubscribing from an event that was never subscribed to.
- Handling different data types for different events.
- Concurrent publications or subscriptions (though for this in-memory implementation, immediate synchronous execution is acceptable).
Examples
Example 1: Basic Subscription and Publication
// Assume EventBus is initialized and available
// Component A (Subscriber)
function SubscriberComponent() {
const handleMessage = (data: { greeting: string }) => {
console.log("Received message:", data.greeting);
};
useSubscribe("userGreeting", handleMessage);
return <div>Listening for greetings...</div>;
}
// Component B (Publisher)
function PublisherComponent() {
const publish = usePublish(); // or directly call EventBus.publish
const sendGreeting = () => {
publish("userGreeting", { greeting: "Hello from Publisher!" });
};
return <button onClick={sendGreeting}>Send Greeting</button>;
}
// Expected Console Output when button is clicked:
// Received message: Hello from Publisher!
Example 2: Handling Multiple Subscribers to the Same Event
// Component C (Another Subscriber)
function AnotherSubscriberComponent() {
const handleMessage = (data: { greeting: string }) => {
console.log("Another component received:", data.greeting);
};
useSubscribe("userGreeting", handleMessage);
return <div>Also listening for greetings!</div>;
}
// When PublisherComponent.sendGreeting() is called, both SubscriberComponent
// and AnotherSubscriberComponent should log their respective messages.
// Expected Console Output when button is clicked:
// Received message: Hello from Publisher!
// Another component received: Hello from Publisher!
Example 3: Different Event Types and Data Structures
// Define specific event types and their data payloads
interface AppEvents {
"userDataUpdated": { userId: string; newName: string };
"systemNotification": { message: string; severity: "info" | "warning" | "error" };
}
// Component D (Subscribes to user data)
function UserDisplayComponent() {
const updateName = (data: { userId: string; newName: string }) => {
console.log(`User ${data.userId} name updated to ${data.newName}`);
};
useSubscribe("userDataUpdated", updateName);
return <div>Displaying user info...</div>;
}
// Component E (Publishes user data update)
function UserProfileEditor() {
const publish = usePublish();
const saveChanges = () => {
publish("userDataUpdated", { userId: "user123", newName: "Jane Doe" });
};
return <button onClick={saveChanges}>Save User Changes</button>;
}
// Component F (Subscribes to notifications)
function NotificationArea() {
const showNotification = (data: { message: string; severity: "info" | "warning" | "error" }) => {
console.log(`Notification (${data.severity}): ${data.message}`);
};
useSubscribe("systemNotification", showNotification);
return <div>Notifications will appear here.</div>;
}
// Component G (Publishes a system notification)
function ErrorLogger() {
const publish = usePublish();
const logError = () => {
publish("systemNotification", { message: "Database connection lost.", severity: "error" });
};
return <button onClick={logError}>Log Error</button>;
}
// Expected Console Output when UserProfileEditor button is clicked:
// User user123 name updated to Jane Doe
// Expected Console Output when ErrorLogger button is clicked:
// Notification (error): Database connection lost.
Constraints
- The event bus implementation must be entirely within a single TypeScript file or a small, self-contained module.
- No external libraries for state management or pub/sub are allowed (e.g., Redux, Zustand, RxJS). You are building this from scratch using React hooks.
- The
useSubscribehook must ensure that subscriptions are cleaned up when the component unmounts. - The event bus should be able to handle at least 100 concurrent subscriptions without significant performance degradation.
- Event data can be any valid JSON-serializable JavaScript object.
Notes
- Consider how you will store subscriptions. A Map where keys are event types and values are arrays of callback functions is a common approach.
- Think about the lifecycle of React components and how
useEffectcan be used to manage subscriptions and unsubscriptions. - TypeScript's utility types (like
Recordor indexed access types) can be very helpful for creating a type-safe event bus that maps event names to their specific data payloads. - For the
usePublishhook, consider if it should be a hook that returns a stable function reference or if you can simply export a direct publish function from your event bus module. The hook approach generally integrates better with React's rendering model. - Focus on robustness and correctness of the subscription/unsubscription logic.