Event Bus in React and Beyond
Supercharge Your Component Communication with a Seamless Event Bus
I've been planning to write this article for years, but my good friend Rado Stankov beat me to it with his article "EventBus in React Applications."
Nevertheless, I will try to describe my approach :)
React is a powerful library for building user interfaces, but sometimes managing state and passing data between deeply nested components can be challenging. One effective solution to this problem is implementing an event bus, which allows components to communicate with each other through a centralized event handling system. In this blog post, we will explore how to create a custom event bus using TypeScript, enabling your components to dispatch and listen for events with ease, and discuss its versatility beyond React applications.
What is an Event Bus?
An event bus is a design pattern that facilitates PubSub-style communication between different parts of an application while keeping components loosely coupled. It acts as a central hub where components can send (dispatch) and receive (subscribe to) events without needing to know about each other.
Components can send messages to an event bus without knowing the recipients. Conversely, they can listen for messages and react without knowing the source. This design enables seamless communication between independent components, simplifying data flow and enhancing modularity.
My (~70 lines) Implementation
import { useEffect, useCallback } from 'react';
export type EventBusMessage<T extends string = string, M = unknown> = {
topic: T;
message: M;
};
type EventAction<T extends EventBusMessage> = {
topic: T['topic'];
data: T['message'];
};
type Filter<T extends EventBusMessage> = T['topic'] | ((event: EventAction<T>) => boolean);
type Subscriber<T extends EventBusMessage> = [Filter<T>, (event: EventAction<T>) => void];
const subscribers = new Set<Subscriber<EventBusMessage>>();
export const busSubscribe = <T extends EventBusMessage>(
filter: Filter<T>,
callback: (event: EventAction<T>) => void
): (() => void) => {
const newSubscriber: Subscriber<T> = [filter, callback];
subscribers.add(newSubscriber as Subscriber<EventBusMessage>);
return () => {
subscribers.delete(newSubscriber as Subscriber<EventBusMessage>);
};
};
export const busDispatch = <T extends EventBusMessage>(
topic: T['topic'],
message: T['message']
): void => {
const eventAction: EventAction<T> = { topic, data: message };
subscribers.forEach(([filter, callback]) => {
try {
if (
(typeof filter === 'string' && filter === eventAction.topic) ||
(typeof filter === 'function' && filter(eventAction as EventAction<EventBusMessage>))
) {
callback(eventAction as EventAction<EventBusMessage>);
}
} catch (error) {
console.error(`Error in event bus subscriber for topic "${topic}":`, error);
}
});
};
export const useEventBus = <T extends EventBusMessage>(
topic: T['topic'],
callback: (message: T['message']) => void,
deps: unknown[] = []
): void => {
const memoizedCallback = useCallback((event: EventAction<T>) => {
callback(event.data);
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [callback, ...deps]);
useEffect(() => {
const unsubscribe = busSubscribe<T>(topic, memoizedCallback);
return unsubscribe;
}, [topic, memoizedCallback]);
};
This implementation provides the basic functionality to subscribe to events and dispatch events. To use it in React, we simply wrap it in a React hook, enabling seamless integration and event management in your components.
I've also created an npm package :)
How to use it?
Now that we have our event bus set up, let's look at how to use it in two simple React components. This example demonstrates how to dispatch and subscribe to events using our custom event bus.
Let's create two components: ButtonComponent
to dispatch an event when a button is clicked, and MessageComponent
to listen for that event and display the message.
import React, { useCallback } from 'react';
import { busDispatch, useEventBus } from './eventBus';
// ButtonComponent to dispatch an event
const ButtonComponent = () => {
const handleOnClick = useCallback(() => {
busDispatch<any>('@@--show-message', 'Hello world');
}, []);
return <button onClick={handleOnClick}>Click Me</button>;
};
// MessageComponent to subscribe to the event
const MessageComponent = () => {
useEventBus<any>('@@--show-message', (message) => {
alert(message);
});
return null; // This component does not render anything
};
// App Component to include both ButtonComponent and MessageComponent
const App = () => {
return (
<div>
<ButtonComponent />
<MessageComponent />
</div>
);
};
export default App;
Explanation
ButtonComponent: This component contains a button. When the button is clicked, it dispatches an event of type
show-message
with a data payload containing the message "Hello world."MessageComponent: This component listens for events of type
show-message
. When such an event is received, it displays an alert with the message from the event data. Note that this component does not render anything to the DOM; it simply listens for events and reacts to them.App Component: This component includes both
ButtonComponent
andMessageComponent
. This setup demonstrates how theButtonComponent
can communicate with theMessageComponent
through the event bus.
By using the event bus, these components can communicate without needing to pass props or use a global state, making the code more modular and easier to manage.
Why Use an Event Bus?
When deciding how to manage state and communication in your React application, an event bus offers a unique combination of benefits that might make it the best choice for your needs.
Simplicity and Ease of Use: The event bus is straightforward to implement and use. It requires minimal setup and is easy to understand, making it accessible for developers of all skill levels.
Avoiding Prop Drilling: Prop drilling can make your code harder to manage and maintain. An event bus allows components to communicate directly without passing props through multiple layers of the component tree.
Performance and Modular Design: An event bus targets specific events, minimizing unnecessary updates and enhancing performance. By decoupling components, an event bus improves modularity. Components can subscribe to and dispatch events independently, making the system more flexible and easier to maintain.
Final thoughts 😉
Choosing the right state management or event handling system for your React application depends on your specific needs. An event bus provides a simple, modular approach to managing events and decoupling components, making it an excellent choice for many scenarios.
Moreover, its cross-platform usability makes it a valuable tool across different JavaScript environments.
By implementing a custom event bus, you gain a flexible and powerful way to handle events in your web application, enhancing both its modularity and maintainability.