98

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()
Aaron
  • 2,364
  • 2
  • 31
  • 56

5 Answers5

76

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'
Bergi
  • 630,263
  • 148
  • 957
  • 1,375
  • 3
    This is the correct, idiomatic way to do this in js – slebetman Jan 21 '18 at 15:59
  • 26
    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. – slebetman Jan 21 '18 at 16:02
  • I would say that this is not appropriate if you want to inject as dependencies the fetch, so it's easier to test. – César Alberca Dec 18 '18 at 19:43
  • 1
    @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. – Bergi Dec 18 '18 at 19:56
  • 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. – bumbur Feb 18 '19 at 11:07
  • @bumbur if you need multiple api instances with different user tokens, then yes go for a `class`. – Bergi Feb 18 '19 at 11:28
  • 3
    @codewise The [link](https://stackoverflow.com/q/38739499/1048572) in the answer explains why `class` syntax should be avoided for singleton objects. – Bergi Apr 03 '19 at 10:39
  • @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. – codewise Apr 03 '19 at 22:09
  • class syntax allows for constructors and dependency injection to support unit testing (especially if you're using Typescript). – java-addict301 Sep 05 '21 at 02:42
  • 1
    @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. – Bergi Sep 05 '21 at 10:49
49

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.

Raisen
  • 4,385
  • 2
  • 25
  • 40
Zlatko
  • 18,936
  • 14
  • 70
  • 123
  • 7
    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. – Josh Stuart Apr 27 '18 at 05:04
  • 2
    I awarded this answer because it actually answers my initial question. Thanks. – Aaron Jun 18 '18 at 16:19
  • "// 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 – Shishir Arora Apr 06 '21 at 21:39
  • @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. – Zlatko Apr 08 '21 at 09:52
  • 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. – Shishir Arora Apr 16 '21 at 15:21
  • 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 Apr 18 '21 at 10:48
  • 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. – Zlatko Apr 18 '21 at 10:53
13

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()
Sumer
  • 2,687
  • 24
  • 24
2

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.

CKE
  • 1,533
  • 19
  • 18
  • 29
Peter
  • 51
  • 4
0

Maybe I'm late, because this question is written in 2018, but it still appear in the top of result page when search for js singleton class and I think that it still not have the right answer even if the others ways works. but don't create a class instance. And this is my way to create a JS singleton class:

class TestClass {
    static getInstance(dev = true) {
        if (!TestClass.instance) {
            console.log('Creating new instance');
            Object.defineProperty(TestClass, 'instance', {
                value: new TestClass(dev),
                writable : false,
                enumerable : true,
                configurable : false
            });
        } else {
            console.log('Instance already exist');
        }
        return TestClass.instance;
    }
    random;
    constructor() {
        this.random = Math.floor(Math.random() * 99999);
    }
}
const instance1 = TestClass.getInstance();
console.log(`The value of random var of instance1 is: ${instance1.random}`);
const instance2 = TestClass.getInstance();
console.log(`The value of random var of instance2 is: ${instance2.random}`);

And this is the result of execution of this code.

Creating new instance
The value of random var of instance1 is: 14929
Instance already exist
The value of random var of instance2 is: 14929

Hope this can help someone

khalid
  • 121
  • 8
  • 2
    The singleton pattern in the OP is better than this. Your code has the problem that a) the constructor is still public and can be called by anyone b) `.instance` is public and can be altered by anyone (including setting it back to `null`) c) `getInstance(false)` can sometimes return a dev instance and sometimes a prod instance, totally independent of the passed argument. This is a prime educational example of why not to use singletons at all. – Bergi Mar 31 '22 at 16:09
  • Ya, it's good point of view, but you can simples resolve that problem using definePropriety and make it (static instance var) immutable, this is an exemple : // static instance; //commented static getInstance(dev = true) { if (!TestClass.instance) { Object.defineProperty(TestClass, 'instance', { value: new TestClass(dev), writable : false, enumerable : true, configurable : false }); After doing that, you cant change the instance anymore. I'll update the code. – khalid Apr 04 '22 at 18:52
  • but i didn't got what you mean about "c) getInstance(false) can sometimes return a dev instance and sometimes a prod instance" – khalid Apr 04 '22 at 18:56
  • 1
    I mean that when calling `getInstance(false)` one would expect this to always return a prod instance, no? But that's not true, it could also return a dev instance, depending on the rest of the code - spooky [action at a distance](https://en.wikipedia.org/wiki/Action_at_a_distance_(computer_programming)). A singleton getter should never have parameters. – Bergi Apr 04 '22 at 19:01
  • Your right, singleton with param is not a singleton, but I did it like this for education prepose, to show that even trying create new instance the object param didn't changed but I'll update the code. – khalid Apr 04 '22 at 19:34