PatternsArchitecture Patterns
Observer Pattern
Foundation of reactivity - state changes and UI observes and updates. Foundation of Redux and Context API
The Observer Pattern defines a one-to-many dependency between objects, where when one object changes state, all its dependents are notified and updated automatically. It's the foundation of reactivity in React - state changes and the UI "observes" and updates.
🎯 The Concept
// Observer Pattern is the foundation of React!
function Counter() {
const [count, setCount] = useState(0); // Subject
// Component "observes" the state
// When count changes, component re-renders
return (
<div>
<p>{count}</p> {/* Observer */}
<button onClick={() => setCount(c => c + 1)}>
Increment
</button>
</div>
);
}📚 Common Front-End Examples
1. Sistema de Eventos Customizado
type EventCallback = (...args: unknown[]) => void;
class EventEmitter {
private events = new Map<string, EventCallback[]>();
on(event: string, callback: EventCallback) {
if (!this.events.has(event)) {
this.events.set(event, []);
}
this.events.get(event)!.push(callback);
}
off(event: string, callback: EventCallback) {
const callbacks = this.events.get(event);
if (callbacks) {
const index = callbacks.indexOf(callback);
if (index > -1) {
callbacks.splice(index, 1);
}
}
}
emit(event: string, ...args: unknown[]) {
const callbacks = this.events.get(event);
if (callbacks) {
callbacks.forEach(callback => callback(...args));
}
}
}
// Usage
const emitter = new EventEmitter();
// Observers
emitter.on('user:login', (user) => {
console.log('User logged in:', user.name);
});
emitter.on('user:login', (user) => {
analytics.track('login', { userId: user.id });
});
// Subject notifies observers
emitter.emit('user:login', { id: 1, name: 'John' });2. State Store Simples (Redux-like)
type Listener = () => void;
interface Action {
type: string;
payload?: unknown;
}
type Reducer<T> = (state: T, action: Action) => T;
class Store<T> {
private state: T;
private listeners: Listener[] = [];
private reducer: Reducer<T>;
constructor(initialState: T, reducer: Reducer<T>) {
this.state = initialState;
this.reducer = reducer;
}
getState(): T {
return this.state;
}
dispatch(action: Action) {
this.state = this.reducer(this.state, action);
// Notify all observers
this.listeners.forEach(listener => listener());
}
subscribe(listener: Listener) {
this.listeners.push(listener);
// Return unsubscribe function
return () => {
const index = this.listeners.indexOf(listener);
if (index > -1) {
this.listeners.splice(index, 1);
}
};
}
}
// Usage
const counterReducer = (state: number, action: Action) => {
switch (action.type) {
case 'INCREMENT':
return state + 1;
case 'DECREMENT':
return state - 1;
default:
return state;
}
};
const store = new Store(0, counterReducer);
// Observers
const unsubscribe1 = store.subscribe(() => {
console.log('State changed:', store.getState());
});
const unsubscribe2 = store.subscribe(() => {
console.log('UI updated with state:', store.getState());
});
// Subject changes state
store.dispatch({ type: 'INCREMENT' });
store.dispatch({ type: 'DECREMENT' });
// Cleanup
unsubscribe1();
unsubscribe2();3. React Hook with Observer Pattern
import { useState, useEffect } from 'react';
function useStore<T>(store: Store<T>) {
const [state, setState] = useState(store.getState());
useEffect(() => {
// Observer subscribes
const unsubscribe = store.subscribe(() => {
setState(store.getState());
});
return unsubscribe; // Cleanup
}, [store]);
return [state, store.dispatch.bind(store)] as const;
}
// Usage
const counterReducer = (state: number, action: Action) => {
switch (action.type) {
case 'INCREMENT':
return state + 1;
case 'DECREMENT':
return state - 1;
default:
return state;
}
};
const store = new Store(0, counterReducer);
function Counter() {
const [count, dispatch] = useStore(store);
return (
<div>
<p>{count}</p>
<button onClick={() => dispatch({ type: 'INCREMENT' })}>
+
</button>
<button onClick={() => dispatch({ type: 'DECREMENT' })}>
-
</button>
</div>
);
}4. Observer for Theme Changes
type Theme = 'light' | 'dark';
type ThemeListener = (theme: Theme) => void;
class ThemeManager {
private theme: Theme = 'light';
private listeners: ThemeListener[] = [];
getTheme(): Theme {
return this.theme;
}
setTheme(theme: Theme) {
this.theme = theme;
document.documentElement.setAttribute('data-theme', theme);
// Notify observers
this.listeners.forEach(listener => listener(theme));
}
subscribe(listener: ThemeListener) {
this.listeners.push(listener);
return () => {
const index = this.listeners.indexOf(listener);
if (index > -1) {
this.listeners.splice(index, 1);
}
};
}
}
const themeManager = new ThemeManager();
// Observers
themeManager.subscribe((theme) => {
console.log('Theme changed to:', theme);
});
themeManager.subscribe((theme) => {
localStorage.setItem('theme', theme);
});
// React Hook
function useTheme() {
const [theme, setTheme] = useState(themeManager.getTheme());
useEffect(() => {
const unsubscribe = themeManager.subscribe(setTheme);
return unsubscribe;
}, []);
return [theme, themeManager.setTheme.bind(themeManager)] as const;
}5. Observer para Mudanças de Autenticação
type User = { id: string; name: string } | null;
type AuthListener = (user: User) => void;
class AuthManager {
private user: User = null;
private listeners: AuthListener[] = [];
getUser(): User {
return this.user;
}
login(user: User) {
this.user = user;
this.notify();
}
logout() {
this.user = null;
this.notify();
}
private notify() {
this.listeners.forEach(listener => listener(this.user));
}
subscribe(listener: AuthListener) {
this.listeners.push(listener);
// Notify immediately with current state
listener(this.user);
return () => {
const index = this.listeners.indexOf(listener);
if (index > -1) {
this.listeners.splice(index, 1);
}
};
}
}
const authManager = new AuthManager();
// Observers
authManager.subscribe((user) => {
if (user) {
analytics.identify(user.id);
} else {
analytics.reset();
}
});
authManager.subscribe((user) => {
if (user) {
// Update header, sidebar, etc.
updateUI(user);
}
});
// React Hook
function useAuth() {
const [user, setUser] = useState(authManager.getUser());
useEffect(() => {
const unsubscribe = authManager.subscribe(setUser);
return unsubscribe;
}, []);
return {
user,
login: authManager.login.bind(authManager),
logout: authManager.logout.bind(authManager)
};
}🎯 When to Use
This pattern is commonly used and recommended for:
- State Management - Redux, Zustand, Jotai use Observer Pattern internally
- Event systems - Notify multiple components about changes
- Themes and configurations - Multiple components observe theme changes
- Authentication - Multiple components need to react to login/logout
- Real-time updates - WebSocket, Server-Sent Events notify multiple observers
🔗 Relationship with React
React uses Observer Pattern internally:
// useState is a Subject
const [count, setCount] = useState(0);
// Component is an Observer
function Counter() {
const [count, setCount] = useState(0);
// When count changes, React notifies and re-renders
return <div>{count}</div>;
}📚 Key Takeaways
- Foundation of reactivity - React, Redux, Vue use Observer Pattern
- Decoupling - Subject doesn't know specific observers
- Multiple observers - One event can notify multiple listeners
- Push vs Pull - Subject "pushes" changes to observers
- Memory leaks - Always unsubscribe in useEffect cleanup