So sánh singleton và atact class

Dependency injection does not need to be complex at all, and it's absolutely worth learning and using. Usually it's complicated by the usage of dependency injection frameworks, but they aren't necessary.

In its simplest form, dependency injection is passing dependencies instead of importing or constructing them. This can be implimented by simply using a parameter for what would be imported. Lets say you have a component named MyList that needs to use RESTClient to fetch some data and display it to the user. The "singleton" approach would look something like this:

import restClient from '...' // import singleton
class MyList extends React.Component {
  // use restClient and render stuff
}

This tightly-couples MyList to restClient, and there is no way you can unit test MyList without testing restClient. The DI approach would look something like this:

function MyListFactory(restClient) {
  class MyList extends React.Component {
    // use restClient and render stuff
  }
  return MyList
}

That's all it takes to use DI. It adds at most two lines of code and you get to eliminate an import. The reason why I introduced a new function "factory" is because AFAIK you can't pass additional constructor parameters in React, and I prefer not to pass these things down through React properties because it's not portable and all parent components must know to pass all props to children.

So now you have a function to construct MyList components, but how do you use it? The DI pattern bubbles up the dependency chain. Say you have a component

function MyListFactory(restClient) {
  class MyList extends React.Component {
    // use restClient and render stuff
  }
  return MyList
}

2 that uses MyList. The "singleton" approach would be:

import MyList from '...'
class MyApp extends React.Component {
  render() {
    return 
  }
}

The DI approach is:

function MyAppFactory(ListComponent) {
  class MyApp extends React.Component {
    render() {
      return 
    }
  }
  return MyApp
}

Now we can test

function MyListFactory(restClient) {
  class MyList extends React.Component {
    // use restClient and render stuff
  }
  return MyList
}

2 without testing MyList directly. We could even reuse

function MyListFactory(restClient) {
  class MyList extends React.Component {
    // use restClient and render stuff
  }
  return MyList
}

2 with a completely different kind of list. This pattern bubbles up to the composition root. This is where you call your factories and wire all the components.

import RESTClient from '...'
import MyListFactory from '...'
import MyAppFactory from '...'
const restClient = new RESTClient(...)
const MyList = MyListFactory(restClient)
const MyApp = MyAppFactory(MyList)
ReactDOM.render(, document.getElementById('app'))

Now our system uses a single instance of RESTClient, but we've designed it in a way that components are loosely-coupled and easy to test.

Singletons are classes which can be instantiated once, and can be accessed globally. This single instance can be shared throughout our application, which makes Singletons great for managing global state in an application.

First, let’s take a look at what a singleton can look like using an ES2015 class. For this example, we’re going to build a Counter class that has:

  • a getInstance method that returns the value of the instance
  • a

    let counter = 0; class Counter { getInstance() {

    return this;  
    
    } getCount() {
    return counter;  
    
    } increment() {
    return ++counter;  
    
    } decrement() {
    return --counter;  
    
    } } const counter1 = new Counter(); const counter2 = new Counter(); console.log(counter1.getInstance() === counter2.getInstance()); // false

    0 method that returns the current value of the

    let counter = 0; class Counter { getInstance() {

    return this;  
    
    } getCount() {
    return counter;  
    
    } increment() {
    return ++counter;  
    
    } decrement() {
    return --counter;  
    
    } } const counter1 = new Counter(); const counter2 = new Counter(); console.log(counter1.getInstance() === counter2.getInstance()); // false

    1 variable
  • an

    let counter = 0; class Counter { getInstance() {

    return this;  
    
    } getCount() {
    return counter;  
    
    } increment() {
    return ++counter;  
    
    } decrement() {
    return --counter;  
    
    } } const counter1 = new Counter(); const counter2 = new Counter(); console.log(counter1.getInstance() === counter2.getInstance()); // false

    2 method that increments the value of

    let counter = 0; class Counter { getInstance() {

    return this;  
    
    } getCount() {
    return counter;  
    
    } increment() {
    return ++counter;  
    
    } decrement() {
    return --counter;  
    
    } } const counter1 = new Counter(); const counter2 = new Counter(); console.log(counter1.getInstance() === counter2.getInstance()); // false

    1 by one
  • a

    let counter = 0; class Counter { getInstance() {

    return this;  
    
    } getCount() {
    return counter;  
    
    } increment() {
    return ++counter;  
    
    } decrement() {
    return --counter;  
    
    } } const counter1 = new Counter(); const counter2 = new Counter(); console.log(counter1.getInstance() === counter2.getInstance()); // false

    4 method that decrements the value of

    let counter = 0; class Counter { getInstance() {

    return this;  
    
    } getCount() {
    return counter;  
    
    } increment() {
    return ++counter;  
    
    } decrement() {
    return --counter;  
    
    } } const counter1 = new Counter(); const counter2 = new Counter(); console.log(counter1.getInstance() === counter2.getInstance()); // false

    1 by one

