,

7 Simple Examples of the Observer Pattern in Vanilla JS

Posted by

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:

  1. Minimal Subject/Observer class
  2. DOM events (addEventListener)
  3. Form validation callbacks
  4. Event bus system
  5. Stock ticker analogy
  6. Async fetch observer
  7. 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

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