+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
Part 248 of 354

🚀 Hot Module Replacement: Fast Development

Master hot module replacement: fast development in TypeScript with practical examples, best practices, and real-world applications 🚀

💎Advanced
25 min read

Prerequisites

  • Basic understanding of JavaScript 📝
  • TypeScript installation ⚡
  • VS Code or preferred IDE 💻

What you'll learn

  • Understand HMR fundamentals 🎯
  • Apply HMR in real projects 🏗️
  • Debug common HMR issues 🐛
  • Write type-safe HMR code ✨

🎯 Introduction

Welcome to the exciting world of Hot Module Replacement (HMR)! 🎉 In this guide, we’ll explore how HMR can supercharge your TypeScript development experience by updating code instantly without losing application state.

You’ll discover how HMR can transform your development workflow from slow refresh cycles to lightning-fast ⚡ updates. Whether you’re building React applications 🌐, Node.js servers 🖥️, or complex web apps 📱, understanding HMR is essential for modern TypeScript development.

By the end of this tutorial, you’ll feel confident implementing HMR in your own projects and wonder how you ever developed without it! Let’s dive in! 🏊‍♂️

📚 Understanding Hot Module Replacement

🤔 What is Hot Module Replacement?

Hot Module Replacement is like having a magical auto-refresh 🪄 for your code. Think of it as swapping out parts of a running car engine while it’s still driving - you update specific pieces without stopping the whole application!

In TypeScript terms, HMR allows you to update modules in a running application without losing state 🎨. This means you can:

  • ⚡ See changes instantly (no full page reload)
  • 🚀 Maintain application state during updates
  • 🛡️ Preserve form data and user interactions
  • 💡 Debug more efficiently with live updates

💡 Why Use HMR?

Here’s why developers absolutely love HMR:

  1. Lightning Speed ⚡: Changes appear in milliseconds, not seconds
  2. State Preservation 💾: Keep your app’s current state intact
  3. Better Debugging 🐛: Test changes without recreating scenarios
  4. Improved Focus 🎯: Less context switching between browser and editor

Real-world example: Imagine building a multi-step form 📝. With HMR, you can style step 3 without having to re-fill steps 1 and 2 every time!

🔧 Basic Syntax and Usage

📝 Simple HMR Setup

Let’s start with a friendly example using Webpack and TypeScript:

// 👋 Hello, HMR-enabled TypeScript!
declare const module: {
  hot?: {
    accept(path?: string, callback?: () => void): void;
    dispose(callback: (data: any) => void): void;
  }
};

// 🎨 A simple counter component
class Counter {
  private count: number = 0;
  private element: HTMLElement;

  constructor(elementId: string) {
    this.element = document.getElementById(elementId)!;
    this.render();
  }

  // ➕ Increment counter
  increment(): void {
    this.count++;
    this.render();
    console.log(`🎯 Count is now: ${this.count}`);
  }

  // 🎨 Render the counter
  private render(): void {
    this.element.innerHTML = `
      <div>
        <h2>🔢 Counter: ${this.count}</h2>
        <button onclick="counter.increment()">➕ Click me!</button>
      </div>
    `;
  }

  // 💾 Get current state for HMR
  getState(): { count: number } {
    return { count: this.count };
  }

  // 🔄 Restore state after HMR
  setState(state: { count: number }): void {
    this.count = state.count;
    this.render();
  }
}

// 🚀 Create counter instance
const counter = new Counter('app');

// ✨ HMR magic happens here!
if (module.hot) {
  // 💾 Store state before module replacement
  let state: { count: number } | undefined;
  
  module.hot.dispose((data) => {
    state = counter.getState();
    console.log('💾 Storing state:', state);
  });

  // 🔄 Accept updates and restore state
  module.hot.accept(() => {
    if (state) {
      counter.setState(state);
      console.log('🔄 State restored!');
    }
  });
}

💡 Explanation: The module.hot API lets us handle module updates gracefully, preserving our counter state!

🎯 Webpack Configuration

Here’s how to configure Webpack for TypeScript HMR:

// 🔧 webpack.config.js
const path = require('path');

module.exports = {
  mode: 'development',
  entry: './src/index.ts',
  
  // 🎯 Essential HMR configuration
  devServer: {
    hot: true,                    // ✨ Enable HMR
    liveReload: false,           // 🚫 Disable full reload
    static: './dist',            // 📁 Static files location
  },

  // 📝 TypeScript handling
  module: {
    rules: [
      {
        test: /\.tsx?$/,
        use: 'ts-loader',
        exclude: /node_modules/,
      },
    ],
  },

  // 🔗 File resolution
  resolve: {
    extensions: ['.tsx', '.ts', '.js'],
  },

  output: {
    filename: 'bundle.js',
    path: path.resolve(__dirname, 'dist'),
  },
};