let counter = 0;

class Counter {
  getInstance() {
    return this;
  }

  getCount() {
    return counter;
  }

  increment() {
    return ++counter;
  }

  decrement() {
    return --counter;
  }
}

However, this class doesn’t meet the criteria for a Singleton! A Singleton should only be able to get instantiated once. Currently, we can create multiple instances of the Counter class.

let counter = 0;

class Counter {
  getInstance() {
    return this;
  }

  getCount() {
    return counter;
  }

  increment() {
    return ++counter;
  }

  decrement() {
    return --counter;
  }
}

const counter1 = new Counter();
const counter2 = new Counter();

console.log(counter1.getInstance() === counter2.getInstance()); // false

By calling the

let counter = 0;

class Counter {
  getInstance() {
    return this;
  }

  getCount() {
    return counter;
  }

  increment() {
    return ++counter;
  }

  decrement() {
    return --counter;
  }
}

const counter1 = new Counter();
const counter2 = new Counter();

console.log(counter1.getInstance() === counter2.getInstance()); // false

7 method twice, we just set

let counter = 0;

class Counter {
  getInstance() {
    return this;
  }

  getCount() {
    return counter;
  }

  increment() {
    return ++counter;
  }

  decrement() {
    return --counter;
  }
}

const counter1 = new Counter();
const counter2 = new Counter();

console.log(counter1.getInstance() === counter2.getInstance()); // false

8 and

let counter = 0;

class Counter {
  getInstance() {
    return this;
  }

  getCount() {
    return counter;
  }

  increment() {
    return ++counter;
  }

  decrement() {
    return --counter;
  }
}

const counter1 = new Counter();
const counter2 = new Counter();

console.log(counter1.getInstance() === counter2.getInstance()); // false

9 equal to different instances. The values returned by the getInstance method on

let counter = 0;

class Counter {
  getInstance() {
    return this;
  }

  getCount() {
    return counter;
  }

  increment() {
    return ++counter;
  }

  decrement() {
    return --counter;
  }
}

const counter1 = new Counter();
const counter2 = new Counter();

console.log(counter1.getInstance() === counter2.getInstance()); // false

8 and

let counter = 0;

class Counter {
  getInstance() {
    return this;
  }

  getCount() {
    return counter;
  }

  increment() {
    return ++counter;
  }

  decrement() {
    return --counter;
  }
}

const counter1 = new Counter();
const counter2 = new Counter();

console.log(counter1.getInstance() === counter2.getInstance()); // false

9 effectively returned references to different instances: they aren’t strictly equal!

Let’s make sure that only one instance of the Counter class can be created.

One way to make sure that only one instance can be created, is by creating a variable called

let instance;
let counter = 0;

class Counter {
  constructor() {
    if (instance) {
      throw new Error("You can only create one instance!");
    }
    instance = this;
  }

  getInstance() {
    return this;
  }

  getCount() {
    return counter;
  }

