TL;DR 🚌 We’ll write a lightweight Event Bus from scratch in just 60 lines! 🌱 We’ll learn the use case to best utilize Event Bus in React. 🏋️♀️ We’ll apply Event Bus in a demo with Google Maps API. I recently came across an interesting use case of Event Bus at work. It’s a very lean module to organize logging for analytics in a global-scale web application. It creates great clarity in a large code base so I want to share with you my case study of this useful design pattern. Let’s go. What Is An Event Bus? An Event Bus is a design pattern that allows PubSub-style communication between components while the components remain loosely coupled. A component can send a message to an Event Bus without knowing where the message is sent to. On the other hand, a component can listen to a message on an Event Bus and decide what to do with the message without knowing where the message comes from. With this design, independent components can communicate without knowing each other. The visualization looks like this: Event: The message being sent and received on an Event Bus. Publisher: The sender that an event. emits Subscriber: The receiver that to an event. listens Let’s take a closer look at Event Bus. Building An Event Bus from Scratch Inspired by , we’ll implement the following APIs for our Event Bus: Vue’s legacy Events API type EventHandler = (payload: any) => void interface EventBus { on(key: string, handler: EventHandler): () => void off(key: string, handler: EventHandler): void emit(key: string, ...payload: Parameters<EventHandler>): void once(key: string, handler: EventHandler): void } : for a subscriber to listen (subscribe) to an event and register its event handler. on : for a subscriber to remove (unsubscribe) an event and its event handler. off : for a subscriber to listen to an event only once. once : for a publisher to send an event to the event bus. emit Now, the data structure for our Event Bus should be capable of two things: For publishers: to be able to fire the registered event handlers associated with the event key when is called. emit For subscribers: to be able to add or remove event handlers when , , or is called. on once off We can use a key-value structure for it: type Bus = Record<string, EventHandler[]> To implement the method, all we need to do is to add the event key to the bus and append the event handler to the handler array. We also want to return an unsubscribe function to remove the event handler. on export function eventbus(config?: { // error handler for later onError: (...params: any[]) => void }): EventBus { const bus: Bus = {} const on: EventBus['on'] = (key, handler) => { if (bus[key] === undefined) { bus[key] = [] } bus[key]?.push(handler) // unsubscribe function return () => { off(key, handler) } } return { on } } To implement , we can simply remove the event handler from the bus. off const off: EventBus['off'] = (key, handler) => { const index = bus[key]?.indexOf(handler) ?? -1 bus[key]?.splice(index >>> 0, 1) } When is called, what we want to do is to fire all the event handlers that associate with the event. We’ll add error handling here to make sure all the event handlers will be fired despite errors. emit const emit: EventBus['emit'] = (key, payload) => { bus[key]?.forEach((fn) => { try { fn(payload) } catch (e) { config?.onError(e) } }) } Since will only listen to an event exactly once, we can think of it as a method that registers a handler that deregisters itself after firing. One way to do it is to create a higher order function : once handleOnce const once: EventBus['once'] = (key, handler) => { const handleOnce = (payload: Parameters<typeof handler>) => { handler(payload) off(key, handleOnce as typeof handler) } on(key, handleOnce as typeof handler) } Now we have all the methods in our Event Bus! Improve TypeScript Typing The current typing for the Event Bus is quite open-ended. The event key could be any string and the event handler could be any function. To make it safer to use, we can add type checking to add the event key and handler association to the . EventBus type EventKey = string | symbol type EventHandler<T = any> = (payload: T) => void type EventMap = Record<EventKey, EventHandler> type Bus<E> = Record<keyof E, E[keyof E][]> interface EventBus<T extends EventMap> { on<Key extends keyof T>(key: Key, handler: T[Key]): () => void off<Key extends keyof T>(key: Key, handler: T[Key]): void once<Key extends keyof T>(key: Key, handler: T[Key]): void emit<Key extends keyof T>(key: Key, ...payload: Parameters<T[Key]>): void } interface EventBusConfig { onError: (...params: any[]) => void } export function eventbus<E extends EventMap>( config?: EventBusConfig ): EventBus<E> { const bus: Partial<Bus<E>> = {} const on: EventBus<E>['on'] = (key, handler) => { if (bus[key] === undefined) { bus[key] = [] } bus[key]?.push(handler) return () => { off(key, handler) } } const off: EventBus<E>['off'] = (key, handler) => { const index = bus[key]?.indexOf(handler) ?? -1 bus[key]?.splice(index >>> 0, 1) } const once: EventBus<E>['once'] = (key, handler) => { const handleOnce = (payload: Parameters<typeof handler>) => { handler(payload) // TODO: find out a better way to type `handleOnce` off(key, handleOnce as typeof handler) } on(key, handleOnce as typeof handler) } const emit: EventBus<E>['emit'] = (key, payload) => { bus[key]?.forEach((fn) => { try { fn(payload) } catch (e) { config?.onError(e) } }) } return { on, off, once, emit } } Now we are instructing TypeScript that the has to be one of the and the handler should have the corresponding handler type. For example: key keyof T interface MyBus { 'on-event-1': (payload: { data: string }) => void } const myBus = eventbus<MyBus>() You should be able to see a clear type definition when developing. Use The Event Bus in React I created a application to demonstrate how to use the Event Bus we just built. Remix You can find the . GitHub repository for the demo here The demo showcases how to organize logging with the Event Bus in an isomorphic React application. I picked three events to log: : the event happens when the map finishes instantiating or a user finishes dragging or zooming the map. onMapIdle : the event happens when a user clicks on the map. onMapClick : the event happens when a user clicks on a map marker. onMarkerClick Let’s create two . One for the map, and one for the marker. event channels import { eventbus } from 'eventbus' export const mapEventChannel = eventbus<{ onMapIdle: () => void onMapClick: (payload: google.maps.MapMouseEvent) => void }>() import { eventbus } from 'eventbus' import type { MarkerData } from '~/data/markers' export const markerEventChannel = eventbus<{ onMarkerClick: (payload: MarkerData) => void }>() The reason to separate event channels is to create a clear separation of concerns. This pattern can grow horizontally when your application grows. Now, let’s use the event channels in React components. import { markers } from '~/data/marker' import { logUserInteraction } from '~/utils/logger' import { mapEventChannel } from '~/eventChannels/map' import { markerEventChannel } from '~/eventChannels/marker' export async function loader() { return json({ GOOGLE_MAPS_API_KEY: process.env.GOOGLE_MAPS_API_KEY, }) } export default function Index() { const data = useLoaderData() const portal = useRef<HTMLDivElement>(null) const [selectedMarker, setSelectedMarker] = useState<MarkerData>() useEffect(() => { // subscribe to events when mounting const unsubscribeOnMapIdle = mapEventChannel.on('onMapIdle', () => { logUserInteraction('on map idle.') }) const unsubscribeOnMapClick = mapEventChannel.on( 'onMapClick', (payload) => { logUserInteraction('on map click.', payload) } ) const unsubscribeOnMarkerClick = markerEventChannel.on( 'onMarkerClick', (payload) => { logUserInteraction('on marker click.', payload) } ) // unsubscribe events when unmounting return () => { unsubscribeOnMapIdle() unsubscribeOnMapClick() unsubscribeOnMarkerClick() } }, []) const onMapIdle = (map: google.maps.Map) => { mapEventChannel.emit('onMapIdle') setZoom(map.getZoom()!) const nextCenter = map.getCenter() if (nextCenter) { setCenter(nextCenter.toJSON()) } } const onMapClick = (e: google.maps.MapMouseEvent) => { mapEventChannel.emit('onMapClick', e) } const onMarkerClick = (marker: MarkerData) => { markerEventChannel.emit('onMarkerClick', marker) setSelectedMarker(marker) } return ( <> <GoogleMap apiKey={data.GOOGLE_MAPS_API_KEY} markers={markers} onClick={onMapClick} onIdle={onMapIdle} onMarkerClick={onMarkerClick} /> <Portal container={portal.current}> {selectedMarker && <Card {...selectedMarker} />} </Portal> <div ref={portal} /> </> ) } What we did was subscribe to the events in the component and emit the events when the map and markers interacted. Moreover, By subscribing and unsubscribing with the component’s lifecycle, we are able to register only the necessary event handlers at the given moment of the user journey. Index Final Thoughts If you are looking for an Event Bus library, there are a : couple of choices recommended by Vue.js Mitt tiny-emitter There’s also an interesting . One of the maintainers suggested a few Redux-based tools to handle events: discussion on Reddit about using Redux as an Event Bus redux-toolkit’s new Listener Middleware redux-observable Redux-Saga References GitHub: Mitt GitHub: tiny-emitter GitHub: redux-observable GitHub: Redux-Saga GitHub: eventbus-demo Documentation: Vue 3 Migration Guide — Events API Documentation: redux-toolkit’s new Listener Middleware Documentation: Remix This article is originally posted on here.