Pub Sub vs Observer
Separate publishers from subscribers vs subject–observer coupling — when each fits UI apps, and a tiny EventEmitter sketch.
- javascript
- pub-sub
Both patterns fan out “something happened” to multiple listeners. The difference is coupling and topology.
Observer
A subject keeps a list of observers and notifies them directly. Observers know the subject (or at least subscribe on it). Classic OOP textbook:
class Subject {
constructor() {
this.observers = new Set();
}
subscribe(fn) {
this.observers.add(fn);
return () => this.observers.delete(fn);
}
notify(data) {
for (const fn of this.observers) fn(data);
}
}
const weather = new Subject();
const unsub = weather.subscribe((t) => console.log('temp', t));
weather.notify(22);
unsub();
DOM events are observer-like: you call element.addEventListener on a known target.
Pub/Sub
Publishers emit named events on a broker (bus). Publishers don’t hold subscriber lists; subscribers don’t know who published. Coupling is to the channel name, not the instance.
function createBus() {
const map = new Map(); // event -> Set<fn>
return {
on(type, fn) {
if (!map.has(type)) map.set(type, new Set());
map.get(type).add(fn);
return () => map.get(type)?.delete(fn);
},
emit(type, payload) {
map.get(type)?.forEach((fn) => fn(payload));
},
off(type, fn) {
map.get(type)?.delete(fn);
},
};
}
const bus = createBus();
bus.on('cart:add', (item) => console.log(item));
bus.emit('cart:add', { id: 'sku_1' });
Comparison
| Observer | Pub/Sub | |
|---|---|---|
| Knows peers? | Subject knows observers | Via channel only |
| Discovery | Hold subject ref | Agree on event names |
| Risk | Tight object graphs | Global bus spaghetti |
| UI fit | Component state, DOM nodes | Cross-feature app events |
Real frontend mapping
- React props/callbacks / context — structured data flow; not a free bus
- Redux / Zustand subscriptions — store as subject
- CustomEvent on
window— pub/sub with DOM as broker (origin-wide noise) - Node
EventEmitter— named events on an instance (hybrid)
Footguns shared by both
- Forgotten unsubscribe → leaks (especially SPA navigations)
- Sync notify re-entrancy — emit inside a handler that emits again
- Error in one subscriber killing the loop — isolate:
notify(data) {
for (const fn of this.observers) {
try { fn(data); }
catch (e) { console.error(e); }
}
}
- Event name sprawl — treat names as an API; version or namespace (
checkout:paid)
When to pick which
- Observer: lifecycle tied to one object (model, store, DOM node).
- Pub/Sub: distant features must react without importing each other — then constrain the bus (modules own channels, not one god object).
Interview answer (out loud)
“Observer: subject holds observers and notifies them. Pub/sub: a broker routes named events so publishers and subscribers stay decoupled. DOM is observer-like; an app-wide event bus is pub/sub. Both need unsubscribe discipline to avoid SPA leaks.”
Once listeners
function once(bus, type, fn) {
const off = bus.on(type, (payload) => {
off();
fn(payload);
});
return off;
}
Common for handshake events (ready). Built into Node’s EventEmitter as once.
Typing event maps (TypeScript sketch)
type Events = {
'cart:add': { id: string };
'cart:clear': void;
};
// bus.on/emit keyed by Events — prevents stringly-typed chaos
Treat the event map as a versioned public API of the module.
Further reading
Related
Related guides
- Event Emitter PatternImplement on/off/emit with Map of Sets — once, error isolation, memory leaks, and how this differs from DOM events.
- AbortControllerCancel fetch and other async work with AbortController and AbortSignal — timeouts, race conditions, and cleanup when components unmount.
- Array find, some, every, includesShort-circuiting array predicates: find, findIndex, some, every, and includes — when to use each and common interview traps.
- Array map, filter, reducemap, filter, and reduce as the core transform toolkit — immutability, chaining costs, reduce patterns, and when a plain loop is clearer.
- Arrow Functions Deep DiveLexical this, no arguments object, no construct, concise bodies — when arrows help and when methods and generators need classic functions.