DEV Community

Cover image for Singleton design pattern. JS/TS Examples.
Vlad R
Vlad R

Posted on • Updated on

Singleton design pattern. JS/TS Examples.

So why do we need to know design patterns?

First of all it helps you to save your time. Programming is not a new thing, a lot of problems have been already solved before. Lots of patterns and approaches have been invented and most of them are time tested. If you don't want to reinvent the wheel, you might be interested to know more about those preexisting patterns and approaches.
So design patterns are typical solutions to commonly occurring problems in programming.

In this short article we are going to cover the singleton design pattern. This pattern is a type of creational design patterns and probably one of the simplest ones.
 

Implementation

The Singleton pattern is just a way of creating a single object that is shared amongst a bunch of different resources throughout your application without having to recreate that object or losing the information inside of it.

1. It ensures that there is only one instance of a given class
For example we may create a logger class which prints logs and keeps them inside the class. Following this patters you have to have a single instance of the logger which prevents losing/overwriting the logs list.

2. This pattern also provides a way to access the single instance globally
Going back to our logger class, it's pretty much obvious that we need it to be accessible from any file in our project. Because errors may appear anywhere and we want to log them.

 

Pros and Cons

Pros (Global variables vs Singleton):

  • Comparing to global variables, Singletons can not be modified(speaking of var in JS).
  • Unlike global variables it doesn't exist until instantiated.
  • Another advantage is that you are absolutely sure of the number of instances.
  • You can manage the state of that instance.

Cons of using this design pattern:

  • Someday when you have a lot of parts of your app rely on that Singleton obj it may became hard to change Singleton obj itself.
  • As we already know Singleton's methods could be called from different parts of your app simultaneously at the same time which may cause a data/variables within this object to be overwritten/read incorrectly.

FYI: there are more cons actually, but we're not going to cover all of them in this article.

 

Examples

JS Example:

class SingletonLogger {
  // prevent modifying the instance property,
  // for example set it to null to create a second instance.
  static #instance;
  // prevent modifying/overwriting logs array.
  #logs = [];

  // classes in JavaScript...
  //    may not have a private field named '#constructor'
  // so it's not possible to disable...
  //    calling constructor() {} in JS.
  // if you try new SingletonLogger()...
  //    with private constructor in TS it will throw an error
  constructor() {
    if (SingletonLogger.#instance) {
      throw new Error('Can not instantiate second singleton');
    }
    SingletonLogger.#instance = this;
  }

  // getInstance should be static...
  //    to be able to call SingletonLogger.getInstance()
  static getInstance() {
    if (!SingletonLogger.#instance) {
      SingletonLogger.#instance = new SingletonLogger();
    }
    return SingletonLogger.#instance;
  }

  log(error) {
    this.#logs.push(error);
    console.log(error);
  }

  // since #logs array is private, we need to create a getter
  get logsArray() {
    return this.#logs;
  }
}

// Usage:
const logger = SingletonLogger.getInstance();
try {
  throw new Error('first err');
} catch(err) {
  logger.log(err); // log: first err
}
console.log(logger.logsArray); // log: [first err]

const logger2 = SingletonLogger.getInstance();
try {
  throw new Error('second err');
} catch(err) {
  logger2.log(err); // log: second err
}
console.log(logger2.logsArray); // log: [first err, second err]

// const logger3 = new SingletonLogger();
// Error: Can not instantiate second singleton

Enter fullscreen mode Exit fullscreen mode

TS example:

class SingletonLogger {
    private static instance: SingletonLogger;
    private logs: Array<Error> = [];

    private constructor() { }

    public static getInstance(): SingletonLogger {
        if (!SingletonLogger.instance) {
            SingletonLogger.instance = new SingletonLogger();
        }
        return SingletonLogger.instance;
    }

    log(error: Error) {
        this.logs.push(error);
        console.log(error);
    }

    get logsArray() {
        return this.logs;
    }
}

// Usage:
// const singleton = new SingletonLogger(); // ...
//    TS won't let you do this
// Constructor of class 'SingletonLogger' is private...
//    and only accessible within the class declaration.

const singleton = SingletonLogger.getInstance();
try {
    throw new Error('first err');
} catch(err) {
    singleton.log(err as Error); // log: first err 
}
console.log(singleton.logsArray); // log: [first err] 

const sameSingleton = SingletonLogger.getInstance();
try {
    throw new Error('second err');
} catch(err) {
    sameSingleton.log(err as Error); // log: second err 
}
console.log(sameSingleton.logsArray); // log: [first err, second err] 
Enter fullscreen mode Exit fullscreen mode

 

Conclusion

To be honest I haven't found any use cases in Front End development where singleton design pattern would be really helpful. Of course you can create the same logger as we did above or use the cache as a singleton object. But I personally think you will rarely have to use it.
Anyways, this design pattern is a part of Gang of Four design patterns and knowing all these patterns will take you to the next level as a dev.

Thanks for reading! Any feedback is appreciated!😊

Photo by Lucas Campoi

Discussion (2)

Collapse
subztep profile image
Andras Serfozo

I haven't found any use cases in Front End development where singleton design pattern would be really helpful

I implemented singleton recently in TS for a special use case, and tbh I still have no idea how could I resolve it without - probably pointless - significant code refactoring.

So, it happens during development only. The stack is Vite and Vue with hot module replacement, also Three.js as a big background canvas.

First create an Object3D and add to the scene. Update the code anywhere, save it, the HMR kicks in and update the DOM alongside the 3D canvas nicely, but my object gets duplicated. Another save, another unwanted instance and so on.

I created a module just to keep a "global" array persistent for holding my single 3d object but didn't work, duplicates for each re-run. However, after a while I made a proper singleton class instance, loaded with a Map as my object container and voila, no more duplication, works like a proper session, keep any object instances unchanged. I'm not really sure why.

Collapse
ryabinin profile image
Vlad R Author

interesting, thanks for sharing your use case, Andras!