Codeskill

Learn to code, step by step

Observables / reactive patterns practical

A look at reactive patterns in plain JavaScript. Observables, subscriptions, and push-based updates when many parts of the UI care about the same changing values. You do not need RxJS to understand the idea; we build a small, practical version first.

Pull vs push

A store with subscribe is already reactive in a loose sense: when state changes, subscribers get pushed the new value. Observables generalise that pattern to streams of values over time – user input, timers, WebSocket messages, not just application state.

Pull: ask for data when you need it (getState(), fetch). Push: register once, receive updates as they happen.

A tiny observable

export function createObservable(subscribeFn) {
  return {
    subscribe(observer) {
      const wrapped =
        typeof observer === 'function'
          ? { next: observer }
          : observer;

      const unsubscribe = subscribeFn(wrapped);
      return typeof unsubscribe === 'function' ? unsubscribe : () => {};
    },
  };
}
const clicks = createObservable(({ next }) => {
  const handler = (event) => next(event);
  document.addEventListener('click', handler);
  return () => document.removeEventListener('click', handler);
});

const unsub = clicks.subscribe({
  next(event) {
    console.log('clicked', event.target);
  },
});

The teardown function returned from the producer is critical. Without it, every subscription leaks listeners.

From DOM events to streams

export function fromEvent(target, eventName) {
  return createObservable(({ next }) => {
    const handler = (event) => next(event);
    target.addEventListener(eventName, handler);
    return () => target.removeEventListener(eventName, handler);
  });
}

export function map(observable, fn) {
  return createObservable(({ next, error, complete }) =>
    observable.subscribe({
      next: (value) => next(fn(value)),
      error,
      complete,
    })
  );
}
const searchInput = document.querySelector('#search');
const input$ = fromEvent(searchInput, 'input');

const query$ = map(input$, (event) => event.target.value.trim());

query$.subscribe((query) => {
  filterList(query);
});

Debouncing with operators

Search-as-you-type should not hit the server on every keypress. Debounce waits for a pause before emitting.

export function debounce(observable, ms) {
  return createObservable(({ next, error, complete }) => {
    let timer;

    return observable.subscribe({
      next(value) {
        clearTimeout(timer);
        timer = setTimeout(() => next(value), ms);
      },
      error,
      complete,
    });
  });
}

debounce(query$, 300).subscribe((query) => {
  fetchResults(query);
});

Bridging stores and observables

Your store can expose an observable side for reactive UI while keeping a simple getState for one-off reads.

export function storeToObservable(store) {
  return createObservable(({ next }) => {
    return store.subscribe(next);
  });
}

storeToObservable(taskStore).subscribe((state) => {
  document.title = `${state.tasks.length} tasks`;
});

Signals (lightweight reactivity)

Signals are a newer primitive: a value plus subscribers, often with automatic dependency tracking in computed values. The platform may add signals natively; you can approximate them today.

export function signal(initial) {
  let value = initial;
  const subs = new Set();

  return {
    get() {
      return value;
    },
    set(next) {
      if (Object.is(value, next)) return;
      value = next;
      subs.forEach((fn) => fn(value));
    },
    subscribe(fn) {
      subs.add(fn);
      return () => subs.delete(fn);
    },
  };
}

const count = signal(0);
count.subscribe((n) => {
  document.querySelector('#count').textContent = String(n);
});

When observables earn their keep

  • Many event sources merged (clicks, keys, WebSocket, worker messages).
  • Time-based logic: debounce, throttle, timeouts, animation frames.
  • Cancellation chains where unsubscribing must abort in-flight work.
  • Simple store + render is enough for CRUD panels – do not add observables for fashion.

The next tutorial moves heavy work off the main thread with Web Workers.

PreviousState management without a framework