iwantcoding.com
🔥 Daily 👥 Rooms 🏆 Top Log in Sign up

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);

Test yourself

Q1. Observer decouples…
Q2. Modern frameworks expose Observer as…
Q3. A classic pitfall is…

Discussion

Loading…