  increment() {
    return ++counter;
  }

  decrement() {
    return --counter;
  }
}

const counter1 = new Counter();
const counter2 = new Counter();
// Error: You can only create one instance!

4. In the constructor of Counter, we can set

let instance;
let counter = 0;

class Counter {
  constructor() {
    if (instance) {
      throw new Error("You can only create one instance!");
    }
    instance = this;
  }

  getInstance() {
    return this;
  }

  getCount() {
    return counter;
  }

  increment() {
    return ++counter;
  }

  decrement() {
    return --counter;
  }
}

const counter1 = new Counter();
const counter2 = new Counter();
// Error: You can only create one instance!

4 equal to a reference to the instance when a new instance is created. We can prevent new instantiations by checking if the

let instance;
let counter = 0;

class Counter {
  constructor() {
    if (instance) {
      throw new Error("You can only create one instance!");
    }
    instance = this;
  }

  getInstance() {
    return this;
  }

  getCount() {
    return counter;
  }

  increment() {
    return ++counter;
  }

  decrement() {
    return --counter;
  }
}

const counter1 = new Counter();
const counter2 = new Counter();
// Error: You can only create one instance!

4 variable already had a value. If that’s the case, an instance already exists. This shouldn’t happen: an error should get thrown to let the user know

let instance;
let counter = 0;

class Counter {
  constructor() {
    if (instance) {
      throw new Error("You can only create one instance!");
    }
    instance = this;
  }

  getInstance() {
    return this;
  }

  getCount() {
    return counter;
  }

  increment() {
    return ++counter;
  }

  decrement() {
    return --counter;
  }
}

const counter1 = new Counter();
const counter2 = new Counter();
// Error: You can only create one instance!

Perfect! We aren’t able to create multiple instances anymore.

Let’s export the Counter instance from the

let instance;
let counter = 0;

class Counter {
  constructor() {
    if (instance) {
      throw new Error("You can only create one instance!");
    }
    instance = this;
  }

  getInstance() {
    return this;
  }

  getCount() {
    return counter;
  }

  increment() {
    return ++counter;
  }

  decrement() {
    return --counter;
  }
}

const counter1 = new Counter();
const counter2 = new Counter();
// Error: You can only create one instance!

9 file. But before doing so, we should freeze the instance as well. The

let instance;
let counter = 0;

class Counter {
  constructor() {
    if (instance) {
      throw new Error("You can only create one instance!");
    }
    instance = this;
  }

  getInstance() {
    return this;
  }

  getCount() {
    return counter;
  }

  increment() {
    return ++counter;
  }

  decrement() {
    return --counter;
  }
}

const singletonCounter = Object.freeze(new Counter());
export default singletonCounter;

0 method makes sure that consuming code cannot modify the Singleton. Properties on the frozen instance cannot be added or modified, which reduces the risk of accidentally overwriting the values on the Singleton.

let instance;
let counter = 0;

class Counter {
  constructor() {
    if (instance) {
      throw new Error("You can only create one instance!");
    }
    instance = this;
  }

  getInstance() {
    return this;
  }

  getCount() {
    return counter;
  }

  increment() {
    return ++counter;
  }

  decrement() {
    return --counter;
  }
}

const singletonCounter = Object.freeze(new Counter());
export default singletonCounter;


