Observer Pattern
In this page:
Basic Observer
A basic observer setup has a subject that maintains a list of typed callback functions and calls each of them whenever some event or state change occurs, decoupling the thing that changes from the things reacting to it.
Example: Basic Observer
type Callback = (data: string) => void;
class Subject {
private observers: Callback[] = [];
subscribe(cb: Callback) { this.observers.push(cb); }
notify(data: string) { this.observers.forEach((cb) => cb(data)); }
}
const subject = new Subject();
subject.subscribe((data) => console.log("Received:", data));
subject.notify("event fired");
Subscribe and Unsubscribe
Subscribing adds a typed callback to the subject's internal list, while unsubscribing removes it — typically by returning a cleanup function from subscribe itself so callers don't need to track the callback reference separately.
Example: Subscribe and Unsubscribe
type Callback = () => void;
class Subject {
private observers: Callback[] = [];
subscribe(cb: Callback): () => void {
this.observers.push(cb);
return () => {
this.observers = this.observers.filter((o) => o !== cb);
};
}
}
const subject = new Subject();
const unsubscribe = subject.subscribe(() => console.log("notified"));
unsubscribe();
console.log("Unsubscribed");
Observers and State
Observers reacting to state changes typically receive the new state as a typed argument when notified, letting each observer update itself based on exactly what changed rather than re-fetching state independently.
Example: Observers and State
interface State { count: number; }
type Callback = (state: State) => void;
class Store {
private observers: Callback[] = [];
subscribe(cb: Callback) { this.observers.push(cb); }
setState(state: State) { this.observers.forEach((cb) => cb(state)); }
}
const store = new Store();
store.subscribe((state) => console.log("New count:", state.count));
store.setState({ count: 5 });
Typed Event Objects
Typing the event object passed to observers — rather than passing loose, individual arguments — makes it easy to add new fields to what observers receive later without breaking every existing observer's function signature.
Example: Typed Event Objects
interface ClickEvent {
x: number;
y: number;
}
type Callback = (event: ClickEvent) => void;
class Subject {
private observers: Callback[] = [];
subscribe(cb: Callback) { this.observers.push(cb); }
notify(event: ClickEvent) { this.observers.forEach((cb) => cb(event)); }
}
const subject = new Subject();
subject.subscribe((e) => console.log(e.x, e.y));
subject.notify({ x: 10, y: 20 });
When to Use Observer
Reach for the observer pattern when multiple independent parts of a program need to react to the same event source, like UI components all needing to update when shared data changes — for a single consumer, a plain callback is simpler.
Example: When to Use Observer
type Callback = (value: number) => void;
class DataSource {
private observers: Callback[] = [];
subscribe(cb: Callback) { this.observers.push(cb); }
update(value: number) { this.observers.forEach((cb) => cb(value)); }
}
const source = new DataSource();
source.subscribe((v) => console.log("Widget A:", v));
source.subscribe((v) => console.log("Widget B:", v));
source.update(42);
Chapter Quiz — Complete all 7 topics to unlock
0/7 topics done
Complete these topics first: