DEV Community

Roel
Roel

Posted on • Edited on

A practical example of the Observer Pattern in TypeScript

Are you struggling with your business logic and keeping classes seperated? Do you need a way to split some logic into seperate classes and keep them SOLID? This short guide might really help you out!

In TypeScript, managing global state efficiently across components or services is a common challenge. In this guide, we'll explore a simple yet effective way to create a global state service using the observer pattern. This approach is especially useful when you want to keep your codebase lightweight and avoid unnecessary dependencies.

So for example, instead of your sessiontimer.ts depending on a sessionwarning.ts and a session-logout.ts we can have all these files depend on only one service called a globalstate.ts.

The Global Observer Service

Let's start by creating a GlobalStateService class that follows the observer pattern:

class GlobalStateService {
  private static instance: GlobalStateService;

  private eventListeners: Map<string, ((data?: any) => void)[]> = new Map();

   //Ensure a singleton
  static getInstance(): GlobalStateService {
    if (!GlobalStateService.instance) {
      GlobalStateService.instance = new GlobalStateService();
    }
    return GlobalStateService.instance;
  }

  private constructor() {  }

  on(eventName: string, listener: (data?: any) => void): void {
    if (!this.eventListeners.has(eventName)) {
      this.eventListeners.set(eventName, []);
    }
    this.eventListeners.get(eventName)?.push(listener);
  }

  off(eventName: string, listener: (data?: any) => void): void {
    const listeners = this.eventListeners.get(eventName);
    if (listeners) {
      const index = listeners.indexOf(listener);
      if (index !== -1) {
        listeners.splice(index, 1);
      }
    }
  }

  emit(eventName: string, data?: any): void {
    const listeners = this.eventListeners.get(eventName);
    if (listeners) {
      listeners.forEach((listener) => listener(data));
    }
  }
}


Enter fullscreen mode Exit fullscreen mode

This class has 3 functions, on, off and emit. We implement this class into other classes. For example a SessionPopup will use a GlobalStateService and the 'on' function. When we use this 'on' function we can read the data submitted by that 'on' function. We can also 'emit' an event with data to every class which also uses the GlobalStateService.

Usage in Components

Now, let's see how to use this GlobalStateService in a component. In this example we trigger a popup.

export class SessionPopup {
  // Example usage:
  const globalStateService = GlobalStateService.getInstance();

  private constructor(){
    this.globalStateService.on('sessionWarning', (remainingSeconds) => {
      console.log(`Session warning! open popup`);
      this.open();
      // Trigger your session warning logic here
    });
  }

  private open(): void{
     //open the popup
  }

  // Don't forget to unsubscribe when components are destroyed
  sessionWarningSubscription();
  sessionExpiredSubscription();
}

Enter fullscreen mode Exit fullscreen mode

When the SessionPopup is created it subscribes to all events called 'sessionWarning'. Now we can open the popup when another component triggers this event.

All Done!

With this change your other components can subscribe and wait until a trigger needs to be executed.

Conclusion

This lightweight global state service provides a straightforward mechanism for managing events and state across your TypeScript application using the subscriber pattern. It allows you to avoid unnecessary dependencies, keeping your codebase clean and easy to maintain. Feel free to customize and extend the service according to your specific requirements.

Top comments (0)