RizTech Academy logo
RizTech Academy
Design Patterns in JavaScriptLesson 6 of 625 min

Patterns in the browser, and when not to use one

The point of learning patterns is reading code, not writing it. You will spend far more of your career in somebody else's codebase, and the value of the vocabulary is that an unfamiliar file becomes recognisable in thirty seconds.

A field guide

You see Probably So
a function returning an object factory check what it sets by default
an IIFE assigned to a const module pattern pre-2015 code, or a bundle
an object whose values are all functions strategy lookup what varies is the key
a function returning a function partial application the outer arguments are configuration
subscribe / on / addEventListener observer find the unsubscribe
a function wrapping another and returning the same shape decorator what does it add?
Object.freeze on the way out defensive copy somebody was bitten
new Proxy(...) decorator, at the property level usually a framework

The browser is the catalogue

Every one of these is in the platform, load-bearing:

// factory — none of these say which class you get
document.createElement("li")
new URL("/products", location.origin)
Promise.resolve(value)

// strategy — the algorithm as an argument
[...orders].sort((a, b) => a.pieces - b.pieces)
orders.filter(isPending)
JSON.stringify(order, (k, v) => (k === "secret" ? undefined : v))

// observer
element.addEventListener("click", onClick)
new MutationObserver(callback).observe(node, { childList: true })
new IntersectionObserver(onVisible).observe(image)

// decorator — same shape in, same shape out
fetch = withRetry(fetch)
const memoised = memoize(expensiveThing)

// module — evaluated once, shared everywhere
import { rates } from "./rates.js"

MutationObserver and IntersectionObserver have the pattern in the name. IntersectionObserver is the one to remember — it is how lazy-loaded images work, and it replaced a decade of scroll-handler code.

Decorators, which this module has not covered directly

Because in JavaScript they are usually just a function returning a function:

function withLogging(fn) {
  return (...args) => {
    console.log(`calling ${fn.name}`, args);
    const result = fn(...args);
    console.log(`${fn.name} returned`, result);
    return result;
  };
}

const price = withLogging(priceFor);

Same signature in, same signature out, something added in between — and they stack:

const price = withLogging(withCache(priceFor));

That is the decorator pattern with no classes and no syntax. You will meet it as middleware in Express, as higher-order components in older React, and as withRetry or debounce in every utility file ever written.

debounce is worth recognising as one:

function debounce(fn, ms) {
  let timer;
  return (...args) => {
    clearTimeout(timer);
    timer = setTimeout(() => fn(...args), ms);
  };
}

input.addEventListener("input", debounce(search, 300));

Takes a function, returns a function with the same calling shape, adds behaviour. A decorator and a closure at once.

In frameworks

You will meet a framework next, and it is these ideas with the wiring done for you:

  • React hooks — useState is a factory returning a value and a setter closed over the component's state.
  • React context — observer. Providers publish, consumers subscribe.
  • Redux / stores — your capstone's state.js, formalised: one module, private state, subscribe, notify.
  • Express middleware — decorators on a request handler, stacked.
  • Vue reactivity, MobX — Proxy-based decorators intercepting property access.

None of it is new. Recognising subscribe in a store you have never seen is worth more than having memorised the twenty-three names.

Recognising misuse

Patterns applied where they were not needed have a look.

A class with one method and no state. That is a function.

class PriceCalculator {
  calculate(plan, pieces) { … }
}

A factory that only calls a constructor.

function createOrder(customer, item) {
  return new Order(customer, item);
}

No defaults, no derived fields, no name improvement. Delete it.

An interface-shaped object with one implementation, forever. Ask when the second is expected; if the answer is vague, it is indirection.

Decorators stacked six deep, where no file tells you what the function does and you follow six wrappers to find out.

A utils.js, helpers.js or managers.js with thirty unrelated exports. Not a module — a drawer for things nobody wanted to name.

Object.freeze on everything, including objects that never escape the function. Defensive habits applied without asking what they defend against.

None of this means the author was foolish. It usually means the future they anticipated did not arrive. Simplify when you are in there for another reason, not as a project.

The rule worth keeping

Refactor to patterns, not with them.

Write the straightforward thing. When it becomes awkward — the third else if, the second caller wanting a variation, the function that now needs four collaborators — the shape of the fix will usually be a pattern, and you will recognise it.

Arriving that way means the pattern solves a problem you actually have, and you can justify it in review. Which is the practical test: if you cannot say what varies, you do not need the pattern.

And in JavaScript specifically, ask one more question first: would a function do? Most of the time, it would.

Check your work

Why patterns matter most for reading: you spend more time in code you did not write.

What an IIFE assigned to a const tells you: pre-2015 code, or bundler output.

What a function returning a function usually is: partial application, or a decorator.

Why debounce is a decorator: same calling shape in and out, with behaviour added.

What frameworks are, in pattern terms: stores are the module pattern plus observer; middleware is stacked decorators; context is observer.

The signs of misuse: a class with one method, a factory that only calls new, an interface with one implementation, six-deep decorators, a utils.js of thirty exports.

What misuse usually means: the anticipated future did not arrive.

The rule: refactor to patterns, not with them.

The extra JavaScript question: would a function do?

Practice

  1. Open your capstone and name every pattern already in it without adding any.
  2. Find three addEventListener calls on any web page's source. Each is an observer.
  3. Write withLogging and wrap one of your own functions with it.
  4. Stack two decorators and confirm the order changes the behaviour.
  5. Write debounce and attach it to an input. Confirm one call per pause.
  6. Find a class in your code with one method and no state. Make it a function.
  7. Find a factory that only calls a constructor. Delete it and see whether anything is worse.
  8. Read the source of a small npm package and identify two patterns in it.
  9. Look up IntersectionObserver on MDN and explain what it replaced.
  10. Explain to somebody why "we might need it later" is not a reason — and when it is.

Next: writing code other people can read, which matters more than any of this.

Stuck on this lesson?

Being stuck is part of it — but being stuck alone for three days is not. Our internship programme pairs this curriculum with code review and one-to-one help from working developers, and it is free.

About the internship