💭 Intro
In this article, I want to share my experience using the singleton pattern as a service inside a production React application.
🌧️ Problem
We had many places to handle the localisation logic. Our application is a multi-language web app that supports three languages. This results in constant interaction with localisation. However, the issue is that functions are spread across the application for all utilities connected to localisation. Sometimes, even one function was using a set of two languages and the other three languages. Such a mess was creating bugs and misunderstandings.
⚡️ Pattern solution
Such a task can be solved by implementing a singleton pattern because it's shared across the whole application and provides strict access control. It helped our team to centralize the localisation helpers in one place, and it also helps us to reduce the risk of bugs and misunderstanding by providing clear documentation for the methods and usage of the service.
🤔 What is the Singleton pattern?
"Singleton is a creational design pattern that lets you ensure that a class has only one instance while providing a global access point to this instance." - Refactoring Guru.
💡 According to the statement it's exactly what we need!
First, we should have only one instance throughout the whole application, which should be the source of truth.
Second, the instance should be accessed globally and be protected from overwriting from another part of the application because the whole application should use the same number of languages.
💻 Implementation
1) Singleton Class Definition
export class LocalisationService {
private static instance: LocalisationService
}
This is the basic structure of our Singleton class, LocalisationService. We declare a private static variable instance of the same class. This variable will hold the single instance of our class.
2) Private Constructor and Static Creation Method
private constructor() {
this.languages = {
[LanguageCode.EN]: {
shortName: LanguageCode.EN,
longName: LanguageName.ENGLISH,
},
[LanguageCode.DE]: {
shortName: LanguageCode.DE,
longName: LanguageName.DEUTSCH,
},
[LanguageCode.FR]: {
shortName: LanguageCode.FR,
longName: LanguageName.FRANCAIS,
},
}
}
public static getInstance(): LocalizationService {
if (!LocalizationService.instance) {
LocalizationService.instance = new LocalizationService()
}
return LocalizationService.instance
}
Here, we have a private constructor that initializes the languages object. The constructor is private to prevent creating new instances of the class from outside. The getInstance method is a public static method that returns the single instance of the LocalizationService class. If the instance does not exist, it creates a new one.
3) Public Methods
export enum LanguageCode {
EN = "en",
DE = "de",
FR = "fr",
}
export enum LanguageName {
ENGLISH = "English",
DEUTSCH = "Deutsch",
FRANCAIS = "Français",
}
export interface Language {
shortName: LanguageCode
longName: LanguageName
}
These are the public methods of our Singleton class. getAllLanguages returns all available languages. getLanguageByShortName returns a specific language by its short name. getLanguagesExcept returns all languages except the one specified by the key.
4) Using the Singleton Service
import { LanguageCode, LocalizationService } from "services"
const localizationService = LocalizationService.getInstance()
This is how we use our Singleton service in our code. We import the LocalizationService and call the getInstance method to get the single instance of the service.
The code of the whole Service
export enum LanguageCode {
EN = "en",
DE = "de",
FR = "fr",
}
export enum LanguageName {
ENGLISH = "English",
DEUTSCH = "Deutsch",
FRANCAIS = "Français",
}
export interface Language {
shortName: LanguageCode
longName: LanguageName
}
/**
* Represents the localization service, responsible for managing language-related tasks.
* This class follows the Singleton pattern to ensure only one instance is used throughout the application.
*/
export class LocalizationService {
private static instance: LocalizationService
private readonly languages: { [key: string]: Language }
/**
* Initializes a new instance of the LocalizationService class.
* The constructor is private to prevent instantiation from outside the class.
*/
private constructor() {
this.languages = {
[LanguageCode.EN]: {
shortName: LanguageCode.EN,
longName: LanguageName.ENGLISH,
},
[LanguageCode.DE]: {
shortName: LanguageCode.DE,
longName: LanguageName.DEUTSCH,
},
[LanguageCode.FR]: {
shortName: LanguageCode.FR,
longName: LanguageName.FRANCAIS,
},
}
}
/**
* Retrieves the single instance of the LocalizationService class.
* If an instance does not exist, it creates a new one.
* @returns {LocalizationService} The single instance of the LocalizationService class.
*/
public static getInstance(): LocalizationService {
if (!LocalizationService.instance) {
LocalizationService.instance = new LocalizationService()
}
return LocalizationService.instance
}
/**
* Retrieves an array of all available languages.
* @returns {Language[]} An array containing all available languages.
*/
public getAllLanguages(): Language[] {
return Object.values(this.languages)
}
/**
* Retrieves a language by its short name.
* @param {LanguageCode} shortName - The short name of the language to retrieve.
* @returns {Language | undefined} The language with the specified short name, or undefined if not found.
*/
public getLanguageByShortName(shortName: LanguageCode): Language | undefined {
return this.languages[shortName]
}
/**
* Retrieves an array of all languages except the one specified by the key.
* @param {LanguageCode} key - The key of the language to exclude from the result.
* @returns {Language[]} An array containing all languages except the one specified by the key.
*/
public getLanguagesExcept(key: LanguageCode): Language[] {
return this.getAllLanguages().filter((language) => language.shortName !== key)
}
}
🏁 Result
I have a good feeling about how this pattern fits into the React application development ecosystem. We already saw its success of it through popular libraries such as Redux. Implementing this pattern in a production application brought us development benefits in terms of consistency and development speed.
If you have any questions regarding this or other patterns, let's discuss them in the comments!
🧐 Do you have any other examples of design pattern implementation in your React codebase? Share it!
Top comments (1)