Hone logo
Hone
Problems

React Event Emitter: Building a Cross-Component Communication System

This challenge focuses on implementing a robust event emitter pattern within a React application using TypeScript. A common need in complex React applications is to facilitate communication between components that don't have a direct parent-child relationship. An event emitter provides a decoupled and flexible way to achieve this.

Problem Description

Your task is to create a reusable EventEmitter class and integrate it into a React application to manage cross-component communication. This EventEmitter will allow components to "publish" events and other components to "subscribe" to those events, receiving a callback when an event is published.

Key Requirements:

  1. EventEmitter Class:

    • Implement a TypeScript class named EventEmitter.
    • This class should have methods for:
      • on(eventName: string, listener: (...args: any[]) => void): Subscribes a listener function to a specific event.
      • off(eventName: string, listener: (...args: any[]) => void): Unsubscribes a specific listener from an event. If no listener is provided, unsubscribe all listeners for that event.
      • emit(eventName: string, ...args: any[]): Publishes an event, triggering all subscribed listeners with the provided arguments.
  2. React Integration:

    • Create a React context (EventEmitterContext) to provide the EventEmitter instance throughout the application.
    • Develop a custom hook (useEventEmitter) to easily access the EventEmitter instance in any functional component.
    • Implement a withEventEmitter higher-order component (HOC) for class components (optional, but good for demonstrating broader compatibility).
  3. Example Usage:

    • Demonstrate the EventEmitter's functionality with at least two distinct components that communicate without direct prop drilling. For instance, a "Sender" component that emits an event with data, and a "Receiver" component that listens for that event and updates its UI.

Expected Behavior:

  • When a component emits an event, all components subscribed to that event should have their corresponding listener functions executed with the emitted arguments.
  • Unsubscribing a listener should prevent it from being called on subsequent emit calls for that event.
  • The EventEmitter should handle multiple listeners for the same event.

Edge Cases to Consider:

  • Subscribing or unsubscribing a listener that has already been subscribed/unsubscribed.
  • Emitting an event that has no subscribers.
  • Unsubscribing all listeners for an event.
  • Ensuring listeners are properly unsubscribed when components unmount to prevent memory leaks.

Examples

Example 1: Basic Event Emission and Subscription

Imagine a simple scenario where a Counter component increments a value and emits a countUpdated event. Another component, Display, listens for this event and shows the current count.

  • EventEmitter Instance: A single EventEmitter instance is created and shared via context.
  • Counter Component:
    • Has a button to increment a local state.
    • On increment, it calls eventEmitter.emit('countUpdated', newCount).
  • Display Component:
    • Uses useEventEmitter to get the eventEmitter.
    • In a useEffect hook, it calls eventEmitter.on('countUpdated', handleCountUpdate).
    • handleCountUpdate updates the Display component's state with the new count.
    • The useEffect cleanup function calls eventEmitter.off('countUpdated', handleCountUpdate).

Input: User clicks the increment button in the Counter component multiple times.

Output: The Display component's text updates to reflect the changing count, e.g., "Current Count: 1", "Current Count: 2", etc.

Explanation: The Counter component publishes the countUpdated event with the new count value. The Display component, subscribed to countUpdated, receives this value and updates its rendered output.

Example 2: Passing Multiple Arguments

Consider a scenario where a "User Form" component submits user data. It emits a userSubmitted event with the user object and a boolean indicating success.

  • UserForm Component:
    • On successful form submission, calls eventEmitter.emit('userSubmitted', userObject, true).
  • NotificationService Component (or a similar listener):
    • Subscribes to userSubmitted.
    • The listener function receives userObject and isSuccess.
    • If isSuccess is true, it displays a success message.

Input: The UserForm successfully submits the data {"name": "Alice", "email": "alice@example.com"}.

Output: The NotificationService displays a message like "User Alice submitted successfully!".

Explanation: The userSubmitted event is emitted with two arguments. The listener in NotificationService correctly receives and processes both arguments.

Constraints

  • All implementations must be in TypeScript.
  • The EventEmitter class should be pure JavaScript/TypeScript, independent of React.
  • The React integration (Context, hook, HOC) should be idiomatic for React functional components.
  • Avoid prop drilling for communication between disparate components.
  • Ensure proper cleanup of event listeners to prevent memory leaks, especially when components unmount.

Notes

  • Think about how you will manage the listeners for each event name. A Map or an object where keys are event names and values are arrays of listeners could be a good starting point.
  • Consider the order of operations for on and emit. Should listeners be executed synchronously?
  • For the React integration, how will you ensure that components subscribing to events are correctly unsubscribed when they unmount? useEffect with a cleanup function is your friend here.
  • The withEventEmitter HOC is an optional but valuable addition to demonstrate how to integrate this pattern with class components.
Loading editor...
typescript