Let’s take a look at an application that implements the Counter example. We have the following files:

  • let instance; let counter = 0; class Counter { constructor() {

    if (instance) {  
      throw new Error("You can only create one instance!");  
    }  
    instance = this;  
    
    } getInstance() {
    return this;  
    
    } getCount() {
    return counter;  
    
    } increment() {
    return ++counter;  
    
    } decrement() {
    return --counter;  
    
    } } const counter1 = new Counter(); const counter2 = new Counter(); // Error: You can only create one instance!

    9: contains the Counter class, and exports a Counter instance as its default export
  • let instance; let counter = 0; class Counter { constructor() {

    if (instance) {  
      throw new Error("You can only create one instance!");  
    }  
    instance = this;  
    
    } getInstance() {
    return this;  
    
    } getCount() {
    return counter;  
    
    } increment() {
    return ++counter;  
    
    } decrement() {
    return --counter;  
    
    } } const singletonCounter = Object.freeze(new Counter()); export default singletonCounter;

    5: loads the

    let instance; let counter = 0; class Counter { constructor() {

    if (instance) {  
      throw new Error("You can only create one instance!");  
    }  
    instance = this;  
    
    } getInstance() {
    return this;  
    
    } getCount() {
    return counter;  
    
    } increment() {
    return ++counter;  
    
    } decrement() {
    return --counter;  
    
    } } const singletonCounter = Object.freeze(new Counter()); export default singletonCounter;

    6 and

    let instance; let counter = 0; class Counter { constructor() {

    if (instance) {  
      throw new Error("You can only create one instance!");  
    }  
    instance = this;  
    
    } getInstance() {
    return this;  
    
    } getCount() {
    return counter;  
    
    } increment() {
    return ++counter;  
    
    } decrement() {
    return --counter;  
    
    } } const singletonCounter = Object.freeze(new Counter()); export default singletonCounter;

    7 modules
  • let instance; let counter = 0; class Counter { constructor() {

    if (instance) {  
      throw new Error("You can only create one instance!");  
    }  
    instance = this;  
    
    } getInstance() {
    return this;  
    
    } getCount() {
    return counter;  
    
    } increment() {
    return ++counter;  
    
    } decrement() {
    return --counter;  
    
    } } const singletonCounter = Object.freeze(new Counter()); export default singletonCounter;

    6: imports Counter, and adds Counter’s

    let counter = 0; class Counter { getInstance() {

    return this;  
    
    } getCount() {
    return counter;  
    
    } increment() {
    return ++counter;  
    
    } decrement() {
    return --counter;  
    
    } } const counter1 = new Counter(); const counter2 = new Counter(); console.log(counter1.getInstance() === counter2.getInstance()); // false

    2 method as an event listener to the red button, and logs the current value of

    let counter = 0; class Counter { getInstance() {

    return this;  
    
    } getCount() {
    return counter;  
    
    } increment() {
    return ++counter;  
    
    } decrement() {
    return --counter;  
    
    } } const counter1 = new Counter(); const counter2 = new Counter(); console.log(counter1.getInstance() === counter2.getInstance()); // false

    1 by invoking the

    let counter = 0; class Counter { getInstance() {

    return this;  
    
    } getCount() {
    return counter;  
    
    } increment() {
    return ++counter;  
    
    } decrement() {
    return --counter;  
    
    } } const counter1 = new Counter(); const counter2 = new Counter(); console.log(counter1.getInstance() === counter2.getInstance()); // false

    0 method
  • let instance; let counter = 0; class Counter { constructor() {

    if (instance) {  
      throw new Error("You can only create one instance!");  
    }  
    instance = this;  
    
    } getInstance() {
    return this;  
    
    } getCount() {
    return counter;  
    
    } increment() {
    return ++counter;  
    
    } decrement() {
    return --counter;  
    
    } } const singletonCounter = Object.freeze(new Counter()); export default singletonCounter;

    7: imports Counter, and adds Counter’s

    let counter = 0; class Counter { getInstance() {

    return this;  
    
    } getCount() {
    return counter;  
    
    } increment() {
    return ++counter;  
    
    } decrement() {
    return --counter;  
    
    } } const counter1 = new Counter(); const counter2 = new Counter(); console.log(counter1.getInstance() === counter2.getInstance()); // false

    2 method as an event listener to the blue button, and logs the current value of

    let counter = 0; class Counter { getInstance() {

    return this;  
    
    } getCount() {
    return counter;  
    
    } increment() {
    return ++counter;  
    
    } decrement() {
    return --counter;  
    
    } } const counter1 = new Counter(); const counter2 = new Counter(); console.log(counter1.getInstance() === counter2.getInstance()); // false

    1 by invoking the

    let counter = 0; class Counter { getInstance() {

    return this;  
    
    } getCount() {
    return counter;  
    
    } increment() {
    return ++counter;  
    
    } decrement() {
    return --counter;  
    
    } } const counter1 = new Counter(); const counter2 = new Counter(); console.log(counter1.getInstance() === counter2.getInstance()); // false

    0 method

