mdashikjs/blog
All posts
The Observer Pattern Is Everywhere in JavaScript — Once You See It
Frontend

The Observer Pattern Is Everywhere in JavaScript — Once You See It

Frontend4 min

The Observer Pattern Is Everywhere in JavaScript — Once You See It

EventEmitter, DOM events, React state, Redux subscriptions, RxJS — it is all one pattern wearing different hats. Understanding it once makes every one of them easier.

JavaScriptDesign PatternsReact
Share:

The shape

One subject holds state. Observers register interest. When state changes, the subject notifies them. That is it.

function createObservable(initial) {
  let value = initial;
  const listeners = new Set();

  return {
    get: () => value,
    set: (next) => {
      value = next;
      listeners.forEach((fn) => fn(value));
    },
    subscribe: (fn) => {
      listeners.add(fn);
      return () => listeners.delete(fn);
    },
  };
}

Thirty lines. Every store library in the ecosystem is a variation on this.

Why subscribe returns a function

That tiny detail is load-bearing. Returning an unsubscribe function means the caller never has to remember how they subscribed — they just keep the returned function and call it on cleanup. Compare:

// Clunky
source.addEventListener('change', handler);
// later...
source.removeEventListener('change', handler); // need the same handler reference

// Clean
const unsubscribe = source.subscribe(handler);
// later...
unsubscribe();

React's useEffect cleanup expects exactly this shape. Zustand, Jotai, and every modern state library uses it. DOM events are the odd one out.

Where it shows up in React

useSyncExternalStore is literally a hook that subscribes to an observable. You write the two halves — subscribe and getSnapshot — and React handles the rerender:

const value = useSyncExternalStore(
  store.subscribe,
  store.get
);

That is the bridge between any observable and React. If a library exposes the pattern, you can use it in React without an adapter.

The memory leak trap

Observables without cleanup are leaks. I spent a day chasing why a dashboard got slower every navigation. It was a chart component subscribing to a global store in useEffect — without returning the unsubscribe.

useEffect(() => {
  store.subscribe(setData); // BUG: no cleanup
}, []);

Every mount added a listener. None were ever removed. The fix:

useEffect(() => store.subscribe(setData), []);

Returning the unsubscribe directly makes it almost impossible to forget.

When to skip it

The pattern is overkill for request/response. If code needs a value right now, just call a function. Observers are for things that change over time and have multiple consumers. A single caller fetching once is a function call, not an observable.

One pattern, many libraries

Once you see the shape, RxJS stops feeling alien. Observables are just subscribable subjects with operators layered on top. MobX is observables with automatic subscription tracking. Redux is one big observable with a reducer. They differ in ergonomics, not in fundamentals.

MA

Written by Md Ashik

Senior Software Engineer building reliable backends. I write about the practical tradeoffs behind shipping software that holds up in production.