Skip to content

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.

The browser already has an event system. You can dispatch a custom event on window (or any DOM element) and listen for it elsewhere.

// Emitter
function ThemeToggle() {
function toggle() {
window.dispatchEvent(
new CustomEvent('theme:toggle', { detail: { theme: 'dark' } })
)
}
return <button onClick={toggle}>Dark mode</button>
}
// Listener
function 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.

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 emit
bus.emit('user:login', { userId: 'abc-123' })
// Any component can listen
useEffect(() => {
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.

ConcernPropsContextEvent Bus
Data flowExplicit, one-directionalImplicit, top-downFully decoupled
TraceabilityCall site shows everythingProvider in treeNo caller visibility
Re-render scopeChild subtreeAll consumersNone (manual control)
Type safetyNativeNativeDepends on implementation
Performance costProps that change re-renderAll consumers re-renderListener registration only
React integrationNativeNativeRequires 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.

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 BroadcastChannel or storage events.
  • 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.