Singleton Pattern in frontend

Singleton Pattern in frontend

How design patterns can be used in frontend part 1

in this blog post, I will try to explain the pattern in a simplified way the Singleton Design Pattern, its main purpose is to reconstruct the instantiation of a class to a singular instance, I need to clarify that the use case is very difficult to find but if you think about it in a deeper way I found some interesting uses.

The more known use case is to use a class to define the configuration of an application, to make use of this pattern in this use case we need to define a Class with the definitions and a configuration that can instantiate the class only if this instance does not exist like this

class AppConfig {
  constructor() {
    this.apiEndpoint = 'https://api.example.com';
    // Other configuration options
  }
}

const AppConfiguration = (() => {
  let instance;

  function createInstance() {
    return new AppConfig();
  }

  return {
    getInstance: () => {
      if (!instance) {
        instance = createInstance();
      }
      return instance;
    },
  };
})();

const config = AppConfiguration.getInstance();

This use case can be some const defined in a simplified way centralizing them or defining them in the env vars of the deployment process that is because is typically known as an antipattern and not a real pattern.

There are 2 ways very handful to use it, one is to use this pattern to govern the state of an application in a simplified way like a JS vanilla application there is an example of this use case

class GlobalState {
  constructor() {
    this.data = {};
  }

  setData(key, value) {
    this.data[key] = value;
  }

  getData(key) {
    return this.data[key];
  }
}

const AppState = (() => {
  let instance;

  function createInstance() {
    return new GlobalState();
  }

  return {
    getInstance: () => {
      if (!instance) {
        instance = createInstance();
      }
      return instance;
    },
  };
})();

// Usage
const appState = AppState.getInstance();
// Set Data
appState.setData('user', { id: 1, name: 'John Doe' });
// Get Data
console.log(appState.getData('user'))

with this use case, we can resolve a simple state management centralized

The other common use case is to use as a Bus or a PubSub system to communicate 2 elements of different scopes

class EventBus {
  constructor() {
    this.listeners = {};
  }

  subscribe(event, callback) {
    if (!this.listeners[event]) {
      this.listeners[event] = [];
    }
    this.listeners[event].push(callback);
  }

  publish(event, data) {
    if (this.listeners[event]) {
      this.listeners[event].forEach(callback => callback(data));
    }
  }
}

const EventManager = (() => {
  let instance;

  function createInstance() {
    return new EventBus();
  }

  return {
    getInstance: () => {
      if (!instance) {
        instance = createInstance();
      }
      return instance;
    },
  };
})();

// Usage
const eventBus = EventManager.getInstance();
eventBus.subscribe('userLoggedIn', (user) => {
  console.log(`${user.name} logged in`);
});

There are some simple examples of this implementation of the more complex to-understand design pattern the Singleton Pattern.