💡 Practical Examples

🎮 Example 1: Gaming Dashboard with HMR

Let’s build a gaming dashboard that maintains state during development:

// 🏆 Game statistics interface
interface GameStats {
  player: string;
  score: number;
  level: number;
  powerUps: string[];
  emoji: string;
}

// 🎮 Gaming dashboard with HMR support
class GamingDashboard {
  private stats: GameStats;
  private container: HTMLElement;

  constructor(elementId: string) {
    this.container = document.getElementById(elementId)!;
    this.stats = {
      player: "TypeScript Hero",
      score: 0,
      level: 1,
      powerUps: [],
      emoji: "🎮"
    };
    this.render();
    this.setupEventListeners();
  }

  // ⚡ Add score (simulate gameplay)
  addScore(points: number): void {
    this.stats.score += points;
    
    // 🆙 Level up every 1000 points
    const newLevel = Math.floor(this.stats.score / 1000) + 1;
    if (newLevel > this.stats.level) {
      this.levelUp(newLevel);
    }
    
    this.render();
    console.log(`✨ Score: ${this.stats.score}, Level: ${this.stats.level}`);
  }

  // 📈 Level up with power-ups
  private levelUp(newLevel: number): void {
    this.stats.level = newLevel;
    const powerUp = this.generatePowerUp();
    this.stats.powerUps.push(powerUp);
    
    // 🎊 Celebration effect
    this.showLevelUpEffect();
  }

  // 🎁 Generate random power-up
  private generatePowerUp(): string {
    const powerUps = ["🚀 Speed Boost", "💪 Strength", "🛡️ Shield", "✨ Magic", "⚡ Lightning"];
    return powerUps[Math.floor(Math.random() * powerUps.length)];
  }

  // 🎉 Show level up effect
  private showLevelUpEffect(): void {
    const effect = document.createElement('div');
    effect.innerHTML = '🎉 LEVEL UP! 🎉';
    effect.style.cssText = `
      position: fixed;
      top: 50%;
      left: 50%;
      transform: translate(-50%, -50%);
      font-size: 2em;
      color: gold;
      animation: fadeInOut 2s ease-in-out;
      pointer-events: none;
    `;
    document.body.appendChild(effect);
    setTimeout(() => effect.remove(), 2000);
  }

  // 🎨 Render the dashboard
  private render(): void {
    this.container.innerHTML = `
      <div class="gaming-dashboard">
        <h1>${this.stats.emoji} ${this.stats.player}</h1>
        <div class="stats">
          <div class="stat">🎯 Score: ${this.stats.score.toLocaleString()}</div>
          <div class="stat">📊 Level: ${this.stats.level}</div>
          <div class="stat">⚡ Power-ups: ${this.stats.powerUps.length}</div>
        </div>
        <div class="power-ups">
          <h3>🎁 Collected Power-ups:</h3>
          <ul>
            ${this.stats.powerUps.map(powerUp => `<li>${powerUp}</li>`).join('')}
          </ul>
        </div>
        <div class="controls">
          <button class="score-btn" data-points="100">+100 🎯</button>
          <button class="score-btn" data-points="250">+250 ⚡</button>
          <button class="score-btn" data-points="500">+500 🚀</button>
        </div>
      </div>
    `;
  }

  // 🎯 Setup event listeners
  private setupEventListeners(): void {
    this.container.addEventListener('click', (e) => {
      const target = e.target as HTMLElement;
      if (target.classList.contains('score-btn')) {
        const points = parseInt(target.dataset.points || '0');
        this.addScore(points);
      }
    });
  }

  // 💾 HMR: Get current state
  getState(): GameStats {
    return { ...this.stats };
  }

  // 🔄 HMR: Restore state
  setState(state: GameStats): void {
    this.stats = { ...state };
    this.render();
    this.setupEventListeners();
  }
}

// 🚀 Initialize the dashboard
const dashboard = new GamingDashboard('app');

// ✨ HMR implementation
if (module.hot) {
  let savedState: GameStats | undefined;

  module.hot.dispose((data) => {
    savedState = dashboard.getState();
    console.log('💾 Saving game state:', savedState);
  });

  module.hot.accept(() => {
    if (savedState) {
      dashboard.setState(savedState);
      console.log('🔄 Game state restored! Keep playing! 🎮');
    }
  });
}

🎯 Try it yourself: Add a reset button and see how HMR preserves your progress while you code!

🛒 Example 2: Shopping Cart with Real-time Updates

Let’s create a shopping cart that maintains items during development:

// 🛍️ Product interface
interface Product {
  id: string;
  name: string;
  price: number;
  emoji: string;
  category: 'electronics' | 'clothing' | 'food' | 'books';
}

