Front-end Engineering Lab
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:

  1. State Management - Redux, Zustand, Jotai use Observer Pattern internally
  2. Event systems - Notify multiple components about changes
  3. Themes and configurations - Multiple components observe theme changes
  4. Authentication - Multiple components need to react to login/logout
  5. 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

On this page