Getting started General
Components
Forms
Trends
Utilities
Plugins Sass Migrate from v1
  Join us
  JavaScript Articles

JavaScript Publish/Subscribe: The Clear Guide to Subscribe, Subscriber & Pub/Sub

JavaScript

A straightforward look at the publish/subscribe pattern in JavaScript. You'll learn how to subscribe to events, what a subscriber actually does, how publish triggers your subscribers, and how to build a tiny yet powerful Pub/Sub utility you can drop into any app.

JavaScript publish/subscribe guide
Publish/Subscribe in JavaScript: subscribe, subscriber, publish, and unsubscribe

The publish/subscribe model (often shortened to pub/sub) helps you decouple features. A component publishes an event. Every subscriber that subscribed to that event runs its callback. The publisher doesn't know who subscribed. The subscriber doesn't need to know who published. The result: simple, testable code.

What does subscribe mean?

To subscribe is to register a callback for a named event. You can have one subscriber or many subscribers for the same event. When someone publishes that event, each subscriber's callback runs with the provided data.

// Minimal shape we aim for
// bus.subscribe(eventName, callback)
// bus.publish(eventName, ...args)

A tiny, robust Pub/Sub utility

Here's a clean implementation that covers the real-world needs: subscribe, publish, unsubscribe, and subscribeOnce. It's small, fast, and framework-agnostic.

export function createPubSub() {
  const subscribersByEvent = Object.create(null);

  function subscribe(eventName, callback) {
    if (typeof callback !== 'function') {
      throw new TypeError('subscriber callback must be a function');
    }
    if (!Array.isArray(subscribersByEvent[eventName])) {
      subscribersByEvent[eventName] = [];
    }
    subscribersByEvent[eventName].push(callback);

    // return unsubscribe handle for convenience
    return function unsubscribe() {
      const list = subscribersByEvent[eventName];
      if (!list) return;
      const index = list.indexOf(callback);
      if (index !== -1) list.splice(index, 1);
      if (list.length === 0) delete subscribersByEvent[eventName];
    };
  }

  function subscribeOnce(eventName, callback) {
    const unsubscribe = subscribe(eventName, function wrappedOnce(...args) {
      unsubscribe();
      callback(...args);
    });
    return unsubscribe;
  }

  function publish(eventName, ...args) {
    const list = subscribersByEvent[eventName];
    if (!list || list.length === 0) return 0;

    // copy to prevent mutation during publish from breaking iteration
    const currentSubscribers = list.slice();
    for (const subscriber of currentSubscribers) {
      try {
        subscriber(...args);
      } catch (error) {
        // In production, you might log this error. We keep the loop going
        // so one bad subscriber doesn't break others.
      }
    }
    return currentSubscribers.length;
  }

  function clear(eventName) {
    if (eventName) {
      delete subscribersByEvent[eventName];
    } else {
      for (const key in subscribersByEvent) delete subscribersByEvent[key];
    }
  }

  return { subscribe, subscribeOnce, publish, clear };
}

// Example usage
const bus = createPubSub();

const unsubscribeAlert = bus.subscribe('alert', (payload) => {
  console.log('Alert subscriber:', payload.message);
});

bus.publish('alert', { message: 'Hello, subscriber!' });

// Stop this subscriber later
unsubscribeAlert();

// Subscribe once
bus.subscribeOnce('ready', () => console.log('Ready fired once'));
bus.publish('ready');
bus.publish('ready'); // second call does nothing

Subscribers in the UI: simple wiring

A common pattern is keeping UI components lean. The form publishes events like form:success or form:error. A toast system subscribes to those events and shows messages. The form doesn't import the toast. The toast doesn't import the form.

// form.js
import { bus } from './shared-bus.js';

async function submitForm(data) {
  try {
    await api.save(data);
    bus.publish('form:success', { message: 'Saved!' });
  } catch (error) {
    bus.publish('form:error', { message: 'Please try again.' });
  }
}

// toast.js
import { bus } from './shared-bus.js';

bus.subscribe('form:success', ({ message }) => showToast(message, 'success'));
bus.subscribe('form:error', ({ message }) => showToast(message, 'error'));

Unsubscribe and memory safety

Always keep an unsubscribe handle. Remove the subscriber when a component unmounts or a page changes. This prevents dangling subscribers and memory leaks.

const stop = bus.subscribe('tick', () => updateTime());

// later
stop(); // unsubscribe this subscriber

Pattern tips and best practices

  • Name events consistently: Use namespaces like cart:add, user:login.
  • Return unsubscribe: Every subscribe should return a clean unsubscribe function.
  • Keep payloads simple: Prefer plain objects. Document required fields.
  • Don't overuse pub/sub: Direct function calls are fine when coupling is intentional.
  • Handle errors per subscriber: One broken subscriber should not halt others.
  • Provide subscribeOnce: Handy for one-off lifecycle moments like ready.

Typed channels (optional)

In TypeScript, you can type event names and payloads for safer subscribe and publish calls.

type Events = {
  'alert': { message: string };
  'ready': void;
};

function createTypedPubSub>() {
  const bus = createPubSub();
  return bus as {
    subscribe(event: K, cb: (payload: E[K]) => void): () => void;
    subscribeOnce(event: K, cb: (payload: E[K]) => void): () => void;
    publish(event: K, payload: E[K]): number;
    clear(event?: keyof E): void;
  };
}

const typedBus = createTypedPubSub();
typedBus.subscribe('alert', (p) => console.log(p.message));
typedBus.publish('alert', { message: 'Typed and safe' });

Conclusion

The publish/subscribe model gives you a clean way to connect parts of your app without tight coupling. With a lightweight utility, a simple subscribe API, and careful subscriber management, your code stays flexible and maintainable.

Start Building with Axentix

Ready to create amazing websites? Get started with Axentix framework today.

Get Started

Related Posts