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:
- Lightning Speed ⚡: Changes appear in milliseconds, not seconds
- State Preservation 💾: Keep your app’s current state intact
- Better Debugging 🐛: Test changes without recreating scenarios
- 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
- 🧹 Always Clean Up: Remove event listeners and clear timers in dispose callbacks
- 💾 State Serialization: Ensure your state can be JSON serialized for HMR
- 🔄 Graceful Fallbacks: Handle cases where HMR isn’t available
- 🎯 Minimal State: Only preserve essential state, not derived data
- ✨ 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:
- 💻 Practice with the chat app exercise above
- 🏗️ Add HMR to your existing TypeScript projects
- 📚 Explore framework-specific HMR solutions (React Fast Refresh, Vue HMR)
- 🌟 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! 🎉⚡✨