Front-end Engineering Lab
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:

  1. Analytics services - Single instance to track events across the entire application
  2. Global configurations - API URLs, feature flags, environment (dev/prod)
  3. API clients - HTTP client, Firebase, Supabase - single connection
  4. Loggers - Centralized logging system
  5. 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

On this page