JavaScript icon

counter.js

JavaScript icon

redButton.js

JavaScript icon

blueButton.js

1import "./redButton";

2import "./blueButton";

3

4console.log("Click on either of the buttons 🚀!");

Both

let instance;
let counter = 0;

class Counter {
  constructor() {
    if (instance) {
      throw new Error("You can only create one instance!");
    }
    instance = this;
  }

  getInstance() {
    return this;
  }

  getCount() {
    return counter;
  }

  increment() {
    return ++counter;
  }

  decrement() {
    return --counter;
  }
}

const singletonCounter = Object.freeze(new Counter());
export default singletonCounter;

7 and

let instance;
let counter = 0;

class Counter {
  constructor() {
    if (instance) {
      throw new Error("You can only create one instance!");
    }
    instance = this;
  }

  getInstance() {
    return this;
  }

  getCount() {
    return counter;
  }

  increment() {
    return ++counter;
  }

  decrement() {
    return --counter;
  }
}

const singletonCounter = Object.freeze(new Counter());
export default singletonCounter;

6 import the same instance from

let instance;
let counter = 0;

class Counter {
  constructor() {
    if (instance) {
      throw new Error("You can only create one instance!");
    }
    instance = this;
  }

  getInstance() {
    return this;
  }

  getCount() {
    return counter;
  }

  increment() {
    return ++counter;
  }

  decrement() {
    return --counter;
  }
}

const counter1 = new Counter();
const counter2 = new Counter();
// Error: You can only create one instance!

9. This instance is imported as Counter in both files.

When we invoke the

let counter = 0;

class Counter {
  getInstance() {
    return this;
  }

  getCount() {
    return counter;
  }

  increment() {
    return ++counter;
  }

  decrement() {
    return --counter;
  }
}

const counter1 = new Counter();
const counter2 = new Counter();

console.log(counter1.getInstance() === counter2.getInstance()); // false

2 method in either

let instance;
let counter = 0;

class Counter {
  constructor() {
    if (instance) {
      throw new Error("You can only create one instance!");
    }
    instance = this;
  }

  getInstance() {
    return this;
  }

  getCount() {
    return counter;
  }

  increment() {
    return ++counter;
  }

  decrement() {
    return --counter;
  }
}

const singletonCounter = Object.freeze(new Counter());
export default singletonCounter;

6 or

let instance;
let counter = 0;

class Counter {
  constructor() {
    if (instance) {
      throw new Error("You can only create one instance!");
    }
    instance = this;
  }

  getInstance() {
    return this;
  }

  getCount() {
    return counter;
  }

  increment() {
    return ++counter;
  }

  decrement() {
    return --counter;
  }
}

const singletonCounter = Object.freeze(new Counter());
export default singletonCounter;

7, the value of the

let counter = 0;

class Counter {
  getInstance() {
    return this;
  }

  getCount() {
    return counter;
  }

  increment() {
    return ++counter;
  }

  decrement() {
    return --counter;
  }
}

const counter1 = new Counter();
const counter2 = new Counter();

console.log(counter1.getInstance() === counter2.getInstance()); // false

1 property on the Counter instance updates in both files. It doesn’t matter whether we click on the red or blue button: the same value is shared among all instances. This is why the counter keeps incrementing by one, even though we’re invoking the method in different files.


Tradeoffs

