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;
"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);
};
- 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]
);
-
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]);
- 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 };
- 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;
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;
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)