ES6 Singleton vs Instantiating a Class once

50,713

Solution 1

The difference is if you want to test things.

Say you have api.spec.js test file. And that your API thingy has one dependency, like those Constants.

Specifically, constructor in both your versions takes one parameter, your Constants import.

So your constructor looks like this:

class API {
    constructor(constants) {
      this.API_URL = constants.API_URL;
    }
    ...
}



// single-instance method first
import API from './api';
describe('Single Instance', () => {
    it('should take Constants as parameter', () => {
        const mockConstants = {
            API_URL: "fake_url"
        }
        const api = new API(mockConstants); // all good, you provided mock here.
    });
});

Now, with exporting instance, there's no mocking.

import API from './api';
describe('Singleton', () => {
    it('should let us mock the constants somehow', () => {
        const mockConstants = {
            API_URL: "fake_url"
        }
        // erm... now what?
    });
});

With instantiated object exported, you can't (easily and sanely) change its behavior.

Solution 2

I would recommend neither. This is totally overcomplicated. If you only need one object, do not use the class syntax! Just go for

import Constants from '../constants';

export default {
  url: Constants.API_URL,
  getCities() {
    return fetch(this.url, { method: 'get' }).then(response => response.json());
  }
};

import API from './services/api-service'

or even simpler

import Constants from '../constants';

export const url = Constants.API_URL;
export function getCities() {
  return fetch(url, { method: 'get' }).then(response => response.json());
}

import * as API from './services/api-service'

Solution 3

Both are different ways. Exporting a class like as below

const APIobj = new _API();
export default APIobj;   //shortcut=> export new _API()

and then importing like as below in multiple files would point to same instance and a way of creating Singleton pattern.

import APIobj from './services/api-service'

Whereas the other way of exporting the class directly is not singleton as in the file where we are importing we need to new up the class and this will create a separate instance for each newing up Exporting class only:

export default API;

Importing class and newing up

import API from './services/api-service';
let api = new API()

Solution 4

Another reason to use Singleton Pattern is in some frameworks (like Polymer 1.0) you can't use export syntax.
That's why second option (Singleton pattern) is more useful, for me.

Hope it helps.

Share:
50,713
Aaron
Author by

Aaron

I work for Globant and am currently on assignment at Google. I love building stuff!

Updated on July 08, 2022

Comments

  • Aaron
    Aaron almost 2 years

    I see patterns which make use of a singleton pattern using ES6 classes and I am wondering why I would use them as opposed to just instantiating the class at the bottom of the file and exporting the instance. Is there some kind of negative drawback to doing this? For example:

    ES6 Exporting Instance:

    import Constants from '../constants';
    
    class _API {
      constructor() {
        this.url = Constants.API_URL;
      }
    
      getCities() {
        return fetch(this.url, { method: 'get' })
          .then(response => response.json());
      }
    }
    
    const API = new _API();
    export default API;
    

    Usage:

    import API from './services/api-service'
    

    What is the difference from using the following Singleton pattern? Are there any reasons for using one from the other? Im actually more curious to know if the first example I gave can have issues that I am not aware of.

    Singleton Pattern:

    import Constants from '../constants';
    
    let instance = null;
    
    class API {
      constructor() {
    
        if(!instance){
          instance = this;
        }
    
        this.url = Constants.API_URL;
    
        return instance;
      }
    
      getCities() {
        return fetch(this.url, { method: 'get' })
          .then(response => response.json());
      }
    }
    
    export default API;
    

    Usage:

    import API from './services/api-service';
    
    let api = new API()
    
  • slebetman
    slebetman over 6 years
    This is the correct, idiomatic way to do this in js
  • slebetman
    slebetman over 6 years
    Note that javascript has always had singletons built-in to the language. We just don't call them singletons, we call them object literals. So whenever you need a single instance of an object js programmers will automatically create an object literal. In js, a lot of things that are "design patterns" in other languages are built-in syntax.
  • Josh Stuart
    Josh Stuart almost 6 years
    Javascript developers tend to hardcode all their dependencies via import for whatever reason. I agree that it's better practice to pass in dependencies via the constructor so that it's a) testable, b) reusable.
  • Aaron
    Aaron almost 6 years
    I awarded this answer because it actually answers my initial question. Thanks.
  • César Alberca
    César Alberca over 5 years
    I would say that this is not appropriate if you want to inject as dependencies the fetch, so it's easier to test.
  • Bergi
    Bergi over 5 years
    @CésarAlberca OP didn't use dependency injection, so I didn't consider this. And still you wouldn't need a class for that, a module import or factory function should be enough to make fetch mockable.
  • bumbur
    bumbur about 5 years
    It might also not work when you would like to set a user token for api so that you don't need to pass it all the time to api calls that require it.
  • Bergi
    Bergi about 5 years
    @bumbur if you need multiple api instances with different user tokens, then yes go for a class.
  • Bergi
    Bergi about 5 years
    @codewise The link in the answer explains why class syntax should be avoided for singleton objects.
  • codewise
    codewise about 5 years
    @Bergi Thanks for the coaching. I did some reading yesterday about JavaScript and classes. Started reading Eric Elliot's work carefully. Very illuminating. I think I was confused by my OO background and the normal pattern of checking object types.
  • Shishir Arora
    Shishir Arora about 3 years
    "// erm... now what?" why not? API.url = MockConstants.API_URL; Object has all those instance properties/methods, whatever class had access with "this".. But ofcourse mutation would give rise to other problems in unit tests
  • Zlatko
    Zlatko about 3 years
    @ShishirArora the problem is with this. You have a test, where you assert that API.url === 'example.com'. All good. Then someone inserts this API.url === 'something else' before your test - you're modifying the API object for the whole test suite, not just one single test instance. Now you broke other tests - even though you didn't (potentially) break the code itself.
  • Shishir Arora
    Shishir Arora about 3 years
    That should be part of tear down operation in all test suits. You should clean all side-effects left by a test, before starting a new one.parrellelizing tests would be a problem, though.
  • Zlatko
    Zlatko about 3 years
    That's kind of a problem. With the singleton version, you'd have either the parallelization issue, or teardown/setup issue, or write a lot of code, just so you can write a test. The other version avoids this. Of course there are other reasons, testing is just one. And it's not like one thing is better than the other, they both have their place.
  • Zlatko
    Zlatko about 3 years
    Additionally, what if you want this behavior in more then just the test? Like, I want to start X instances of a database driver, and each connects to its own database server? Same problem, but now it's not the test any more.
  • java-addict301
    java-addict301 over 2 years
    class syntax allows for constructors and dependency injection to support unit testing (especially if you're using Typescript).
  • Bergi
    Bergi over 2 years
    @java-addict301 You don't need constructors for dependency injection. Factories or partial application work just as well. Also the question was about the singleton pattern, without any DI in the constructor. In that case, just use a module - and modern unit testing frameworks even allow injecting module dependencies.