// 🛒 Cart item with quantity
interface CartItem extends Product {
  quantity: number;
}

// 🛒 Shopping cart with HMR support
class ShoppingCart {
  private items: Map<string, CartItem> = new Map();
  private container: HTMLElement;

  // 📦 Sample products
  private readonly products: Product[] = [
    { id: '1', name: 'TypeScript Book', price: 29.99, emoji: '📘', category: 'books' },
    { id: '2', name: 'Coffee Mug', price: 12.99, emoji: '☕', category: 'electronics' },
    { id: '3', name: 'Gaming Mouse', price: 59.99, emoji: '🖱️', category: 'electronics' },
    { id: '4', name: 'T-Shirt', price: 19.99, emoji: '👕', category: 'clothing' },
    { id: '5', name: 'Pizza', price: 15.99, emoji: '🍕', category: 'food' },
  ];

  constructor(elementId: string) {
    this.container = document.getElementById(elementId)!;
    this.render();
    this.setupEventListeners();
  }

  // ➕ Add item to cart
  addToCart(productId: string): void {
    const product = this.products.find(p => p.id === productId);
    if (!product) return;

    const existingItem = this.items.get(productId);
    if (existingItem) {
      existingItem.quantity++;
    } else {
      this.items.set(productId, { ...product, quantity: 1 });
    }

    this.render();
    this.showAddedToCartEffect(product);
    console.log(`➕ Added ${product.emoji} ${product.name} to cart!`);
  }

  // ➖ Remove item from cart
  removeFromCart(productId: string): void {
    const item = this.items.get(productId);
    if (!item) return;

    if (item.quantity > 1) {
      item.quantity--;
    } else {
      this.items.delete(productId);
    }

    this.render();
    console.log(`➖ Removed ${item.emoji} ${item.name} from cart`);
  }

  // 💰 Calculate total
  private getTotal(): number {
    let total = 0;
    this.items.forEach(item => {
      total += item.price * item.quantity;
    });
    return total;
  }

  // ✨ Show "added to cart" effect
  private showAddedToCartEffect(product: Product): void {
    const effect = document.createElement('div');
    effect.innerHTML = `${product.emoji} Added to cart!`;
    effect.style.cssText = `
      position: fixed;
      top: 20px;
      right: 20px;
      background: #4CAF50;
      color: white;
      padding: 10px 20px;
      border-radius: 5px;
      animation: slideInFade 3s ease-in-out;
    `;
    document.body.appendChild(effect);
    setTimeout(() => effect.remove(), 3000);
  }

  // 🎨 Render the cart
  private render(): void {
    const cartItemsHtml = Array.from(this.items.values()).map(item => `
      <div class="cart-item">
        <span class="item-info">
          ${item.emoji} ${item.name} 
          <small>($${item.price})</small>
        </span>
        <div class="quantity-controls">
          <button class="remove-btn" data-id="${item.id}">➖</button>
          <span class="quantity">${item.quantity}</span>
          <button class="add-btn" data-id="${item.id}">➕</button>
        </div>
        <span class="item-total">$${(item.price * item.quantity).toFixed(2)}</span>
      </div>
    `).join('');

    const productsHtml = this.products.map(product => `
      <div class="product-card">
        <div class="product-emoji">${product.emoji}</div>
        <div class="product-name">${product.name}</div>
        <div class="product-price">$${product.price}</div>
        <button class="add-to-cart-btn" data-id="${product.id}">
          ➕ Add to Cart
        </button>
      </div>
    `).join('');

    this.container.innerHTML = `
      <div class="shopping-app">
        <div class="products-section">
          <h2>🛍️ Available Products</h2>
          <div class="products-grid">
            ${productsHtml}
          </div>
        </div>
        
        <div class="cart-section">
          <h2>🛒 Shopping Cart (${this.items.size} items)</h2>
          ${cartItemsHtml || '<p>🛒 Your cart is empty!</p>'}
          
          ${this.items.size > 0 ? `
            <div class="cart-total">
              <h3>💰 Total: $${this.getTotal().toFixed(2)}</h3>
              <button class="checkout-btn">🎯 Checkout</button>
            </div>
          ` : ''}
        </div>
      </div>
    `;
  }

  // 🎯 Setup event listeners
  private setupEventListeners(): void {
    this.container.addEventListener('click', (e) => {
      const target = e.target as HTMLElement;
      
      if (target.classList.contains('add-to-cart-btn')) {
        const productId = target.dataset.id!;
        this.addToCart(productId);
      }
      
      if (target.classList.contains('add-btn')) {
        const productId = target.dataset.id!;
        this.addToCart(productId);
      }
      
      if (target.classList.contains('remove-btn')) {
        const productId = target.dataset.id!;
        this.removeFromCart(productId);
      }

      if (target.classList.contains('checkout-btn')) {
        alert(`🎉 Checkout successful! Total: $${this.getTotal().toFixed(2)}`);
      }
    });
  }

