Observer Pattern
The observer pattern defines a one-to-many relationship: when one object (subject) changes state, all its dependents (observers) are notified automatically.
Core implementation
ts
class EventEmitter<T extends Record<string, unknown>> {
private listeners: Partial<{ [K in keyof T]: ((data: T[K]) => void)[] }> = {};
on<K extends keyof T>(event: K, listener: (data: T[K]) => void) {
if (!this.listeners[event]) this.listeners[event] = [];
this.listeners[event]!.push(listener);
return () => this.off(event, listener); // return unsubscribe
}
off<K extends keyof T>(event: K, listener: (data: T[K]) => void) {
this.listeners[event] = this.listeners[event]?.filter(l => l !== listener);
}
emit<K extends keyof T>(event: K, data: T[K]) {
this.listeners[event]?.forEach(l => l(data));
}
}
// usage
type AppEvents = { 'user:login': { userId: string }; 'cart:updated': { count: number }; };
const emitter = new EventEmitter<AppEvents>();
const unsubscribe = emitter.on('user:login', ({ userId }) => {
console.log('User logged in:', userId);
});
emitter.emit('user:login', { userId: 'u1' });
unsubscribe(); // cleanupWhere you see it in frontend
- DOM events -
addEventListener/removeEventListener. - RxJS -
Observable.subscribe(). - Zustand -
store.subscribe(). - React Query - cache subscriptions.
- WebSocket -
socket.on('message', handler).
In React - custom event bus
tsx
// cross-component communication without prop drilling or shared state
const bus = new EventEmitter();
// component A - emit
function ButtonA() {
return <button onClick={() => bus.emit('refresh', {})}>Refresh</button>;
}
// component B - listen
function TableB() {
useEffect(() => {
return bus.on('refresh', () => refetch());
}, []);
}Observer is everywhere in frontend - the DOM event system itself is observer. The main risk is memory leaks from unremoved listeners - always clean up in
useEffectreturn.
updated
did you know it?