, ,

The Easiest Way to Understand Publish–Subscribe in JavaScript

Posted by

Stop wiring callbacks manually, learn how pub–sub makes your code modular, scalable, and surprisingly elegant.

Stop wiring callbacks manually, learn how pub–sub makes your code modular, scalable, and surprisingly elegant.

Introduction

Let’s be honest, JavaScript apps are full of “when this happens, do that” logic.

  • When a user clicks → update the UI.
  • When data loads → show a spinner.
  • When payment succeeds → send an email.

It’s easy to start with callbacks or addEventListener()but as your app grows, everything ends up tangled together.

That’s where the Publish–Subscribe pattern (or Pub–Sub) shines.

It lets you decouple your code so that parts of your app can communicate without knowing about each other.

By the end of this guide, you’ll not only understand how Pub–Sub works, but you’ll know how to build your own version from scratch, use it in real-world scenarios, and recognize it in frameworks you already use.


1️. What Is the Publish–Subscribe Pattern?

At its core, the pattern has three players:

  1. Publisher → Emits or broadcasts an event.
  2. Subscriber → Listens for specific events.
  3. Channel (or Event Bus) → Handles communication between them.

Analogy:
Think of it like a radio station:

  • The station (publisher) sends a signal.
  • Your radio (subscriber) tunes into that frequency.
  • They never directly talk to each other, only through the airwaves (channel).

So when the publisher emits "news", all subscribers of "news" Receive it instantly.

That’s the essence of Pub–Sub.


2️. Why Use Pub–Sub?

As apps grow, direct function calls create tight coupling:

// tightly coupled
function fetchUser() {
// ...
updateUI();
logActivity();
sendNotification();
}

The fetchUser function now knows too much.
 If one function changes, everything else breaks.

Pub–Sub fixes this by turning the flow into events:

eventBus.publish("user:fetched", userData);

Now, anyone can subscribe to "user:fetched" and react without modifying the core logic.

✅ Better modularity
✅ Easier debugging
✅ Flexible event-driven design


3️. Building a Simple Pub–Sub from Scratch

Let’s implement it step by step.

Step 1: Create an event bus

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

subscribe(event, callback) {
if (!this.events[event]) this.events[event] = [];
this.events[event].push(callback);
}

publish(event, data) {
if (!this.events[event]) return;
this.events[event].forEach((callback) => callback(data));
}
}

That’s it, a working Pub–Sub system in 10 lines.

Usage:

const bus = new EventBus();

bus.subscribe("user:login", (user) => {
console.log("Welcome,", user.name);
});

bus.subscribe("user:login", () => {
console.log("Log: user logged in");
});

bus.publish("user:login", { name: "Umar" });

Output:

Welcome, Umar
Log: user logged in

✅ One event, multiple reactions
✅ Publisher doesn’t care who’s listening


4️. Adding Unsubscribe Support

In real apps, you’ll want to remove listeners to avoid memory leaks.

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

subscribe(event, callback) {
if (!this.events[event]) this.events[event] = [];
this.events[event].push(callback);

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

publish(event, data) {
if (!this.events[event]) return;
this.events[event].forEach((callback) => callback(data));
}
}

Usage:

const unsubscribe = bus.subscribe("data:update", (data) =>
console.log("Data:", data)
);

bus.publish("data:update", { id: 1 }); // works
unsubscribe();
bus.publish("data:update", { id: 2 }); // no output

✅ Now you can clean up listeners dynamically.


5️. Real-World Example Cross-Component Communication

Imagine two completely separate modules:

Header.js

bus.subscribe("theme:change", (theme) => {
document.body.className = theme;
});

Settings.js

const toggleTheme = () => {
const theme = document.body.className === "dark" ? "light" : "dark";
bus.publish("theme:change", theme);
};
document.querySelector("#toggle").addEventListener("click", toggleTheme);

Now, Header.js updates instantly when it Settings.js triggers the event 
 without either file knowing about the other.

✅ True decoupling
✅ Zero import dependencies


6️. Real-World Example Analytics Tracking

bus.subscribe("page:viewed", (data) => {
console.log("Tracking page:", data.url);
});

bus.publish("page:viewed", { url: "/dashboard" });

Even if your tracking logic changes later (Google Analytics → Mixpanel), you don’t touch your core UI logic, just the subscriber.

✅ Separation of concerns
✅ Easier to maintain and replace modules


7️. Async Pub–Sub for Real APIs

Sometimes events depend on asynchronous data.
 You can use Promises or async callbacks:

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

subscribe(event, callback) {
if (!this.events[event]) this.events[event] = [];
this.events[event].push(callback);
}

async publish(event, data) {
if (!this.events[event]) return;
for (const callback of this.events[event]) {
await callback(data);
}
}
}

Usage:

const bus = new AsyncBus();

bus.subscribe("order:placed", async (order) => {
await sendEmail(order);
console.log("Email sent!");
});

bus.publish("order:placed", { id: 101, total: 499 });

✅ Perfect for network-driven or I/O-heavy events
✅ Keeps logic readable and modular


8️. Pub–Sub in Frameworks You Already Use

Pub–Sub isn’t just a theory, it’s everywhere:

When you call socket.on('message', ...) In Socket.IO, you’re literally using Pub–Sub under the hood.


9️. Advanced: Namespaced Events

As apps grow, naming collisions can happen.
 You can create namespaced events (like "user:login" or "chat:new") or even wildcard matching.

bus.subscribe("user:*", (event) => console.log("User event triggered:", event));
bus.publish("user:logout", { id: 3 });

✅ Adds flexibility for large-scale apps
✅ Mirrors event systems in Redux and WebSocket-based architectures


10. Real Analogy Coffee Shop Broadcast

Think of it like this:

Barista (Publisher): announces “Order ready: Latte!”
👂 Customers (Subscribers): only those waiting for a latte respond.
🎙️ Microphone (Event Bus): transmits the message to everyone in the shop.

If no one’s waiting for a latte, nothing happens.

That’s literally how Pub–Sub works.


Why It Matters

The Pub–Sub pattern turns your codebase from “do this, then that” spaghetti
 into event-driven flow, where parts of your app talk through a shared event system.

✅ Loosely coupled architecture
✅ More scalable, maintainable code
✅ Easier debugging and testing
✅ Foundation for real-time systems (WebSockets, Redis, MQTT)

It’s not just a pattern; it’s the backbone of how modern apps communicate.


Conclusion

Pub–Sub makes your code modular and powerful by decoupling communication:

  1. Publishers emit events without caring who listens.
  2. Subscribers react without needing to know who triggered it.
  3. The event bus connects them invisibly.

You can implement it in under 15 lines, yet it scales to millions of messages in real systems.

✅ Understand it once, and you’ll start seeing it everywhere.

The easiest way to understand Pub–Sub is to build it once and then use it everywhere.


Call to Action

Have you used Pub–Sub in your own projects, maybe without realizing it?
 Drop a comment 👇 with your favorite example (or a tricky bug you solved with it).

And if your teammate still wires components together manually, share this post and set them free from callback hell.

One response

Leave a Reply

Your email address will not be published. Required fields are marked *