Restricting the instantiation to just one instance could potentially save a lot of memory space. Instead of having to set up memory for a new instance each time, we only have to set up memory for that one instance, which is referenced throughout the application. However, Singletons are actually considered an anti-pattern, and can (or.. should) be avoided in JavaScript.

In many programming languages, such as Java or C++, it’s not possible to directly create objects the way we can in JavaScript. In those object-oriented programming languages, we need to create a class, which creates an object. That created object has the value of the instance of the class, just like the value of

let instance;
let counter = 0;

class Counter {
  constructor() {
    if (instance) {
      throw new Error("You can only create one instance!");
    }
    instance = this;
  }

  getInstance() {
    return this;
  }

  getCount() {
    return counter;
  }

  increment() {
    return ++counter;
  }

  decrement() {
    return --counter;
  }
}

const counter1 = new Counter();
const counter2 = new Counter();
// Error: You can only create one instance!

4 in the JavaScript example.

However, the class implementation shown in the examples above is actually overkill. Since we can directly create objects in JavaScript, we can simply use a regular object to achieve the exact same result. Let’s cover some of the disadvantages of using Singletons!

Using a regular object

Let’s use the same example as we saw previously. However this time, the

let counter = 0;

class Counter {
  getInstance() {
    return this;
  }

  getCount() {
    return counter;
  }

  increment() {
    return ++counter;
  }

  decrement() {
    return --counter;
  }
}

const counter1 = new Counter();
const counter2 = new Counter();

console.log(counter1.getInstance() === counter2.getInstance()); // false

1 is simply an object containing:

  • a 1import Counter from "../src/counterTest"; 2 3test("incrementing 1 time should be 1", () => { 4 Counter.increment(); 5 expect(Counter.getCount()).toBe(1); 6}); 7 8test("incrementing 3 extra times should be 4", () => { 9 Counter.increment(); 10 Counter.increment(); 11 Counter.increment(); 12 expect(Counter.getCount()).toBe(4); 13}); 14 15test("decrementing 1 times should be 3", () => { 16 Counter.decrement(); 17 expect(Counter.getCount()).toBe(3); 18}); 1 property
  • an

    let counter = 0; class Counter { getInstance() {

    return this;  
    
    } getCount() {
    return counter;  
    
    } increment() {
    return ++counter;  
    
    } decrement() {
    return --counter;  
    
    } } const counter1 = new Counter(); const counter2 = new Counter(); console.log(counter1.getInstance() === counter2.getInstance()); // false

    2 method that increments the value of 1import Counter from "../src/counterTest"; 2 3test("incrementing 1 time should be 1", () => { 4 Counter.increment(); 5 expect(Counter.getCount()).toBe(1); 6}); 7 8test("incrementing 3 extra times should be 4", () => { 9 Counter.increment(); 10 Counter.increment(); 11 Counter.increment(); 12 expect(Counter.getCount()).toBe(4); 13}); 14 15test("decrementing 1 times should be 3", () => { 16 Counter.decrement(); 17 expect(Counter.getCount()).toBe(3); 18}); 1 by one
  • a

    let counter = 0; class Counter { getInstance() {

    return this;  
    
    } getCount() {
    return counter;  
    
    } increment() {
    return ++counter;  
    
    } decrement() {
    return --counter;  
    
    } } const counter1 = new Counter(); const counter2 = new Counter(); console.log(counter1.getInstance() === counter2.getInstance()); // false

    4 method that decrements the value of 1import Counter from "../src/counterTest"; 2 3test("incrementing 1 time should be 1", () => { 4 Counter.increment(); 5 expect(Counter.getCount()).toBe(1); 6}); 7 8test("incrementing 3 extra times should be 4", () => { 9 Counter.increment(); 10 Counter.increment(); 11 Counter.increment(); 12 expect(Counter.getCount()).toBe(4); 13}); 14 15test("decrementing 1 times should be 3", () => { 16 Counter.decrement(); 17 expect(Counter.getCount()).toBe(3); 18}); 1 by one

