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 —
useStateis 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
- Open your capstone and name every pattern already in it without adding any.
- Find three
addEventListenercalls on any web page's source. Each is an observer. - Write
withLoggingand wrap one of your own functions with it. - Stack two decorators and confirm the order changes the behaviour.
- Write
debounceand attach it to an input. Confirm one call per pause. - Find a class in your code with one method and no state. Make it a function.
- Find a factory that only calls a constructor. Delete it and see whether anything is worse.
- Read the source of a small npm package and identify two patterns in it.
- Look up
IntersectionObserveron MDN and explain what it replaced. - 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