Build better React apps with Pub/Sub
How to create a global event bus in React & Typescript
While building Seam, I came across a tricky software design challenge. I needed to decouple several components to have a separate header from the main body of the application. But that created a problem: how could the publish button talk to the main component?
The Event Bus Pattern — aka “PubSub”
PubSub stands for “Publish/Subscribe” and is a software pattern to send and receive events from around the application.
An Event Bus is a design pattern that allows PubSub-style communication between components while the components remain loosely coupled.
Implementing an event bus would solve my problem: the publish button could fire a publish event, and then the main body component could listen for the event to start the publish process. Everything would be:
- Reuseable. The same listener can be invoked from anywhere in the app. No need to write duplicate logic.
- Faster to build. New components can quickly be built, and fire the same events even when they are refactored to different locations in the app.
- Less Buggy. Typescript allows for events to only receive the data they expect.
Let’s get started!
Creating the Event Bus Emitter
Luckily, several lightweight libraries already exist to create an event bus in Typescript. I opted for Mitt because it’s super small by having no dependencies. You can read this article if you want to try writing your own.
Install it to your node dependencies using either npm or yarn in the standard fashion:
yarn add mitt
There are only two important functions in a pub/sub library: firing an event, and listening for an event:
import mitt from 'mitt'
const emitter = mitt()
// listen to an event
emitter.on('foo', e => console.log('foo', e) )
// listen to all events
emitter.on('*', (type, e) => console.log(type, e) )
// fire an event
emitter.emit('foo', { a: 'b' })
Great! So our publish button can fire a publish event, and our main component can listen for that event, and call the appropriate function.
Unfortunately we aren’t done that easily — we still need to make sure that the emitter (and the same instance of the emitter) is accessible from anywhere in the application. If we just call const emitter = mitt()
in each component, it’ll be a new instance of the event bus and not have all the listeners from elsewhere in the application.
Global Variables React — the easiest way to get global state
Typically, global state is tricky to accomplish in React. You either need to adopt a very heavy framework like Redux, or pass around parameters as props and callbacks.
Warning- don’t try to pass the emitter as a prop. If you pass the mitt
emitter to children as props, they won’t be updated as new components add themselves as listeners, because the emitter can’t flow back up the stack. You’ll have weird bugs as there won’t be a single source of truth for the listeners list.
To solve this challenge, we need to make a single instance of the event bus global. The easiest way to do this is to add it to the React window:
// eventEmitter.d.ts
import mitt, { Emitter } from 'mitt';
declare global {
interface Window {
emitter: Emitter<Events>;
}
}
Now we can simply grab the emitter from any component, and register new listeners and emit events:
useEffect(() => {
// subscribe to events when mounting
window.emitter.on('SEAM_EVENT_TOGGLE_MOBILE', (deviceType) => setIsMobileMode(deviceType == "mobile"))
}, [])
Firing events is just as easy:
window.emitter.emit("SEAM_EVENT_TOGGLE_MOBILE", "mobile")
Use Typescript for your events — profit!
The last piece of the puzzle will significantly increase your coding velocity and eliminate bugs. Use typescript to define events for your event bus. Then, you’ll be able to use autocomplete in VSCode to code faster and make sure you are only using events that exist (no more misspellings of event strings!)
Add an event type to your emitter type file:
// eventEmitter.d.ts
import mitt, { Emitter } from 'mitt';
export type Events = {
SEAM_EVENT_TOGGLE_MOBILE: "mobile" | "desktop";
SEAM_EVENT_TOGGLE_MODE: "edit" | "view";
};
Creating an Events type then allows the typing autocorrect to help us as we write event code. VSCode will even provide the list of all our events, and make sure we are providing the right types in the listener functions!
Conclusion
The Event Bus pattern in React allows loosely coupled components, increasing reusability and reducing bugs. Using Typescript allows for autocomplete as you code, make it fast and easy to make new expansions on your product. Good luck!