PatternsArchitecture Patterns
Singleton Pattern
Ensures a single instance of something - commonly used for Analytics, Firebase instances, or global configurations
The Singleton Pattern ensures that a class has only one instance and provides a global point of access to it. In React, we commonly use it for Analytics, Firebase instances, or global configurations that cannot be duplicated.
🎯 The Problem
// ❌ BAD: Multiple instances
class Analytics {
track(event: string) {
console.log('Track:', event);
}
}
const analytics1 = new Analytics();
const analytics2 = new Analytics();
// Two different instances! 😱
// ✅ GOOD: Singleton
class Analytics {
private static instance: Analytics;
private constructor() {}
static getInstance(): Analytics {
if (!Analytics.instance) {
Analytics.instance = new Analytics();
}
return Analytics.instance;
}
track(event: string) {
console.log('Track:', event);
}
}
const analytics = Analytics.getInstance();
// Always the same instance! ✨📚 Common Front-End Examples
1. Analytics Singleton
class AnalyticsService {
private static instance: AnalyticsService;
private initialized = false;
private constructor() {}
static getInstance(): AnalyticsService {
if (!AnalyticsService.instance) {
AnalyticsService.instance = new AnalyticsService();
}
return AnalyticsService.instance;
}
init(config: { apiKey: string; userId?: string }) {
if (this.initialized) return;
// Initialize Google Analytics, Mixpanel, etc.
this.initialized = true;
}
track(event: string, properties?: Record<string, unknown>) {
if (!this.initialized) {
console.warn('Analytics not initialized');
return;
}
// Send event
console.log('Track:', event, properties);
}
identify(userId: string) {
// Identify user
console.log('Identify:', userId);
}
}
// Usage - always the same instance
const analytics = AnalyticsService.getInstance();
analytics.init({ apiKey: 'xxx' });
analytics.track('page_view');
// Anywhere in the app
const sameAnalytics = AnalyticsService.getInstance();
sameAnalytics.track('button_click');2. Firebase Singleton
import { initializeApp, FirebaseApp } from 'firebase/app';
import { getAuth, Auth } from 'firebase/auth';
import { getFirestore, Firestore } from 'firebase/firestore';
class FirebaseService {
private static instance: FirebaseService;
private app: FirebaseApp | null = null;
private auth: Auth | null = null;
private db: Firestore | null = null;
private constructor() {}
static getInstance(): FirebaseService {
if (!FirebaseService.instance) {
FirebaseService.instance = new FirebaseService();
}
return FirebaseService.instance;
}
initialize(config: { apiKey: string; authDomain: string; projectId: string }) {
if (this.app) return;
this.app = initializeApp(config);
this.auth = getAuth(this.app);
this.db = getFirestore(this.app);
}
getAuth() {
if (!this.auth) {
throw new Error('Firebase not initialized');
}
return this.auth;
}
getFirestore() {
if (!this.db) {
throw new Error('Firebase not initialized');
}
return this.db;
}
}
// Usage
const firebase = FirebaseService.getInstance();
firebase.initialize({
apiKey: 'xxx',
authDomain: 'xxx',
projectId: 'xxx'
});
// Anywhere
const auth = firebase.getAuth();
const db = firebase.getFirestore();3. Global Configuration Singleton
interface AppConfig {
apiUrl: string;
environment: 'development' | 'production';
features: {
darkMode: boolean;
analytics: boolean;
};
}
class ConfigService {
private static instance: ConfigService;
private config: AppConfig | null = null;
private constructor() {}
static getInstance(): ConfigService {
if (!ConfigService.instance) {
ConfigService.instance = new ConfigService();
}
return ConfigService.instance;
}
initialize(config: AppConfig) {
if (this.config) {
console.warn('Config already initialized');
return;
}
this.config = config;
}
get(): AppConfig {
if (!this.config) {
throw new Error('Config not initialized');
}
return this.config;
}
getApiUrl(): string {
return this.get().apiUrl;
}
isFeatureEnabled(feature: keyof AppConfig['features']): boolean {
return this.get().features[feature];
}
}
// Usage
const config = ConfigService.getInstance();
config.initialize({
apiUrl: 'https://api.example.com',
environment: 'production',
features: {
darkMode: true,
analytics: true
}
});
// Anywhere
const apiUrl = config.getApiUrl();
const hasDarkMode = config.isFeatureEnabled('darkMode');4. Logger Singleton
type LogLevel = 'debug' | 'info' | 'warn' | 'error';
class Logger {
private static instance: Logger;
private level: LogLevel = 'info';
private logs: Array<{ level: LogLevel; message: string; timestamp: Date }> = [];
private constructor() {}
static getInstance(): Logger {
if (!Logger.instance) {
Logger.instance = new Logger();
}
return Logger.instance;
}
setLevel(level: LogLevel) {
this.level = level;
}
private shouldLog(level: LogLevel): boolean {
const levels: LogLevel[] = ['debug', 'info', 'warn', 'error'];
return levels.indexOf(level) >= levels.indexOf(this.level);
}
debug(message: string) {
if (this.shouldLog('debug')) {
this.logs.push({ level: 'debug', message, timestamp: new Date() });
console.debug(message);
}
}
info(message: string) {
if (this.shouldLog('info')) {
this.logs.push({ level: 'info', message, timestamp: new Date() });
console.info(message);
}
}
warn(message: string) {
if (this.shouldLog('warn')) {
this.logs.push({ level: 'warn', message, timestamp: new Date() });
console.warn(message);
}
}
error(message: string, error?: Error) {
if (this.shouldLog('error')) {
this.logs.push({ level: 'error', message, timestamp: new Date() });
console.error(message, error);
}
}
getLogs() {
return [...this.logs];
}
}
// Usage
const logger = Logger.getInstance();
logger.setLevel('debug');
logger.info('App started');
logger.error('Something went wrong', new Error('Test'));
// Anywhere
const sameLogger = Logger.getInstance();
sameLogger.debug('Debug message');5. Global Cache Singleton
class GlobalCache {
private static instance: GlobalCache;
private cache = new Map<string, { data: unknown; expires: number }>();
private constructor() {}
static getInstance(): GlobalCache {
if (!GlobalCache.instance) {
GlobalCache.instance = new GlobalCache();
}
return GlobalCache.instance;
}
set<T>(key: string, value: T, ttl: number = 60000) {
this.cache.set(key, {
data: value,
expires: Date.now() + ttl
});
}
get<T>(key: string): T | null {
const item = this.cache.get(key);
if (!item) return null;
if (Date.now() > item.expires) {
this.cache.delete(key);
return null;
}
return item.data as T;
}
clear() {
this.cache.clear();
}
has(key: string): boolean {
return this.cache.has(key) && Date.now() <= (this.cache.get(key)?.expires || 0);
}
}
// Usage
const cache = GlobalCache.getInstance();
cache.set<{ id: number; name: string }>('user', { id: 1, name: 'John' }, 300000); // 5 minutes
// Anywhere
const user = cache.get<{ id: number; name: string }>('user');
if (!user) {
// Fetch from server
console.log('User not in cache');
} else {
console.log('User from cache:', user.name);
}🎯 When to Use
This pattern is commonly used and recommended for:
- Analytics services - Single instance to track events across the entire application
- Global configurations - API URLs, feature flags, environment (dev/prod)
- API clients - HTTP client, Firebase, Supabase - single connection
- Loggers - Centralized logging system
- Global cache - Cache shared between components without prop drilling
⚠️ Considerations
- Testability - Can make testing harder (mocks)
- Global state - Can hide dependencies
- React Context - Consider using Context API for global state in React
- Memory leaks - Singleton persists for the entire application lifetime
📚 Key Takeaways
- Single instance - Ensures no duplication
- Global access - Available anywhere in the code
- Lazy initialization - Created only when needed
- Resource control - Useful for shared resources (DB, cache)
- Use in moderation - Prefer Context API in React for global state