  // 💾 HMR: Get current state
  getState(): { items: [string, CartItem][] } {
    return {
      items: Array.from(this.items.entries())
    };
  }

  // 🔄 HMR: Restore state
  setState(state: { items: [string, CartItem][] }): void {
    this.items = new Map(state.items);
    this.render();
    this.setupEventListeners();
  }
}

// 🚀 Initialize the shopping cart
const cart = new ShoppingCart('app');

// ✨ HMR magic for shopping cart
if (module.hot) {
  let cartState: { items: [string, CartItem][] } | undefined;

  module.hot.dispose((data) => {
    cartState = cart.getState();
    console.log('💾 Saving cart state:', cartState);
  });

  module.hot.accept(() => {
    if (cartState) {
      cart.setState(cartState);
      console.log('🔄 Shopping cart restored! Keep shopping! 🛒');
    }
  });
}

🚀 Advanced Concepts

🧙‍♂️ Advanced HMR: Module Dependencies

When you’re ready to level up, handle complex module dependencies:

// 🎯 Advanced HMR with module dependency tracking
interface HMRState {
  [key: string]: any;
}

class HMRManager {
  private static instance: HMRManager;
  private stateRegistry: Map<string, HMRState> = new Map();
  private dependencyGraph: Map<string, Set<string>> = new Map();

  static getInstance(): HMRManager {
    if (!HMRManager.instance) {
      HMRManager.instance = new HMRManager();
    }
    return HMRManager.instance;
  }

  // 📝 Register a module for HMR
  registerModule(
    moduleId: string, 
    getState: () => HMRState,
    setState: (state: HMRState) => void,
    dependencies: string[] = []
  ): void {
    // 🔗 Track dependencies
    this.dependencyGraph.set(moduleId, new Set(dependencies));

    if (module.hot) {
      module.hot.dispose((data) => {
        const state = getState();
        this.stateRegistry.set(moduleId, state);
        console.log(`💾 Saved state for ${moduleId}:`, state);
      });

      module.hot.accept(() => {
        const state = this.stateRegistry.get(moduleId);
        if (state) {
          setState(state);
          console.log(`🔄 Restored state for ${moduleId}`);
          
          // 🔄 Notify dependent modules
          this.notifyDependents(moduleId);
        }
      });
    }
  }

  // 📢 Notify dependent modules of changes
  private notifyDependents(moduleId: string): void {
    this.dependencyGraph.forEach((deps, dependentId) => {
      if (deps.has(moduleId)) {
        console.log(`🔗 Module ${dependentId} depends on ${moduleId}, updating...`);
        // Trigger dependent module updates here
      }
    });
  }

  // 🧹 Clean up state
  clearState(moduleId: string): void {
    this.stateRegistry.delete(moduleId);
    this.dependencyGraph.delete(moduleId);
  }
}

🏗️ Advanced HMR: CSS-in-JS Integration

For the brave developers working with styled components:

// 🎨 HMR with CSS-in-JS and styled components
interface ThemeState {
  primaryColor: string;
  secondaryColor: string;
  darkMode: boolean;
  fontSize: number;
}

class ThemeManager {
  private theme: ThemeState = {
    primaryColor: '#007acc',
    secondaryColor: '#ff6b6b',
    darkMode: false,
    fontSize: 16
  };

  private styleSheet: CSSStyleSheet;

  constructor() {
    this.styleSheet = this.createStyleSheet();
    this.applyTheme();
  }

  // 🎨 Create dynamic stylesheet
  private createStyleSheet(): CSSStyleSheet {
    const style = document.createElement('style');
    document.head.appendChild(style);
    return style.sheet as CSSStyleSheet;
  }

  // ✨ Apply theme with CSS variables
  private applyTheme(): void {
    const cssVars = `
      :root {
        --primary-color: ${this.theme.primaryColor};
        --secondary-color: ${this.theme.secondaryColor};
        --background: ${this.theme.darkMode ? '#1a1a1a' : '#ffffff'};
        --text: ${this.theme.darkMode ? '#ffffff' : '#333333'};
        --font-size: ${this.theme.fontSize}px;
      }
      
      body {
        background: var(--background);
        color: var(--text);
        font-size: var(--font-size);
        transition: all 0.3s ease;
      }
      
      .theme-demo {
        padding: 20px;
        border: 2px solid var(--primary-color);
        background: linear-gradient(45deg, var(--primary-color), var(--secondary-color));
        color: white;
        border-radius: 10px;
        margin: 10px 0;
      }
    `;

    // 🔄 Update stylesheet
    if (this.styleSheet.cssRules.length > 0) {
      this.styleSheet.deleteRule(0);
    }
    this.styleSheet.insertRule(cssVars, 0);
  }