JavaScript icon

counter.js

1let count = 0;

2

3const counter = {

4 increment() {

5 return ++count;

6 },

7 decrement() {

8 return --count;

9 }

10};

11

12Object.freeze(counter);

13export { counter };

Since objects are passed by reference, both

let instance;
let counter = 0;

class Counter {
  constructor() {
    if (instance) {
      throw new Error("You can only create one instance!");
    }
    instance = this;
  }

  getInstance() {
    return this;
  }

  getCount() {
    return counter;
  }

  increment() {
    return ++counter;
  }

  decrement() {
    return --counter;
  }
}

const singletonCounter = Object.freeze(new Counter());
export default singletonCounter;

6 and

let instance;
let counter = 0;

class Counter {
  constructor() {
    if (instance) {
      throw new Error("You can only create one instance!");
    }
    instance = this;
  }

  getInstance() {
    return this;
  }

  getCount() {
    return counter;
  }

  increment() {
    return ++counter;
  }

  decrement() {
    return --counter;
  }
}

const singletonCounter = Object.freeze(new Counter());
export default singletonCounter;

7 are importing a reference to the same

let counter = 0;

class Counter {
  getInstance() {
    return this;
  }

  getCount() {
    return counter;
  }

  increment() {
    return ++counter;
  }

  decrement() {
    return --counter;
  }
}

const counter1 = new Counter();
const counter2 = new Counter();

console.log(counter1.getInstance() === counter2.getInstance()); // false

1 object. Modifying the value of

1import Counter from "../src/counterTest";

2

3test("incrementing 1 time should be 1", () => {

4 Counter.increment();

5 expect(Counter.getCount()).toBe(1);

6});

7

8test("incrementing 3 extra times should be 4", () => {

9 Counter.increment();

10 Counter.increment();

11 Counter.increment();

12 expect(Counter.getCount()).toBe(4);

13});

14

15test("decrementing 1 times should be 3", () => {

16 Counter.decrement();

17 expect(Counter.getCount()).toBe(3);

18});

1 in either of these files will modify the value on the

let counter = 0;

class Counter {
  getInstance() {
    return this;
  }

  getCount() {
    return counter;
  }

  increment() {
    return ++counter;
  }

  decrement() {
    return --counter;
  }
}

const counter1 = new Counter();
const counter2 = new Counter();

console.log(counter1.getInstance() === counter2.getInstance()); // false

1, which is visible in both files.

Testing

Testing code that relies on a Singleton can get tricky. Since we can’t create new instances each time, all tests rely on the modification to the global instance of the previous test. The order of the tests matter in this case, and one small modification can lead to an entire test suite failing. After testing, we need to reset the entire instance in order to reset the modifications made by the tests.

JavaScript icon

superCounter.js

1import Counter from "../src/counterTest";

2

3test("incrementing 1 time should be 1", () => {

4 Counter.increment();

5 expect(Counter.getCount()).toBe(1);

6});

7

8test("incrementing 3 extra times should be 4", () => {

9 Counter.increment();

10 Counter.increment();

11 Counter.increment();

12 expect(Counter.getCount()).toBe(4);

13});

14

15test("decrementing 1 times should be 3", () => {

16 Counter.decrement();

17 expect(Counter.getCount()).toBe(3);

18});

Dependency hiding

When importing another module,

1import Counter from "./counter";

2

3export default class SuperCounter {

4 constructor() {

5 this.count = 0;

6 }

7

8 increment() {

9 Counter.increment();

10 return (this.count += 100);

11 }

12

13 decrement() {

14 Counter.decrement();

15 return (this.count -= 100);

16 }

17}

1 in this case, it may not be obvious that module is importing a Singleton. In other files, such as

let instance;
let counter = 0;

class Counter {
  constructor() {
    if (instance) {
      throw new Error("You can only create one instance!");
    }
    instance = this;
  }

