DEV Community

Soumya Ranjan Padhy
Soumya Ranjan Padhy

Posted on

useEventEmitter: A react hook to emit and listen to custom events

In React, there are often scenarios where we need to communicate between different components without passing props down multiple levels. A common solution is to use global state managers like "Redux" or "Zustand". Alternatively, we can use an event pattern similar to Node.js’s "EventEmitter". In this post, I’ll walk you through how I built a custom hook, "useEventEmitter", to address this issue in a React and TypeScript environment by leveraging the "EventTarget" API.

Step 1: Setting up the Global Event Emitter

The core of the solution is creating a global event emitter based on the "EventTarget" class. This allows us to dispatch and listen for events globally.

// event-emitter.ts
class GlobalEventEmitter extends EventTarget {}

const globalEventEmitter = new GlobalEventEmitter();

export default globalEventEmitter;
Enter fullscreen mode Exit fullscreen mode

"EventTarget" is a built-in browser API that allows objects to handle events and dispatch them. By creating a single instance of "GlobalEventEmitter", we ensure that all components share the same event system.

Step 2: The Custom "useEventEmitter" Hook

2.1. Defining the Hook and State

import { useEffect, useState, useCallback, useRef } from "react";
import globalEventEmitter from "./event-emitter";

export const useEventEmitter = <T>(eventName: string) => {
  const [eventData, setEventData] = useState<T>();
  const skipRerender = useRef(false);
};
Enter fullscreen mode Exit fullscreen mode
  • useEventEmitter: This is a generic hook that takes an "eventName" of type string and uses a generic type for the event data.
  • eventData: A state variable to hold the data from the event.
  • skipRerender: A ref used to control whether the component should re-render when an event is received.

2.2. Publishing an Event

const publishEvent = useCallback(
    (eventData: T, skipRender = true) => {
      skipRerender.current = skipRender;
      const event = new CustomEvent(eventName, { detail: eventData });
      globalEventEmitter.dispatchEvent(event);
    },
    [eventName]
  );
Enter fullscreen mode Exit fullscreen mode
  • publishEvent: This function dispatches an event with the given data.
    • eventData: The data to send with the event.
    • skipRender: A flag to indicate if the re-render should be skipped (default is "true").
  • skipRerender.current: Sets the value of "skipRerender" to determine if the component should skip updating its state.
  • new CustomEvent(eventName, { detail: eventData }): Creates a new "CustomEvent" with the specified event name and data.
  • globalEventEmitter.dispatchEvent(event): Dispatches the created event to the global event emitter.

2.3. Subscribing to the Event

  useEffect(() => {
    const listener = (event: Event) => {
      if (skipRerender.current) {
        skipRerender.current = false;
        return;
      }
      setEventData((event as CustomEvent).detail);
    };

    globalEventEmitter.addEventListener(eventName, listener);

    // Cleanup subscription on unmount
    return () => {
      globalEventEmitter.removeEventListener(eventName, listener);
    };
  }, [eventName, skipRerender]);
Enter fullscreen mode Exit fullscreen mode
  • useEffect: Handles subscribing to the event and cleaning up the subscription when the component unmounts or when "eventName" changes.
  • listener: A function that will be called whenever the specified event is dispatched.
    • skipRerender.current: Checks if the re-render should be skipped. If so, it resets "skipRerender.current" and returns.
    • setEventData((event as CustomEvent).detail): Updates the state with the event data.
  • globalEventEmitter.addEventListener(eventName, listener): Registers the listener to the global event emitter for the specified "eventName".
  • Cleanup Function: Removes the event listener when the component is unmounted or when "eventName" changes. This prevents memory leaks and ensures the component is cleaned up properly.

2.4. Returning Values

 return { eventData, publishEvent };
Enter fullscreen mode Exit fullscreen mode
  • eventData: The current state data from the event, which components can use to display or react to the event.
  • publishEvent: The function to dispatch events, which components can use to send data.

Step 3: Creating the Publisher Component

// publisher.tsx
import { useEventEmitter } from "../lib/use-event-emitter";

const Publisher = () => {
  const { publishEvent } = useEventEmitter<{ data: string }>("event-name");

  const handleClick = () => {
    // Publish event without triggering a rerender
    publishEvent({ data: "Hello World" });

    // Trigger a re-render after 5 seconds
    setTimeout(() => {
      publishEvent({ data: "Hello World Again" }, false);
    }, 5000);
  };

  return (
    <div>
      <button onClick={handleClick}>Publish Event</button>
    </div>
  );
};

export default Publisher;
Enter fullscreen mode Exit fullscreen mode

Here, we use the "publishEvent" function to send out data. When the button is clicked, the event is dispatched with "data", and after 5 seconds, another event is dispatched, allowing subscribers to respond.

Step 4: Creating the Subscriber Component

// subscriber.tsx
import { useEventEmitter } from "../lib/use-event-emitter";

const Subscriber = () => {
  const { eventData } = useEventEmitter<{ data: string }>("event-name");

  return <div>{eventData && <div>{eventData.data}</div>}</div>;
};

export default Subscriber;
Enter fullscreen mode Exit fullscreen mode

This component uses the "useEventEmitter" hook to subscribe to the event named "event-name". When the event is dispatched, the "eventData" state is updated, and the component re-renders to display the data.

Conclusion

  • App-Wide Notifications: Dispatch notifications that can be received by various components across the application, making it easy to implement a global notification system.
  • WebSocket Message Updates: Broadcast WebSocket messages to multiple components that need to react to real-time data updates, without coupling them directly.
  • Custom Event Handling: Create a flexible event system where you can add custom business logic to track event history, manage event-driven state changes, or implement complex interactions between components.

Overall, while global state managers such as "Redux" and "Zustand" are powerful tools, "useEventEmitter" provides a lightweight and versatile option for handling inter-component communication, offering a different approach that may fit well in many React applications.

Feel free to try out this hook in your projects! Let me know your thoughts, and happy coding!

Top comments (0)