Observer
The Observer pattern lets one subject notify many dependents (observers) when state changes. Event listeners, pub/sub, reactive streams, and most UI frameworks are all observer variants.
Classic + EventEmitter + RxJS
EXAMPLE
// 1) Hand-rolled — a Subject
class Subject {
constructor() { this.observers = new Set(); }
subscribe(fn) { this.observers.add(fn); return () => this.observers.delete(fn); }
notify(payload) { this.observers.forEach(fn => fn(payload)); }
}
const userCreated = new Subject();
const unsub = userCreated.subscribe(u => console.log('welcome', u.name));
userCreated.notify({ name: 'Ada' });
unsub();
// 2) Node EventEmitter — the standard library version
import { EventEmitter } from 'node:events';
class Cart extends EventEmitter {
add(item) {
this.items.push(item);
this.emit('item:added', item);
}
}
const cart = new Cart();
cart.on('item:added', item => updateBadge(item));
cart.once('checkout', () => log('first checkout'));
cart.off('item:added', updateBadge);
// 3) DOM events — observer baked in
button.addEventListener('click', () => console.log('clicked'));
// Generic — custom events
const bus = new EventTarget();
bus.addEventListener('order:paid', e => fulfil(e.detail));
bus.dispatchEvent(new CustomEvent('order:paid', { detail: { id: 'o_1' } }));
// 4) RxJS — observer pattern as composable streams
import { Subject, fromEvent, debounceTime, distinctUntilChanged, switchMap } from 'rxjs';
import { ajax } from 'rxjs/ajax';
const input = document.querySelector('#search');
fromEvent(input, 'input').pipe(
debounceTime(300),
map(e => e.target.value.trim()),
distinctUntilChanged(),
switchMap(q => ajax.getJSON(`/search?q=${q}`)),
).subscribe(results => render(results));
// 5) Vue / React — reactive systems are observer underneath
// A computed/derived value is an observer of its inputs.
// 6) Decoupling — fan-out events to multiple subsystems
orderEvents.on('order:created', sendEmail);
orderEvents.on('order:created', updateInventory);
orderEvents.on('order:created', logAudit);
// Producer doesn't know subscribers; trivial to add a 4th.
// 7) Async / queued — push events through Redis / Kafka / SQS for cross-process observers
// In-process: in-memory observer
// Cross-process: pub/sub via a broker
// 8) Anti-patterns
// • Forgetting to unsubscribe → memory leaks (and double-firing)
// • Synchronous heavy work in the listener → blocks the producer
// • Sharing mutable payloads — observers mutate, others see weird state
//
// Fixes:
// • Always return + call an unsubscribe function on teardown
// • Schedule heavy work via queueMicrotask / setImmediate / a worker
// • Freeze or shallow-copy the payload before notify
// 9) TypeScript — typed events
type Events = {
'user:created': { id: string; name: string };
'order:paid': { orderId: string; total: number };
};
class TypedBus<E> {
private map = new Map<keyof E, Set<(p: any) => void>>();
on<K extends keyof E>(name: K, fn: (p: E[K]) => void) {
if (!this.map.has(name)) this.map.set(name, new Set());
this.map.get(name)!.add(fn);
return () => this.map.get(name)!.delete(fn);
}
emit<K extends keyof E>(name: K, payload: E[K]) {
this.map.get(name)?.forEach(fn => fn(payload));
}
}
const bus2 = new TypedBus<Events>();
bus2.on('user:created', u => console.log(u.id, u.name)); // typed payload
Why it matters
Reach for Observer when one event needs many reactions. The big trap: forgetting to unsubscribe — leaks accumulate quietly and double-firing breaks invariants in ways that look like flaky tests.
Tip: Tweak the snippet with Try it Yourself », then sit the quiz at the bottom of the page.
Example
Example
class EventBus {
#subs = new Map();
on(evt, fn) { (this.#subs.get(evt) ?? this.#subs.set(evt, new Set()).get(evt)).add(fn); }
emit(evt, payload) { this.#subs.get(evt)?.forEach(fn => fn(payload)); }
}
Try it Yourself »
Exercise
Event-emitter call to register a listener.
bus.
('user.created', fn);
Two letters.
Discussion
Loading…