Back to the book of the four horsemen of Javapocalypse, they defined the Observer pattern as a way to define a one-to-many dependency between objects so that when one object changes state, all its dependents are notified and updated automatically.

It can seem strange, but it’s a pattern you probably already use if you play with JS listeners, because Listener is another name of this pattern. So this code :

const myListener = document.addListener(click, () => console.log(User clicked));

uses the Observer pattern.

As explained by the Java boys, you can create a component that can trigger some code asynchronously in another component. This pattern is one of the best in JS and a fundamental of Reactive Programming.

Note: other names of this pattern are Publisher, Subscriber or Listener.

The basic version

This first iteration of the pattern does the same job as the book’s initial version: trigger something at a specific moment.

We have two components: the subject (the thing which triggers something) and the listener (the triggered thing). For example, if you design a theme management service for your web app, you want the style of your application to be updated when the theme changes. Somewhere you will have your application renderer, which listens to changes of the theme management service.

In JS, we will use functions to make the pattern working.

Subject

import Styles from "./Styles";

class ThemeService {
  constructor() {
    this.theme = "default";
    this.styles = { primary: "blue" };

    this.listeners = [];
  }

  getStyles = () => this.styles;

  getTheme = () => this.theme;

  changeTheme = (newTheme) => {
    if (this.theme === newTheme) {
      return;
    }

    this.theme = newTheme;
    this.styles = Styles[newTheme];

    this._notifyListeners();
  };

  onChange = (listener) => {
    this.listeners.push(listener);
  };

  // That's a private function, so I wrote its name with an underscore
  _notifyListeners = () => {
    this.listeners.forEach((listener) => listener(this.theme, this.styles));
  };
}

export default new ThemeService();

Listener

import ThemeService from "./ThemeService";

function renderApplication(theme, styles) {
  const loadedStyles = styles || ThemeService.getStyles();

  // Do something with styles
}

// Add the listener
ThemeService.onChange(renderApplication);

// Default rendering
renderApplication();

Explanation

Note: I use a Singleton for the service as described in a previous article.

Each time you call the ThemeService.onChange function, you add a new listener to the ThemeService. At the moment you call ThemeService.changeTheme, the ThemeService will call all the listeners.

In the present situation, we will re-render our application when there’s a theme change. It’s a simple way to make a reactive application without any framework.

The “multiple events” version

Now, imagine we have to implement a module to change icons independently from the styles. If we use the previous implementation, we have to add a separate array to store the icons listener and add a function like onChange. That’s not very practical.

If you remember EventListener on a DOM object, you know you can add a specific event listener by specifying its name in the addEventListener(event, listener) function. That’s the proper way to implement a multi-event system.

Subject

import Icons from "./Icons";
import Styles from "./Styles";

class ThemeService {
  constructor() {
    this.theme = "default";
    this.iconsName = "default";
    this.styles = { primary: "blue" };
    this.icons = { default: "icon.png" };

    this.listeners = {};
  }

  getStyles = () => this.styles;

  getTheme = () => this.theme;

  getIconsName = () => this.iconsName;

  getIcons = () => this.icons;

  changeTheme = (newTheme) => {
    if (this.theme === newTheme) {
      return;
    }

    this.theme = newTheme;
    this.styles = Styles[newTheme];

    this._notifyListeners("theme", { styles: this.styles, theme: this.theme });
  };

  changeIcons = (newIconsName) => {
    if (this.iconsName === newIconsName) {
      return;
    }

    this.iconsName = newIconsName;
    this.icons = Icons[newIconsName];

    this._notifyListeners("icons", {
      icons: this.icons,
      iconsName: this.iconsName,
    });
  };

  onChange = (eventName, listener) => {
    if (!this.listeners[eventName]) {
      this.listeners[eventName] = [];
    }

    this.listeners[eventName].push(listener);
  };

  _notifyListeners = (eventName, event) => {
    if (!this.listeners[eventName]) {
      return;
    }

    this.listeners[eventName].forEach((listener) => listener(event));
  };
}

export default new ThemeService();

Listener

import ThemeService from "./ThemeService";

function updateIcons(event) {
  const icons = event.icons;

  // Do something with icons
}

function renderApplication(event) {
  const styles = (event && event.styles) || ThemeService.getStyles();

  // Do something with styles
}

// Add listeners
ThemeService.onChange("theme", renderApplication);
ThemeService.onChange("icons", updateIcons);

// Default rendering
renderApplication();

Explanation

It seems there’s a lot of changes. In reality, there are few significant changes:

  • this.listeners is now an object.
  • ThemeService.onChange and ThemeService._notifyListeners take an event name to know which event triggers the listener passed in parameter.
  • when we notify listeners with ThemeService._notifyListeners, we pass an event as a second parameter. This event will be sent to every listener listening to this event.
  • our listeners use the data from the event object.

And now we have an easily extensible ThemeService with the basics of reactive programming.


I hope you found this article helpful. If you want to talk about this, don’t hesitate to speak to us on the company’s Twitter account or LinkedIn account. :)

Thanks for reading! <3
Thanks for reading! <3

Mis à jour :