Skip to contentJMRG
All posts

Observer Pattern: Reacting to State Changes

design-patternsbehavioralobserver

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 <|.. ConcreteObserver

If 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

  1. Memory leaks: Don't forget to unsubscribe when the component is destroyed
  2. Cascade updates: An observer that modifies state can cause infinite loops
  3. 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).