Event Bus
An event bus lets components communicate without any direct reference to each other. One component emits an event; another component (anywhere in the app) listens for it. This is the publish-subscribe pattern, and it’s the most decoupled form of component communication React offers — because it doesn’t use React at all.
Custom events with the DOM
Section titled “Custom events with the DOM”The browser already has an event system. You can dispatch a custom event on window (or any DOM element) and listen for it elsewhere.
// Emitterfunction ThemeToggle() { function toggle() { window.dispatchEvent( new CustomEvent('theme:toggle', { detail: { theme: 'dark' } }) ) } return <button onClick={toggle}>Dark mode</button>}
// Listenerfunction StatusBar() { useEffect(() => { const handler = (e: CustomEvent) => { console.log('Theme changed to', e.detail.theme) } window.addEventListener('theme:toggle', handler) return () => window.removeEventListener('theme:toggle', handler) }, []) return <div>Status Bar</div>}No common ancestor needed. No provider wrapping the tree. The emitter and listener can be anywhere in the component hierarchy — or even outside React entirely.
Event bus libraries
Section titled “Event bus libraries”For more structure, libraries like mitt provide a typed event bus in under 200 bytes.
import mitt from 'mitt'
type Events = { 'theme:toggle': { theme: string } 'user:login': { userId: string } 'notification:show': { message: string; type: 'info' | 'error' }}
const bus = mitt<Events>()
// Any component can emitbus.emit('user:login', { userId: 'abc-123' })
// Any component can listenuseEffect(() => { bus.on('user:login', ({ userId }) => fetchUserProfile(userId)) return () => bus.off('user:login', handler) // cleanup!}, [])Mitt exposes three methods: on, off, and emit. No classes, no instantiation ceremony. Just a typed map of event names to handlers.
The Node.js EventEmitter pattern (used by libraries like React Native’s DeviceEventEmitter) works the same way but with a class API. If you need wildcards or more sophisticated event handling, eventemitter3 is a popular choice.
Trade-offs vs context vs props
Section titled “Trade-offs vs context vs props”| Concern | Props | Context | Event Bus |
|---|---|---|---|
| Data flow | Explicit, one-directional | Implicit, top-down | Fully decoupled |
| Traceability | Call site shows everything | Provider in tree | No caller visibility |
| Re-render scope | Child subtree | All consumers | None (manual control) |
| Type safety | Native | Native | Depends on implementation |
| Performance cost | Props that change re-render | All consumers re-render | Listener registration only |
| React integration | Native | Native | Requires manual lifecycle |
The event bus wins on decoupling and re-render avoidance. It loses on traceability — nothing in the source code tells you who emits or listens to a given event. This makes debugging harder as the app grows. An event that five components emit and eight listen to is hard to reason about.
When to use an event bus
Section titled “When to use an event bus”Event buses work well for:
- Cross-cutting UI events — toast notifications, global loading states, keyboard shortcuts.
- Cross-window communication — syncing state between browser tabs using
BroadcastChannelorstorageevents. - Third-party integration — a script tag in the page that needs to notify your React app about something.
- Legacy code interop — bridging a jQuery plugin or vanilla JS module to React state.