  // 🎨 Update theme
  updateTheme(newTheme: Partial<ThemeState>): void {
    this.theme = { ...this.theme, ...newTheme };
    this.applyTheme();
    console.log('🎨 Theme updated:', this.theme);
  }

  // 💾 Get state for HMR
  getState(): ThemeState {
    return { ...this.theme };
  }

  // 🔄 Set state for HMR
  setState(state: ThemeState): void {
    this.theme = state;
    this.applyTheme();
  }
}

// 🚀 Initialize theme manager with HMR
const themeManager = new ThemeManager();

// ✨ Register with HMR manager
const hmrManager = HMRManager.getInstance();
hmrManager.registerModule(
  'theme-manager',
  () => themeManager.getState(),
  (state) => themeManager.setState(state)
);

⚠️ Common Pitfalls and Solutions

😱 Pitfall 1: Memory Leaks with Event Listeners

// ❌ Wrong way - event listeners pile up!
class BadComponent {
  constructor() {
    // 💥 Adding listeners without cleanup!
    document.addEventListener('click', this.handleClick);
  }
  
  private handleClick = () => {
    console.log('Clicked!');
  }
}

// ✅ Correct way - clean up properly!
class GoodComponent {
  private boundHandleClick = this.handleClick.bind(this);
  
  constructor() {
    document.addEventListener('click', this.boundHandleClick);
  }
  
  private handleClick(e: Event): void {
    console.log('Clicked!');
  }
  
  // 🧹 Clean up on HMR disposal
  dispose(): void {
    document.removeEventListener('click', this.boundHandleClick);
  }
}

// ✨ HMR cleanup
if (module.hot) {
  const component = new GoodComponent();
  
  module.hot.dispose((data) => {
    component.dispose(); // 🧹 Clean up!
  });
}

🤯 Pitfall 2: Forgetting to Handle Async Operations

// ❌ Dangerous - async operations continue after HMR!
class BadAsyncComponent {
  private timer: number | undefined;
  
  constructor() {
    this.timer = setInterval(() => {
      console.log('⏰ Timer tick!');
    }, 1000);
  }
}

// ✅ Safe - clean up async operations!
class GoodAsyncComponent {
  private timer: number | undefined;
  private abortController: AbortController = new AbortController();
  
  constructor() {
    this.timer = setInterval(() => {
      console.log('⏰ Timer tick!');
    }, 1000);
    
    // 🌐 Fetch with abort signal
    this.fetchData();
  }
  
  private async fetchData(): Promise<void> {
    try {
      const response = await fetch('/api/data', {
        signal: this.abortController.signal
      });
      console.log('📡 Data received!');
    } catch (error) {
      if (error.name !== 'AbortError') {
        console.error('❌ Fetch error:', error);
      }
    }
  }
  
  // 🧹 Proper cleanup
  dispose(): void {
    if (this.timer) {
      clearInterval(this.timer);
    }
    this.abortController.abort();
  }
}

🛠️ Best Practices

  1. 🧹 Always Clean Up: Remove event listeners and clear timers in dispose callbacks
  2. 💾 State Serialization: Ensure your state can be JSON serialized for HMR
  3. 🔄 Graceful Fallbacks: Handle cases where HMR isn’t available
  4. 🎯 Minimal State: Only preserve essential state, not derived data
  5. ✨ Test Without HMR: Ensure your app works with full page reloads too

🧪 Hands-On Exercise

🎯 Challenge: Build a Real-time Chat App with HMR

Create a type-safe chat application that preserves messages during development:

📋 Requirements:

  • ✅ Real-time message display with timestamps
  • 🏷️ User profiles with avatars (emojis)
  • 👤 Multiple users simulation
  • 📅 Message persistence through HMR
  • 🎨 Live theme switching
  • 🔔 Typing indicators

🚀 Bonus Points:

  • Add message reactions (emoji)
  • Implement message search
  • Create message threading
  • Add notification sounds

💡 Solution

🔍 Click to see solution
// 💬 Chat message interface
interface ChatMessage {
  id: string;
  userId: string;
  username: string;
  avatar: string;
  content: string;
  timestamp: Date;
  reactions: Map<string, string[]>; // emoji -> userIds
}

// 👤 User interface
interface ChatUser {
  id: string;
  username: string;
  avatar: string;
  isTyping: boolean;
  lastSeen: Date;
}

