Skip to content

Observer pattern in React

Published: at 03:00 PM

Observer pattern

The observer pattern is widely used in software development. Recently, I was building an app where I needed to use it and wanted to fully understand how it works. Here is my understanding of it.

We have a publisher and subscribers that listen to what the publisher publishes.

This service allows subscribers to subscribe to the publisher by using subscribe and passing a function (for example, a state setter). Invoking that function triggers a re-render. There is also a notify function that executes each callback and updates each subscriber using the callback they previously passed.

type ObserverCallback = (message: string) => void;

export class ObserverService {
  private callbacks: ObserverCallback[] = [];

  subscribe(callback: ObserverCallback) {
    this.callbacks.push(callback);

    // Important: we are not executing the function here; we're returning it for later use
    return () => {
      // Filter out the callback that was passed to subscribe; this works because callbacks are compared by reference
      this.callbacks = this.callbacks.filter(cb => cb !== callback);
    };
  }

  notify(message: string) {
    // Invoke all callbacks to trigger a re-render in subscribed components
    this.callbacks.forEach(callback => callback(message));
  }
}

Here is the implementation of the hook

"use client";

import { ObserverService } from "@/services/observer-service";
import { useState, useEffect } from "react";

const observerService = new ObserverService();

export function useObserver() {
  const [message, setMessage] = useState<string | null>(null);

  useEffect(() => {
    const callback = (message: string) => {
      // This triggers a re-render when called by the notify function inside the observer service
      setMessage(message);
    };

    // subscribe returns an unsubscribe function that allows us to unsubscribe from the publisher
    const unsubscribe = observerService.subscribe(callback);

    return () => {
      unsubscribe();
    };
  }, [observerService]);

  function onClick(message: string) {
    observerService.notify(message);
  }

  return { message, onClick };
}

The following two components both use the same useObserver hook. Because the hook holds a module‑scoped ObserverService instance, each component subscribes to the same publisher. Clicking “Notify” in either component broadcasts a message that both components receive, causing both to re-render with the latest message.

// Observer 1
"use client";

import { useObserver } from "@/hooks/use-observer";
import React from "react";

export default function Observer1() {
  const { message, onClick } = useObserver();
  return (
    <div>
      observer1: {message}{" "}
      <button onClick={() => onClick("Hello from observer1")}>Notify</button>
    </div>
  );
}

Expected behavior of the examples:

// Observer 2
"use client";

import { useObserver } from "@/hooks/use-observer";
import React from "react";

export default function Observer2() {
  const { message, onClick } = useObserver();
  return (
    <div>
      observer2: {message}{" "}
      <button onClick={() => onClick("Hello from observer2")}>Notify</button>
    </div>
  );
}

Kacper Siniło
Fullstack Developer

Back to Top