Learn how to build your own publish/subscribe system in plain JavaScript, no frameworks required.

Introduction: Why Care About the Observer Pattern?
You already use the Observer Pattern every day, even if you don’t know it:
addEventListener
in the browser- Node.js
EventEmitter
- RxJS in Angular
- Vue.js reactivity
The pattern boils down to one idea:
👉 One subject notifies many observers whenever it changes.
Think of YouTube subscriptions: the channel is the subject, and subscribers are the observers. When the channel posts a video, all subscribers receive a notification.
In this article, we’ll break it down with 7 progressively complex examples in Vanilla JS, no frameworks, no magic, just clean code.
1. A Minimal Observer Implementation
The simplest form: an object that can register observers and notify them.
class Subject {
constructor() {
this.observers = [];
}
subscribe(fn) {
this.observers.push(fn);
}
unsubscribe(fn) {
this.observers = this.observers.filter(sub => sub !== fn);
}
notify(data) {
this.observers.forEach(fn => fn(data));
}
}
// Usage
const subject = new Subject();
subject.subscribe((msg) => console.log("Observer 1:", msg));
subject.subscribe((msg) => console.log("Observer 2:", msg));
subject.notify("Hello Observers!");
✅ Output:
Observer 1: Hello Observers!
Observer 2: Hello Observers!
2. Turning DOM Events Into Observers
The browser’s event system is a built-in implementation of the Observer Pattern.
const button = document.querySelector("button");
function logClick(e) {
console.log("Button clicked!", e);
}
button.addEventListener("click", logClick);
Each event listener = an observer. The button = the subject.
✅ Every click notifies all registered observers.
3. Observer for Form Validation
Use the pattern to react to input changes.
class InputSubject {
constructor(input) {
this.input = input;
this.observers = [];
this.input.addEventListener("input", (e) => {
this.notify(e.target.value);
});
}
subscribe(fn) {
this.observers.push(fn);
}
notify(value) {
this.observers.forEach(fn => fn(value));
}
}
// Usage
const nameInput = new InputSubject(document.querySelector("#name"));
nameInput.subscribe((val) => {
console.log("Length:", val.length);
});
nameInput.subscribe((val) => {
console.log("Uppercase:", val.toUpperCase());
});
✅ As the user types, all observers react differently.
4. Building a Simple Event Bus
The Observer Pattern is the foundation of an event bus, useful for decoupling modules.
class EventBus {
constructor() {
this.events = {};
}
on(event, fn) {
(this.events[event] || (this.events[event] = [])).push(fn);
}
off(event, fn) {
this.events[event] = (this.events[event] || []).filter(sub => sub !== fn);
}
emit(event, data) {
(this.events[event] || []).forEach(fn => fn(data));
}
}
// Usage
const bus = new EventBus();
bus.on("login", user => console.log("User logged in:", user));
bus.on("logout", () => console.log("User logged out"));
bus.emit("login", { name: "Ali" });
bus.emit("logout");
✅ Output:
User logged in: { name: "Ali" }
User logged out
5. Stock Price Observer (Real-World Analogy)
Think of a stock ticker: subscribers want updates when the stock price changes.
class Stock {
constructor(symbol) {
this.symbol = symbol;
this.price = 100;
this.observers = [];
}
subscribe(fn) {
this.observers.push(fn);
}
setPrice(newPrice) {
this.price = newPrice;
this.notify();
}
notify() {
this.observers.forEach(fn => fn(this.symbol, this.price));
}
}
// Usage
const apple = new Stock("AAPL");
apple.subscribe((symbol, price) =>
console.log(`${symbol} is now $${price}`)
);
apple.setPrice(120);
apple.setPrice(130);
✅ Output:
AAPL is now $120
AAPL is now $130
6. Observer Pattern with Fetch & Async Data
You can use observers to react to async data (like APIs).
class DataFetcher {
constructor(url) {
this.url = url;
this.observers = [];
}
subscribe(fn) {
this.observers.push(fn);
}
async fetchData() {
const res = await fetch(this.url);
const data = await res.json();
this.notify(data);
}
notify(data) {
this.observers.forEach(fn => fn(data));
}
}
// Usage
const api = new DataFetcher("https://jsonplaceholder.typicode.com/posts");
api.subscribe(data => console.log("Got", data.length, "posts"));
api.fetchData();
✅ Observers react when data arrives.
7. A Mini Reactive State Store
The Observer Pattern = the foundation of frameworks like React/Vue. Let’s build a tiny reactive store.
class Store {
constructor(initialState) {
this.state = initialState;
this.observers = [];
}
subscribe(fn) {
this.observers.push(fn);
fn(this.state); // initial call
}
setState(newState) {
this.state = { ...this.state, ...newState };
this.notify();
}
notify() {
this.observers.forEach(fn => fn(this.state));
}
}
// Usage
const store = new Store({ count: 0 });
store.subscribe((state) => console.log("State updated:", state));
store.setState({ count: 1 });
store.setState({ count: 2 });
✅ Output:
State updated: { count: 0 }
State updated: { count: 1 }
State updated: { count: 2 }
That’s the core idea of React/Vue reactivity in <20 lines.
Wrapping It All Up
Here are 7 simple ways to implement the Observer Pattern in Vanilla JS:
- Minimal Subject/Observer class
- DOM events (
addEventListener
) - Form validation callbacks
- Event bus system
- Stock ticker analogy
- Async fetch observer
- Reactive state store
👉 Key takeaway: The Observer Pattern is about decoupling the subject, which doesn’t need to know what its observers do. It just notifies them.
Once you understand this, you’ll recognize it everywhere: events, sockets, streams, RxJS, even UI frameworks.
Call to Action
👉 Which example clicked for you the most? Drop it in the comments 👇.
📤 Share this with a teammate who’s struggling with design patterns.
🔖 Bookmark this post, you’ll need it next time you explain reactivity or events.
Leave a Reply