// 💬 Real-time chat with HMR support
class ChatApp {
  private messages: ChatMessage[] = [];
  private users: Map<string, ChatUser> = new Map();
  private currentUser: ChatUser;
  private container: HTMLElement;
  private messageInput: HTMLInputElement | null = null;

  constructor(elementId: string) {
    this.container = document.getElementById(elementId)!;
    
    // 👤 Create current user
    this.currentUser = {
      id: 'user-1',
      username: 'TypeScript Dev',
      avatar: '👨‍💻',
      isTyping: false,
      lastSeen: new Date()
    };
    
    this.initializeUsers();
    this.render();
    this.setupEventListeners();
    this.simulateActivity();
  }

  // 👥 Initialize sample users
  private initializeUsers(): void {
    const sampleUsers: ChatUser[] = [
      { id: 'user-2', username: 'React Expert', avatar: '⚛️', isTyping: false, lastSeen: new Date() },
      { id: 'user-3', username: 'Vue Ninja', avatar: '🥷', isTyping: false, lastSeen: new Date() },
      { id: 'user-4', username: 'Angular Pro', avatar: '🅰️', isTyping: false, lastSeen: new Date() },
    ];

    sampleUsers.forEach(user => this.users.set(user.id, user));
    this.users.set(this.currentUser.id, this.currentUser);
  }

  // ✉️ Send message
  private sendMessage(content: string): void {
    if (!content.trim()) return;

    const message: ChatMessage = {
      id: Date.now().toString(),
      userId: this.currentUser.id,
      username: this.currentUser.username,
      avatar: this.currentUser.avatar,
      content: content.trim(),
      timestamp: new Date(),
      reactions: new Map()
    };

    this.messages.push(message);
    this.render();
    this.scrollToBottom();
    
    // 🎵 Play notification sound
    this.playNotificationSound();
    
    console.log(`💬 ${this.currentUser.avatar} ${this.currentUser.username}: ${content}`);
  }

  // 😄 Add reaction to message
  private addReaction(messageId: string, emoji: string): void {
    const message = this.messages.find(m => m.id === messageId);
    if (!message) return;

    if (!message.reactions.has(emoji)) {
      message.reactions.set(emoji, []);
    }

    const reactors = message.reactions.get(emoji)!;
    if (!reactors.includes(this.currentUser.id)) {
      reactors.push(this.currentUser.id);
      this.render();
      console.log(`😄 ${this.currentUser.avatar} reacted with ${emoji}`);
    }
  }

  // 🎵 Play notification sound
  private playNotificationSound(): void {
    // 🔊 Create audio context for notification
    const audioContext = new (window.AudioContext || window.webkitAudioContext)();
    const oscillator = audioContext.createOscillator();
    const gainNode = audioContext.createGain();

    oscillator.connect(gainNode);
    gainNode.connect(audioContext.destination);

    oscillator.frequency.value = 800;
    oscillator.type = 'sine';
    gainNode.gain.setValueAtTime(0.1, audioContext.currentTime);
    gainNode.gain.exponentialRampToValueAtTime(0.01, audioContext.currentTime + 0.1);

    oscillator.start(audioContext.currentTime);
    oscillator.stop(audioContext.currentTime + 0.1);
  }

  // 🤖 Simulate other users' activity
  private simulateActivity(): void {
    setInterval(() => {
      if (Math.random() < 0.3) { // 30% chance
        this.simulateRandomMessage();
      }
    }, 5000);

    setInterval(() => {
      this.simulateTyping();
    }, 3000);
  }

  // 💬 Simulate random messages from other users
  private simulateRandomMessage(): void {
    const messages = [
      "Hey everyone! 👋",
      "Working on some TypeScript magic ✨",
      "HMR is incredible! 🚀",
      "Anyone else loving this tutorial? 📘",
      "Coffee break time! ☕",
      "This chat app is awesome! 💬",
      "TypeScript + HMR = ❤️"
    ];

    const userIds = Array.from(this.users.keys()).filter(id => id !== this.currentUser.id);
    const randomUser = this.users.get(userIds[Math.floor(Math.random() * userIds.length)])!;
    const randomMessage = messages[Math.floor(Math.random() * messages.length)];

    const message: ChatMessage = {
      id: Date.now().toString() + Math.random(),
      userId: randomUser.id,
      username: randomUser.username,
      avatar: randomUser.avatar,
      content: randomMessage,
      timestamp: new Date(),
      reactions: new Map()
    };

    this.messages.push(message);
    this.render();
    this.scrollToBottom();
  }

  // ⌨️ Simulate typing indicators
  private simulateTyping(): void {
    const userIds = Array.from(this.users.keys()).filter(id => id !== this.currentUser.id);
    const randomUser = this.users.get(userIds[Math.floor(Math.random() * userIds.length)])!;
    
    randomUser.isTyping = true;
    this.render();

    setTimeout(() => {
      randomUser.isTyping = false;
      this.render();
    }, 2000);
  }

