A distilled guide based on “Learning Patterns” by Lydia Hallie & Addy Osmani
The Observer Pattern establishes a one‑to‑many relationship between an object that owns some state (the observable) and the dependents that need to react when that state changes (the observers). Whenever an event occurs, the observable notifies every subscribed observer.
1. Anatomy of the Pattern
An observable usually exposes three core capabilities:
Part | Responsibility |
---|---|
observers |
Holds a list of callbacks (observers) |
subscribe(fn) |
Registers an observer |
unsubscribe(fn) |
Removes an observer |
notify(data) |
Invokes every observer with the new data |
Minimal ES 6 Implementation
class Observable {
constructor() {
this.observers = [];
}
subscribe(fn) {
this.observers.push(fn);
}
unsubscribe(fn) {
this.observers = this.observers.filter(obs => obs !== fn);
}
notify(data) {
this.observers.forEach(obs => obs(data));
}
}
This is exactly the skeleton Lydia & Addy showcase before applying it to a real app.
2. From Theory to Practice — A Tiny App
Below is a condensed version of their example: a button and a switch that both report user activity. Whenever a user clicks or toggles, we log to the console and fire a toast notification—two observers reacting to one observable.
import { ToastContainer, toast } from "react-toastify";
import { Button, Switch, FormControlLabel } from "@material-ui/core";
const analytics$ = new Observable(); // ① create observable
function logger(data) { // ② observer 1
console.log(Date.now(), data);
}
function toastify(data) { // ② observer 2
toast(data);
}
analytics$.subscribe(logger); // ③ subscribe
analytics$.subscribe(toastify);
export default function App() {
const handleClick = () => analytics$.notify("Clicked the button!");
const handleToggle = () => analytics$.notify("Toggled the switch!");
return (
<div className="App">
<Button onClick={handleClick}>Click me!</Button>
<FormControlLabel control={<Switch onChange={handleToggle} />} />
<ToastContainer />
</div>
);
}
Result: one source of truth (analytics$
) with multiple independent reactions. This separation keeps concerns tidy and testable.
3. Five Real‑Life Software Use‑Cases
# | Use‑Case | Where the Pattern Shows Up | Benefit |
---|---|---|---|
1 | DOM events | Every addEventListener call makes your callback an observer of a DOM element. |
Decouples UI elements from event logic. |
2 | Node.js EventEmitter |
Streams, HTTP servers, and many core modules emit events you can subscribe to. | Enables modular, event‑driven servers. |
3 | Redux .subscribe() |
Components observe the store; they rerender only when relevant state changes. | Keeps UI synced with global state without tight coupling. |
4 | RxJS Observables | Reactive extensions combine Observer and Iterator patterns to manage async data. | Composable pipelines for events, promises, WebSockets… |
5 | IntersectionObserver API | Lazy‑load images by observing when elements enter the viewport. | Boosts performance with minimal code. |
6 | WebSocket chat apps | Clients subscribe to a channel; server notifies on new messages. | Real‑time updates without polling. |
Tip: Anytime you have “Something happened, tell everyone who cares,” the Observer Pattern is a candidate.
4. Pros & Cons
✅ Pros | ❌ Cons |
---|---|
Enforces separation of concerns—observables don’t know what observers do with the data. | Lots of observers can mean performance overhead if notifications are frequent or expensive. |
Promotes loose coupling; observers can attach/detach at runtime. | Debugging can be trickier: side‑effects live in many places. |
5. Key Takeaways
- Keep the observable small & focused—just manage subscriptions and dispatch events.
- Extract complex reaction logic into observers for clear responsibility boundaries.
- Combine with other patterns (e.g., Provider or Middleware) for scalable architectures.
Content adapted under CC BY‑NC 4.0 from patterns.dev.
Top comments (0)