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.
Related Articles
For related patterns, check out how to implement pub/sub pattern in JavaScript and how to use WeakMap for private data in JavaScript.