  // 📜 Scroll to bottom of chat
  private scrollToBottom(): void {
    const chatMessages = this.container.querySelector('.chat-messages');
    if (chatMessages) {
      chatMessages.scrollTop = chatMessages.scrollHeight;
    }
  }

  // 🎨 Render the chat app
  private render(): void {
    const messagesHtml = this.messages.map(message => {
      const reactionsHtml = Array.from(message.reactions.entries())
        .map(([emoji, userIds]) => `
          <span class="reaction" data-message-id="${message.id}" data-emoji="${emoji}">
            ${emoji} ${userIds.length}
          </span>
        `).join('');

      return `
        <div class="message ${message.userId === this.currentUser.id ? 'own-message' : ''}">
          <div class="message-header">
            <span class="avatar">${message.avatar}</span>
            <span class="username">${message.username}</span>
            <span class="timestamp">${message.timestamp.toLocaleTimeString()}</span>
          </div>
          <div class="message-content">${message.content}</div>
          <div class="message-actions">
            <button class="react-btn" data-message-id="${message.id}" data-emoji="👍">👍</button>
            <button class="react-btn" data-message-id="${message.id}" data-emoji="❤️">❤️</button>
            <button class="react-btn" data-message-id="${message.id}" data-emoji="😂">😂</button>
            <button class="react-btn" data-message-id="${message.id}" data-emoji="🎉">🎉</button>
          </div>
          ${reactionsHtml ? `<div class="reactions">${reactionsHtml}</div>` : ''}
        </div>
      `;
    }).join('');

    const typingUsers = Array.from(this.users.values())
      .filter(user => user.isTyping && user.id !== this.currentUser.id);

    const typingHtml = typingUsers.length > 0 ? `
      <div class="typing-indicator">
        ${typingUsers.map(user => `${user.avatar} ${user.username}`).join(', ')} 
        ${typingUsers.length === 1 ? 'is' : 'are'} typing...
      </div>
    ` : '';

    this.container.innerHTML = `
      <div class="chat-app">
        <div class="chat-header">
          <h2>💬 TypeScript Chat (${this.users.size} users online)</h2>
          <div class="user-info">
            ${this.currentUser.avatar} ${this.currentUser.username}
          </div>
        </div>
        
        <div class="chat-messages">
          ${messagesHtml}
          ${typingHtml}
        </div>
        
        <div class="chat-input">
          <input 
            type="text" 
            placeholder="Type your message... 💬" 
            class="message-input"
            maxlength="500"
          />
          <button class="send-btn">📤 Send</button>
        </div>
        
        <div class="chat-stats">
          📊 Messages: ${this.messages.length} | 
          ⏰ Last update: ${new Date().toLocaleTimeString()}
        </div>
      </div>
    `;
  }

  // 🎯 Setup event listeners
  private setupEventListeners(): void {
    const messageInput = this.container.querySelector('.message-input') as HTMLInputElement;
    const sendBtn = this.container.querySelector('.send-btn') as HTMLButtonElement;

    // 📤 Send message on enter or button click
    messageInput?.addEventListener('keypress', (e) => {
      if (e.key === 'Enter') {
        this.sendMessage(messageInput.value);
        messageInput.value = '';
      }
    });

    sendBtn?.addEventListener('click', () => {
      this.sendMessage(messageInput.value);
      messageInput.value = '';
    });

    // 😄 Handle reactions
    this.container.addEventListener('click', (e) => {
      const target = e.target as HTMLElement;
      
      if (target.classList.contains('react-btn')) {
        const messageId = target.dataset.messageId!;
        const emoji = target.dataset.emoji!;
        this.addReaction(messageId, emoji);
      }
    });
  }

  // 💾 HMR: Get current state
  getState(): {
    messages: ChatMessage[];
    users: [string, ChatUser][];
    currentUser: ChatUser;
  } {
    return {
      messages: this.messages.map(msg => ({
        ...msg,
        reactions: Array.from(msg.reactions.entries())
      })) as any,
      users: Array.from(this.users.entries()),
      currentUser: this.currentUser
    };
  }

  // 🔄 HMR: Restore state
  setState(state: {
    messages: any[];
    users: [string, ChatUser][];
    currentUser: ChatUser;
  }): void {
    this.messages = state.messages.map(msg => ({
      ...msg,
      reactions: new Map(msg.reactions)
    }));
    this.users = new Map(state.users);
    this.currentUser = state.currentUser;
    this.render();
    this.setupEventListeners();
  }
}

// 🚀 Initialize the chat app
const chatApp = new ChatApp('app');

