The singleton pattern is a software design pattern that restricts the instantiation of a class to one "single" instance. This is useful when exactly one object is needed to coordinate actions across the system. The singleton pattern is often used in situations where system-wide actions need to be coordinated from a single central place. Examples include a database connection pool or a logger that needs to be accessible by different parts of an application without necessarily creating new instances.
TOC
- Implementation in TypeScript
- Eager Instantiation
- Avoid Using the Singleton Pattern in React
- But There Is a Good Use Case...
- What Are the Actual Advantages?
Implementation in TypeScript
Here’s how you could implement a simple singleton pattern in TypeScript:
class Singleton {
// Hold a reference to the single created instance
// of the Singleton, initially set to null.
private static instance: Singleton | null = null;
// Make the constructor private to block instantiation
// outside of the class.
private constructor() {
// initialization code
}
// Provide a static method that allows access
// to the single instance.
public static getInstance(): Singleton {
// Check if an instance already exists.
// If not, create one.
if (this.instance === null) {
this.instance = new Singleton();
}
// Return the instance.
return this.instance;
}
// Example method to show functionality.
public someMethod() {
return "Doing something...";
}
}
Eager Instantiation
In the example above, the singleton instance is created the first time it is needed (lazily instantiated). However, sometimes you need to immediately initialize instance. For example, when initialization is asynchronous and takes a certain time. This can be done by instantiating the Singleton instance directly in the declaration.
class Singleton {
private static instance: Singleton = new Singleton();
private constructor() {}
public static getInstance(): Singleton {
return this.instance;
}
}
Avoid Using the Singleton Pattern in React
Almost always you need to avoid using the singleton pattern in React and other frameworks from several key considerations:
Global State Management Concerns: Singletons represent a way to create a global state since they ensure there's only one instance of an object throughout the application. However, managing global state through Singletons can lead to difficulties in tracking changes and debugging, as any part of the application can modify the state at any point. This unpredictability contrasts with the unidirectional data flow and component-based state management that React encourages.
Mutation and Side Effects: The mutable nature of singleton instances introduces the risk of accidental state modifications, leading to unreliable state management. React favors immutability and pure functions, as seen with Redux's reducer functions, which ensure predictability and facilitate debugging by avoiding side effects.
Design and Scalability: Utilizing singletons for state management can encourage a monolithic design that hampers modularity and scalability. React favors a more decentralized approach to state management, allowing individual components to manage their own state or utilizing context and state management libraries to handle shared state in a controlled manner. This approach supports more scalable and reusable code.
Global Scope Pollution: The singleton pattern contributes to the broader issue of global scope pollution, where global variables or instances can conflict with each other or be inadvertently modified.
But There Is a Good Use Case...
One practical application of the singleton pattern within React might be through the implementation of a singleton context. This strategy ensures that once the context is initiated, redundant instantiation within nested components is avoided. Such an approach is especially beneficial for overarching functionalities like setting a universal theme (but not only for theme, write your suggestions and thoughts it in the comments if anyone reads this post). Simplified example:
import React, {
createContext,
useContext,
ReactNode,
Dispatch,
SetStateAction
} from 'react';
interface ThemeColors {
primary: string;
secondary: string;
background: string;
text: string;
}
interface Theme {
colors: ThemeColors;
provided: boolean;
}
const DEFAULT_THEME: Theme = {
colors: {
primary: '#007bff',
secondary: '#6c757d',
background: '#ffffff',
text: '#212529',
},
provided: false,
};
type ThemeContextType = [Theme, Dispatch<SetStateAction<Theme>>];
// When using React's Context, you'll also need to provide
// the type for the updater function if you plan to include it.
const ThemeContext = createContext<ThemeContextType>([
DEFAULT_THEME,
() => null
]);
interface ThemeSingletonProviderProps {
children: ReactNode;
}
export function ThemeSingletonProvider (
{ children }: ThemeSingletonProviderProps
) {
const [theme, setTheme] = useContext(ThemeContext);
if (!theme.provided) {
return (
<ThemeContext.Provider
value={[{ ...theme, provided: true }, setTheme]}
>
{children}
</ThemeContext.Provider>
);
}
return <>{children}</>;
};
What Are the Actual Advantages?
Modular or Micro-Frontend Architecture: In large-scale, modular applications, or those adopting a micro-frontend architecture, different parts of the application may be developed, built, and deployed independently. In such a setup
ThemeSingletonProvider
ensures that all parts of the application, regardless of how they are loaded or what lifecycle stage they are in, use the same theme configuration.Handling External Theme Resources: When the theme involves more than just CSS variables, such as loading external resources (fonts, icons specific to the theme),
ThemeSingletonProvider
can include logic to ensure that theme-related resources are loaded only once and efficiently manage changes.
Top comments (3)
Database initialization is another use case.
Example: If you are using Firestore, you may want to initialize database object only once and use the same object to query
Could you provide a example showing the implementation of this example?
Interesting take