Observer and Pub/Sub Patterns in JavaScript

· 5 min read · Updated March 17, 2026 · intermediate
design-patterns observer events javascript

The observer pattern is a fundamental design pattern that establishes a one-to-many dependency between objects. When one object changes state, all its dependents are automatically notified. This pattern is everywhere in JavaScript—from DOM event handlers to React’s state management and Node.js’s EventEmitter. Understanding it deeply will make you a more effective JavaScript developer.

What Is the Observer Pattern?

In the observer pattern, there are two main actors: the subject (also called the observable) and the observers. The subject maintains a list of observers and notifies them whenever something interesting happens. This creates a loose coupling between the subject and observers—they don’t need to know about each other explicitly.

The key benefits are decoupling and flexibility. You can add or remove observers at runtime without changing the subject’s code. This makes your code more maintainable and easier to extend.

Building a Class-Based Observer

Let’s implement a simple observable class that allows observers to subscribe and receive updates:

class Observable {
  constructor() {
    this.observers = [];
  }

  subscribe(observer) {
    this.observers.push(observer);
    // Return unsubscribe function
    return () => {
      this.observers = this.observers.filter(obs => obs !== observer);
    };
  }

  notify(data) {
    this.observers.forEach(observer => observer(data));
  }
}

// Usage example
const newsFeed = new Observable();

const subscriberA = (article) => {
  console.log(`Subscriber A received: ${article.title}`);
};

const subscriberB = (article) => {
  console.log(`Subscriber B received: ${article.title}`);
};

// Subscribe to updates
newsFeed.subscribe(subscriberA);
newsFeed.subscribe(subscriberB);

// Publish new article
newsFeed.notify({ title: "JavaScript Patterns", content: "..." });

// Unsubscribe when done
const unsubscribe = newsFeed.subscribe((article) => {
  console.log(`One-time subscriber: ${article.title}`);
});
unsubscribe(); // Stop receiving updates

This implementation shows the core concepts: observers register themselves, the observable stores them, and when something happens, the observable iterates through all observers and calls their update functions.

The Pub/Sub Pattern

Pub/Sub (Publish/Subscribe) is a variation of the observer pattern that adds an intermediary called the event bus or message broker. Instead of subscribing directly to a specific object, publishers emit events to named channels, and subscribers listen to those channels.

This additional layer of indirection provides even looser coupling. Publishers and subscribers don’t need to know about each other at all—they communicate entirely through the event bus.

Implementing a Custom Event Emitter

Here’s a practical EventEmitter implementation that follows the pub/sub model:

class EventEmitter {
  constructor() {
    this.events = {};
  }

  on(event, callback) {
    if (!this.events[event]) {
      this.events[event] = [];
    }
    this.events[event].push(callback);
    
    // Return unsubscribe function
    return () => {
      this.events[event] = this.events[event].filter(cb => cb !== callback);
    };
  }

  emit(event, ...args) {
    if (this.events[event]) {
      this.events[event].forEach(callback => callback(...args));
    }
  }

  once(event, callback) {
    const wrapper = (...args) => {
      callback(...args);
      this.off(event, wrapper);
    };
    return this.on(event, wrapper);
  }

  off(event, callback) {
    if (this.events[event]) {
      this.events[event] = this.events[event].filter(cb => cb !== callback);
    }
  }
}

// Usage with named channels
const emitter = new EventEmitter();

// Subscribe to specific events
emitter.on('user:login', (user) => {
  console.log(`${user.name} logged in`);
});

emitter.on('user:login', (user) => {
  analytics.track('login', { userId: user.id });
});

emitter.on('user:logout', (user) => {
  console.log(`${user.name} logged out`);
});

// Emit events
emitter.emit('user:login', { name: 'Alice', id: 1 });
emitter.emit('user:logout', { name: 'Alice', id: 1 });

This EventEmitter supports multiple handlers per event, one-time subscriptions with once(), and clean removal with off().

Real-World Use Cases

Event Systems

The most obvious use case is building custom event systems. When you’re coordinating multiple components that need to react to user actions or data changes, pub/sub provides a clean communication channel without tight coupling.

// A simple event bus for application-wide communication
const EventBus = new EventEmitter();

// Component A publishes
function saveUserData(user) {
  database.save(user)
    .then(() => EventBus.emit('user:saved', user))
    .catch(err => EventBus.emit('user:error', err));
}

// Component B subscribes
EventBus.on('user:saved', () => {
  ui.showNotification('User saved successfully');
  router.navigate('/dashboard');
});

State Management

For medium-complexity applications, you can build a simple state management system using the observer pattern:

function createStore(initialState) {
  const state = { ...initialState };
  const emitter = new EventEmitter();

  return {
    getState() {
      return { ...state };
    },
    setState(updater) {
      const newState = typeof updater === 'function' 
        ? updater(state) 
        : updater;
      Object.assign(state, newState);
      emitter.emit('change', state);
    },
    subscribe(callback) {
      return emitter.on('change', callback);
    }
  };
}

// Using the store
const store = createStore({ count: 0, user: null });

store.subscribe((state) => {
  console.log('State changed:', state);
});

store.setState({ count: 1 });
store.setState((prev) => ({ count: prev.count + 1 }));

Decoupling API Calls from UI

When your application has multiple components that need to react to data fetched from an API, pub/sub prevents the API layer from knowing about your UI components:

const API = {
  async fetchUsers() {
    const users = await fetch('/api/users').then(r => r.json());
    EventBus.emit('users:loaded', users);
    return users;
  }
};

// Any component can subscribe without modifying API
const UserList = {
  init() {
    EventBus.on('users:loaded', (users) => this.render(users));
  },
  render(users) {
    console.log('Rendering', users.length, 'users');
  }
};

When to Use Each Pattern

Choose the basic observer pattern when you have a clear subject-observer relationship and observers need to react to a specific object’s state. Use pub/sub when you need multiple publishers and subscribers to communicate through a central hub, or when you want to completely decouple components from each other.

Both patterns are essential tools in your JavaScript toolkit. They appear everywhere in modern frameworks—React’s component lifecycle events, Vue’s reactivity system, Node.js streams, and browser DOM events all use variations of these patterns under the hood.