How to implement observer pattern in JavaScript

The observer pattern creates a subscription mechanism where multiple observers automatically receive notifications when a subject’s state changes. As the creator of CoreUI with 26 years of JavaScript development experience, I’ve implemented observer patterns in component libraries and state management systems that power reactive UIs for millions of users.

The most maintainable approach uses a Subject class that manages observers and notifies them of state changes.

Basic Observer Pattern

class Subject {
  constructor() {
    this.observers = []
  }

  subscribe(observer) {
    this.observers.push(observer)
  }

  unsubscribe(observer) {
    this.observers = this.observers.filter(obs => obs !== observer)
  }

  notify(data) {
    this.observers.forEach(observer => observer(data))
  }
}

// Usage
const subject = new Subject()

const observer1 = (data) => console.log('Observer 1:', data)
const observer2 = (data) => console.log('Observer 2:', data)

subject.subscribe(observer1)
subject.subscribe(observer2)

subject.notify('Hello World')
// Observer 1: Hello World
// Observer 2: Hello World

subject.unsubscribe(observer1)
subject.notify('New message')
// Observer 2: New message

Observer with State Management

class Store {
  constructor(initialState = {}) {
    this.state = initialState
    this.observers = []
  }

  getState() {
    return this.state
  }

  setState(newState) {
    this.state = { ...this.state, ...newState }
    this.notify(this.state)
  }

  subscribe(observer) {
    this.observers.push(observer)
    return () => this.unsubscribe(observer)
  }

  unsubscribe(observer) {
    this.observers = this.observers.filter(obs => obs !== observer)
  }

  notify(state) {
    this.observers.forEach(observer => observer(state))
  }
}

// Usage
const store = new Store({ count: 0 })

const unsubscribe = store.subscribe((state) => {
  console.log('State updated:', state)
})

store.setState({ count: 1 })
// State updated: { count: 1 }

store.setState({ count: 2 })
// State updated: { count: 2 }

unsubscribe()
store.setState({ count: 3 })
// No output (unsubscribed)

Event-Specific Observers

class EventEmitter {
  constructor() {
    this.events = {}
  }

  on(event, callback) {
    if (!this.events[event]) {
      this.events[event] = []
    }
    this.events[event].push(callback)
  }

  off(event, callback) {
    if (!this.events[event]) return

    this.events[event] = this.events[event].filter(cb => cb !== callback)
  }

  emit(event, data) {
    if (!this.events[event]) return

    this.events[event].forEach(callback => callback(data))
  }

  once(event, callback) {
    const wrapper = (data) => {
      callback(data)
      this.off(event, wrapper)
    }
    this.on(event, wrapper)
  }
}

// Usage
const emitter = new EventEmitter()

emitter.on('user:login', (user) => {
  console.log('User logged in:', user.name)
})

emitter.on('user:logout', () => {
  console.log('User logged out')
})

emitter.emit('user:login', { name: 'John' })
// User logged in: John

emitter.emit('user:logout')
// User logged out

emitter.once('user:register', (user) => {
  console.log('New user:', user.name)
})

emitter.emit('user:register', { name: 'Jane' })
// New user: Jane

emitter.emit('user:register', { name: 'Bob' })
// No output (once only)

Observable Value

class Observable {
  constructor(value) {
    this.value = value
    this.observers = []
  }

  get() {
    return this.value
  }

  set(newValue) {
    if (this.value !== newValue) {
      const oldValue = this.value
      this.value = newValue
      this.notify(newValue, oldValue)
    }
  }

  subscribe(observer) {
    this.observers.push(observer)
    observer(this.value) // Immediately call with current value
    return () => this.unsubscribe(observer)
  }

  unsubscribe(observer) {
    this.observers = this.observers.filter(obs => obs !== observer)
  }

  notify(newValue, oldValue) {
    this.observers.forEach(observer => observer(newValue, oldValue))
  }
}

// Usage
const count = new Observable(0)

count.subscribe((value, oldValue) => {
  console.log(`Count changed from ${oldValue} to ${value}`)
})
// Count changed from undefined to 0

count.set(1)
// Count changed from 0 to 1

count.set(2)
// Count changed from 1 to 2

count.set(2)
// No output (value didn't change)

Computed Observables

class ComputedObservable extends Observable {
  constructor(dependencies, computeFn) {
    super(null)
    this.dependencies = dependencies
    this.computeFn = computeFn

    // Subscribe to all dependencies
    this.dependencies.forEach(dep => {
      dep.subscribe(() => this.recompute())
    })

    this.recompute()
  }

  recompute() {
    const values = this.dependencies.map(dep => dep.get())
    const newValue = this.computeFn(...values)
    this.set(newValue)
  }
}

// Usage
const price = new Observable(100)
const quantity = new Observable(2)

const total = new ComputedObservable(
  [price, quantity],
  (p, q) => p * q
)

total.subscribe((value) => {
  console.log('Total:', value)
})
// Total: 200

price.set(150)
// Total: 300

quantity.set(3)
// Total: 450

React Hook with Observer Pattern

import { useEffect, useState } from 'react'

function useObservable(observable) {
  const [value, setValue] = useState(observable.get())

  useEffect(() => {
    const unsubscribe = observable.subscribe((newValue) => {
      setValue(newValue)
    })

    return unsubscribe
  }, [observable])

  return value
}

// Usage in component
function Counter() {
  const count = useObservable(countObservable)

  return (
    <div>
      <p>Count: {count}</p>
      <button onClick={() => countObservable.set(count + 1)}>
        Increment
      </button>
    </div>
  )
}

Best Practice Note

This is the same observer pattern we use in CoreUI’s reactive components and state management. The observer pattern enables loose coupling between components by allowing subjects to notify observers without knowing their implementation details. Always return unsubscribe functions to prevent memory leaks, and consider using WeakMap for observers to enable automatic garbage collection.

For related patterns, check out how to implement pub/sub pattern in JavaScript and how to use WeakMap for private data in JavaScript.


Speed up your responsive apps and websites with fully-featured, ready-to-use open-source admin panel templates—free to use and built for efficiency.


About the Author