// ✨ HMR magic for chat app
if (module.hot) {
  let chatState: any;

  module.hot.dispose((data) => {
    chatState = chatApp.getState();
    console.log('💾 Saving chat state with', chatState.messages.length, 'messages');
  });

  module.hot.accept(() => {
    if (chatState) {
      chatApp.setState(chatState);
      console.log('🔄 Chat app restored! Keep chatting! 💬');
    }
  });
}

// 🎨 Add some CSS for better styling
const styles = `
  .chat-app {
    max-width: 800px;
    margin: 0 auto;
    border: 1px solid #ddd;
    border-radius: 10px;
    overflow: hidden;
    font-family: system-ui, sans-serif;
  }
  
  .chat-header {
    background: linear-gradient(45deg, #667eea 0%, #764ba2 100%);
    color: white;
    padding: 15px;
    display: flex;
    justify-content: space-between;
    align-items: center;
  }
  
  .chat-messages {
    height: 400px;
    overflow-y: auto;
    padding: 10px;
    background: #f8f9fa;
  }
  
  .message {
    margin-bottom: 15px;
    padding: 10px;
    background: white;
    border-radius: 8px;
    box-shadow: 0 1px 3px rgba(0,0,0,0.1);
  }
  
  .own-message {
    background: #e3f2fd;
    margin-left: 20%;
  }
  
  .message-header {
    display: flex;
    align-items: center;
    gap: 8px;
    margin-bottom: 5px;
    font-size: 0.9em;
    color: #666;
  }
  
  .avatar {
    font-size: 1.2em;
  }
  
  .username {
    font-weight: bold;
  }
  
  .timestamp {
    margin-left: auto;
    font-size: 0.8em;
  }
  
  .message-content {
    margin: 5px 0;
  }
  
  .message-actions {
    display: flex;
    gap: 5px;
    margin-top: 5px;
  }
  
  .react-btn {
    background: none;
    border: 1px solid #ddd;
    border-radius: 15px;
    padding: 2px 6px;
    cursor: pointer;
    font-size: 0.9em;
    transition: background 0.2s;
  }
  
  .react-btn:hover {
    background: #f0f0f0;
  }
  
  .reactions {
    display: flex;
    gap: 5px;
    margin-top: 5px;
  }
  
  .reaction {
    background: #e0e7ff;
    border: 1px solid #c7d2fe;
    border-radius: 12px;
    padding: 2px 8px;
    font-size: 0.8em;
    cursor: pointer;
  }
  
  .typing-indicator {
    font-style: italic;
    color: #666;
    padding: 10px;
    animation: pulse 1.5s infinite;
  }
  
  .chat-input {
    display: flex;
    padding: 15px;
    background: white;
    border-top: 1px solid #ddd;
  }
  
  .message-input {
    flex: 1;
    padding: 10px;
    border: 1px solid #ddd;
    border-radius: 20px;
    margin-right: 10px;
    outline: none;
  }
  
  .send-btn {
    background: #667eea;
    color: white;
    border: none;
    border-radius: 20px;
    padding: 10px 20px;
    cursor: pointer;
    transition: background 0.2s;
  }
  
  .send-btn:hover {
    background: #5a6fd8;
  }
  
  .chat-stats {
    background: #f8f9fa;
    padding: 10px;
    text-align: center;
    font-size: 0.9em;
    color: #666;
  }
  
  @keyframes pulse {
    0%, 50%, 100% { opacity: 1; }
    25%, 75% { opacity: 0.5; }
  }
`;

const styleSheet = document.createElement('style');
styleSheet.textContent = styles;
document.head.appendChild(styleSheet);

🎓 Key Takeaways

You’ve learned so much about Hot Module Replacement! Here’s what you can now do:

  • Implement HMR in TypeScript projects with confidence 💪
  • Preserve application state during development 🛡️
  • Handle complex scenarios like async operations and dependencies 🎯
  • Debug HMR issues like a pro 🐛
  • Build lightning-fast development workflows with TypeScript! 🚀

Remember: HMR is your development superpower, not just a convenience! It’s here to help you build better apps faster. 🤝

🤝 Next Steps

Congratulations! 🎉 You’ve mastered Hot Module Replacement in TypeScript!

Here’s what to do next:

  1. 💻 Practice with the chat app exercise above
  2. 🏗️ Add HMR to your existing TypeScript projects
  3. 📚 Explore framework-specific HMR solutions (React Fast Refresh, Vue HMR)
  4. 🌟 Share your lightning-fast development setup with others!

Remember: Every TypeScript expert was once waiting for slow page reloads. Keep coding, keep innovating, and most importantly, enjoy the speed! 🚀


Happy coding at light speed! 🎉⚡✨