  getInstance() {
    return this;
  }

  getCount() {
    return counter;
  }

  increment() {
    return ++counter;
  }

  decrement() {
    return --counter;
  }
}

const singletonCounter = Object.freeze(new Counter());
export default singletonCounter;

5 in this case, we may be importing that module and invoke its methods. This way, we accidentally modify the values in the Singleton. This can lead to unexpected behavior, since multiple instances of the Singleton can be shared throughout the application, which would all get modified as well.

JavaScript icon

superCounter.js

1import Counter from "./counter";

2

3export default class SuperCounter {

4 constructor() {

5 this.count = 0;

6 }

7

8 increment() {

9 Counter.increment();

10 return (this.count += 100);

11 }

12

13 decrement() {

14 Counter.decrement();

15 return (this.count -= 100);

16 }

17}

Global behavior

A Singleton instance should be able to get referenced throughout the entire app. Global variables essentially show the same behavior: since global variables are available on the global scope, we can access those variables throughout the application.

Having global variables is generally considered as a bad design decision. Global scope pollution can end up in accidentally overwriting the value of a global variable, which can lead to a lot of unexpected behavior.

In ES2015, creating global variables is fairly uncommon. The new

1import Counter from "./counter";

2

3export default class SuperCounter {

4 constructor() {

5 this.count = 0;

6 }

7

8 increment() {

9 Counter.increment();

10 return (this.count += 100);

11 }

12

13 decrement() {

14 Counter.decrement();

15 return (this.count -= 100);

16 }

17}

3 and

1import Counter from "./counter";

2

3export default class SuperCounter {

4 constructor() {

5 this.count = 0;

6 }

7

8 increment() {

9 Counter.increment();

10 return (this.count += 100);

11 }

12

13 decrement() {

14 Counter.decrement();

15 return (this.count -= 100);

16 }

17}

4 keyword prevent developers from accidentally polluting the global scope, by keeping variables declared with these two keywords block-scoped. The new

1import Counter from "./counter";

2

3export default class SuperCounter {

4 constructor() {

5 this.count = 0;

6 }

7

8 increment() {

9 Counter.increment();

10 return (this.count += 100);

11 }

12

13 decrement() {

14 Counter.decrement();

15 return (this.count -= 100);

16 }

17}

5 system in JavaScript makes creating globally accessible values easier without polluting the global scope, by being able to

1import Counter from "./counter";

2

3export default class SuperCounter {

4 constructor() {

5 this.count = 0;

6 }

7

8 increment() {

9 Counter.increment();

10 return (this.count += 100);

11 }

12

13 decrement() {

14 Counter.decrement();

15 return (this.count -= 100);

16 }

17}

6 values from a module, and

1import Counter from "./counter";

2

3export default class SuperCounter {

4 constructor() {

5 this.count = 0;

6 }

7

8 increment() {

9 Counter.increment();

10 return (this.count += 100);

11 }

12

13 decrement() {

14 Counter.decrement();

15 return (this.count -= 100);

16 }

17}

7 those values in other files.

However, the common usecase for a Singleton is to have some sort of global state throughout your application. Having multiple parts of your codebase rely on the same mutable object can lead to unexpected behavior.

Usually, certain parts of the codebase modify the values within global state, whereas others consume that data. The order of execution here is important: we don’t want to accidentally consume data first, when there is no data to consume (yet)! Understanding the data flow when using a global state can get very tricky as your application grows, and dozens of components rely on each other.

State management in React

In React, we often rely on a global state through state management tools such as Redux or React Context instead of using Singletons. Although their global state behavior might seem similar to that of a Singleton, these tools provide a read-only state rather than the mutable state of the Singleton. When using Redux, only pure function reducers can update the state, after a component has sent an action through a dispatcher.

Although the downsides to having a global state don’t magically disappear by using these tools, we can at least make sure that the global state is mutated the way we intend it, since components cannot update the state directly.