Observer Pattern: Reacting to State Changes

What is the Observer Pattern?
The Observer pattern defines a one-to-many dependency between objects so that when one object changes state, all its dependents are notified and updated automatically. It's the foundation of reactive and event-driven systems we use daily.
classDiagram
class Subject {
-observers: Observer[]
+attach(observer)
+detach(observer)
+notify()
}
class Observer {
<<interface>>
+update(state)
}
class ConcreteObserver {
-state
+update(state)
}
Subject "1" --> "*" Observer : notifies
Observer <|.. ConcreteObserverIf you've worked with React, Vue, or simply DOM events, you've already used this pattern without knowing it.
The Problem It Solves
Imagine you have a "source of truth" object (like a shopping cart state) and multiple components that need to react when that state changes:
- The item counter in the header
- The product list in the cart
- The total to pay
- A free shipping indicator
Without the Observer pattern, you'd have to manually update each component every time the cart changes.
Step by Step Implementation
1. Typed Event Emitter
type EventMap = Record<string, unknown>;
type EventCallback<T> = (data: T) => void;
/**
* Generic type-safe event emitter
* @template Events - Map of events and their data types
*/
class TypedEventEmitter<Events extends EventMap> {
private listeners = new Map<keyof Events, Set<EventCallback<any>>>();
/** Subscribe a callback to an event, returns unsubscribe function */
on<K extends keyof Events>(event: K, callback: EventCallback<Events[K]>): () => void {
if (!this.listeners.has(event)) {
this.listeners.set(event, new Set());
}
this.listeners.get(event)!.add(callback);
return () => this.off(event, callback);
}
off<K extends keyof Events>(event: K, callback: EventCallback<Events[K]>): void {
this.listeners.get(event)?.delete(callback);
}
emit<K extends keyof Events>(event: K, data: Events[K]): void {
this.listeners.get(event)?.forEach(callback => callback(data));
}
}2. Event Types
interface StoreEvents<T> {
change: { previous: T; current: T };
error: Error;
}3. Reactive Store
/**
* Reactive store with selective subscriptions
* @template T - State type
*/
class ReactiveStore<T extends object> extends TypedEventEmitter<StoreEvents<T>> {
private state: T;
constructor(initialState: T) {
super();
this.state = structuredClone(initialState);
}
getState(): Readonly<T> {
return structuredClone(this.state);
}
update(updater: (state: T) => void): void {
const previous = structuredClone(this.state);
updater(this.state);
this.emit('change', { previous, current: structuredClone(this.state) });
}
/** Subscribe only to changes in a specific part of the state */
subscribe(selector: (state: T) => unknown, callback: (state: T) => void): () => void {
let previousValue = selector(this.state);
return this.on('change', ({ current }) => {
const currentValue = selector(current);
if (previousValue !== currentValue) {
previousValue = currentValue;
callback(current);
}
});
}
}Practical Example: Shopping Cart
interface AppState {
user: { name: string; email: string } | null;
cart: { items: Array<{ id: string; quantity: number; price: number }> };
ui: { theme: 'light' | 'dark'; loading: boolean };
}
const store = new ReactiveStore<AppState>({
user: null,
cart: { items: [] },
ui: { theme: 'light', loading: false },
});
const unsubTheme = store.subscribe(
state => state.ui.theme,
state => console.log('Theme changed to:', state.ui.theme)
);
const unsubCart = store.subscribe(
state => state.cart.items.length,
state => console.log('Cart updated:', state.cart.items.length, 'items')
);
store.update(state => { state.ui.theme = 'dark'; });
store.update(state => { state.cart.items.push({ id: 'item1', quantity: 2, price: 29.99 }); });
unsubTheme();
unsubCart();When to Use Observer
| Situation | Recommendation |
|---|---|
| Typed event system | Recommended |
| Reactive store (global state) | Recommended |
| Real-time notifications | Recommended |
| Simple direct communication | Consider alternatives |
When NOT to Use Observer
- One-to-one communication: If there's only one receiver, a direct call is simpler
- Critical execution order: Notification order is not guaranteed
- Critical performance: Every change notifies all observers
Common Mistakes
- Memory leaks: Don't forget to unsubscribe when the component is destroyed
- Cascade updates: An observer that modifies state can cause infinite loops
- Over-notification: Notifying changes that aren't relevant to most observers
Conclusion
The Observer pattern is fundamental for building reactive and decoupled systems. It's the foundation of libraries like RxJS, Redux, MobX, and Vue's reactivity system.
In the next article we'll explore the Strategy Pattern, which allows us to swap algorithms at runtime.
Based on "Design Patterns: Elements of Reusable Object-Oriented Software" (Gang of Four).