+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
Part 163 of 355

๐Ÿ“˜ MobX with TypeScript: Observable State

Master mobx with typescript: observable state in TypeScript with practical examples, best practices, and real-world applications ๐Ÿš€

๐Ÿš€Intermediate
25 min read

Prerequisites

  • Basic understanding of JavaScript ๐Ÿ“
  • TypeScript installation โšก
  • VS Code or preferred IDE ๐Ÿ’ป

What you'll learn

  • Understand MobX observable state fundamentals ๐ŸŽฏ
  • Apply MobX observables in real projects ๐Ÿ—๏ธ
  • Debug common MobX issues ๐Ÿ›
  • Write type-safe MobX code โœจ

๐ŸŽฏ Introduction

Welcome to the exciting world of MobX with TypeScript! ๐ŸŽ‰ In this guide, weโ€™ll explore how to create reactive, observable state that makes your applications come alive with automatic updates.

Youโ€™ll discover how MobX can transform your TypeScript development experience by making state management simple, predictable, and incredibly powerful. Whether youโ€™re building web applications ๐ŸŒ, desktop apps ๐Ÿ–ฅ๏ธ, or complex data-driven interfaces ๐Ÿ“Š, understanding MobX observables is essential for creating responsive, maintainable code.

By the end of this tutorial, youโ€™ll feel confident creating observable state that automatically updates your UI whenever data changes! Letโ€™s dive in! ๐ŸŠโ€โ™‚๏ธ

๐Ÿ“š Understanding MobX Observable State

๐Ÿค” What is MobX Observable State?

MobX observable state is like having a smart assistant ๐Ÿค– that watches your data and automatically notifies everyone who cares when something changes. Think of it as a subscription service ๐Ÿ“ก for your data - components subscribe to state changes and get updates instantly!

In TypeScript terms, MobX transforms your regular objects and values into reactive data structures โœจ. This means you can:

  • โœจ Automatic UI updates when data changes
  • ๐Ÿš€ Simple, intuitive state management
  • ๐Ÿ›ก๏ธ Type-safe reactive programming
  • ๐Ÿ”„ Minimal boilerplate code

๐Ÿ’ก Why Use MobX with TypeScript?

Hereโ€™s why developers love MobX observables:

  1. Type Safety ๐Ÿ”’: TypeScript ensures your observable state is properly typed
  2. Better IDE Support ๐Ÿ’ป: Autocomplete and refactoring for reactive code
  3. Automatic Derivations ๐Ÿ“–: Computed values update automatically
  4. Simple Mental Model ๐Ÿ”ง: State changes trigger reactions naturally

Real-world example: Imagine building a shopping cart ๐Ÿ›’. With MobX observables, when you add an item, the cart total, item count, and UI all update automatically!

๐Ÿ”ง Basic Syntax and Usage

๐Ÿ“ Setting Up MobX

First, letโ€™s install MobX with TypeScript support:

# ๐Ÿ“ฆ Install MobX and TypeScript support
npm install mobx
npm install --save-dev @types/node

Enable decorators in your tsconfig.json:

{
  "compilerOptions": {
    "experimentalDecorators": true,
    "useDefineForClassFields": false
  }
}

๐ŸŽจ Creating Your First Observable

Letโ€™s start with a friendly example:

import { makeObservable, observable, action } from 'mobx';

// ๐ŸŽฏ Simple observable counter
class Counter {
  count = 0; // ๐Ÿ“Š Our observable value

  constructor() {
    // ๐ŸŽจ Make this object observable
    makeObservable(this, {
      count: observable,
      increment: action,
      decrement: action
    });
  }

  // โฌ†๏ธ Action to increase count
  increment = (): void => {
    this.count += 1;
    console.log(`Count is now: ${this.count} ๐ŸŽ‰`);
  };

  // โฌ‡๏ธ Action to decrease count
  decrement = (): void => {
    this.count -= 1;
    console.log(`Count is now: ${this.count} ๐Ÿ“‰`);
  };
}

// ๐ŸŽฎ Let's use it!
const counter = new Counter();
counter.increment(); // Count is now: 1 ๐ŸŽ‰
counter.increment(); // Count is now: 2 ๐ŸŽ‰

๐Ÿ’ก Explanation: The makeObservable call tells MobX which properties to watch and which methods can modify state!

๐ŸŽฏ Observable Arrays and Objects

Here are patterns youโ€™ll use daily:

import { makeObservable, observable, action, computed } from 'mobx';

// ๐Ÿ—๏ธ Observable todo list
class TodoStore {
  todos: string[] = []; // ๐Ÿ“ Observable array

  constructor() {
    makeObservable(this, {
      todos: observable,
      addTodo: action,
      removeTodo: action,
      todoCount: computed
    });
  }

  // โž• Add a new todo
  addTodo = (text: string): void => {
    this.todos.push(`${text} โœจ`);
  };

  // ๐Ÿ—‘๏ธ Remove a todo
  removeTodo = (index: number): void => {
    this.todos.splice(index, 1);
  };

  // ๐Ÿ“Š Computed value - automatically updates!
  get todoCount(): number {
    return this.todos.length;
  }
}

๐Ÿ’ก Practical Examples

๐Ÿ›’ Example 1: Shopping Cart Store

Letโ€™s build something real and useful:

import { makeObservable, observable, action, computed } from 'mobx';

// ๐Ÿ›๏ธ Define our product type
interface Product {
  id: string;
  name: string;
  price: number;
  emoji: string; // Every product needs an emoji! 
}

// ๐Ÿ›’ Cart item with quantity
interface CartItem extends Product {
  quantity: number;
}

// ๐Ÿช Shopping cart store
class ShoppingCartStore {
  items: CartItem[] = []; // ๐Ÿ“ฆ Observable array of cart items
  isLoading = false; // โณ Loading state
  
  constructor() {
    makeObservable(this, {
      items: observable,
      isLoading: observable,
      addItem: action,
      removeItem: action,
      updateQuantity: action,
      clear: action,
      totalPrice: computed,
      totalItems: computed,
      isEmpty: computed
    });
  }
  
  // โž• Add item to cart
  addItem = (product: Product): void => {
    const existingItem = this.items.find(item => item.id === product.id);
    
    if (existingItem) {
      // ๐Ÿ“ˆ Increase quantity if item exists
      existingItem.quantity += 1;
    } else {
      // ๐Ÿ†• Add new item
      this.items.push({ ...product, quantity: 1 });
    }
    
    console.log(`Added ${product.emoji} ${product.name} to cart! ๐Ÿ›’`);
  };
  
  // ๐Ÿ—‘๏ธ Remove item completely
  removeItem = (productId: string): void => {
    const index = this.items.findIndex(item => item.id === productId);
    if (index !== -1) {
      const item = this.items[index];
      this.items.splice(index, 1);
      console.log(`Removed ${item.emoji} ${item.name} from cart! ๐Ÿ‘‹`);
    }
  };
  
  // ๐Ÿ”ข Update item quantity
  updateQuantity = (productId: string, quantity: number): void => {
    const item = this.items.find(item => item.id === productId);
    if (item) {
      if (quantity <= 0) {
        this.removeItem(productId);
      } else {
        item.quantity = quantity;
      }
    }
  };
  
  // ๐Ÿงน Clear entire cart
  clear = (): void => {
    this.items = [];
    console.log('Cart cleared! ๐Ÿงน');
  };
  
  // ๐Ÿ’ฐ Computed: Total price (automatically updates!)
  get totalPrice(): number {
    return this.items.reduce((sum, item) => 
      sum + (item.price * item.quantity), 0
    );
  }
  
  // ๐Ÿ“Š Computed: Total items count
  get totalItems(): number {
    return this.items.reduce((sum, item) => sum + item.quantity, 0);
  }
  
  // โ“ Computed: Is cart empty?
  get isEmpty(): boolean {
    return this.items.length === 0;
  }
  
  // ๐Ÿ“‹ Display cart contents
  displayCart = (): void => {
    console.log("๐Ÿ›’ Your cart contains:");
    this.items.forEach(item => {
      console.log(`  ${item.emoji} ${item.name} x${item.quantity} - $${(item.price * item.quantity).toFixed(2)}`);
    });
    console.log(`๐Ÿ’ฐ Total: $${this.totalPrice.toFixed(2)} (${this.totalItems} items)`);
  };
}

// ๐ŸŽฎ Let's use it!
const cart = new ShoppingCartStore();

// Add some products
cart.addItem({ 
  id: "1", 
  name: "TypeScript Book", 
  price: 29.99, 
  emoji: "๐Ÿ“˜" 
});

cart.addItem({ 
  id: "2", 
  name: "Coffee Mug", 
  price: 12.99, 
  emoji: "โ˜•" 
});

cart.addItem({ 
  id: "1", 
  name: "TypeScript Book", 
  price: 29.99, 
  emoji: "๐Ÿ“˜" 
}); // This will increase quantity!

cart.displayCart();
// ๐Ÿ›’ Your cart contains:
//   ๐Ÿ“˜ TypeScript Book x2 - $59.98
//   โ˜• Coffee Mug x1 - $12.99
// ๐Ÿ’ฐ Total: $72.97 (3 items)

๐ŸŽฏ Try it yourself: Add a discount feature and watch the total price update automatically!

๐ŸŽฎ Example 2: Game State Manager

Letโ€™s make it fun with a game:

import { makeObservable, observable, action, computed } from 'mobx';

// ๐Ÿ† Player stats interface
interface PlayerStats {
  name: string;
  level: number;
  experience: number;
  health: number;
  maxHealth: number;
  coins: number;
  achievements: string[];
}

// ๐ŸŽฎ Game state manager
class GameStore {
  // ๐Ÿ‘ค Player state
  player: PlayerStats = {
    name: "Hero",
    level: 1,
    experience: 0,
    health: 100,
    maxHealth: 100,
    coins: 0,
    achievements: ["๐ŸŒŸ New Adventurer"]
  };
  
  // ๐Ÿƒโ€โ™‚๏ธ Game state
  isPlaying = false;
  currentQuest = "";
  enemies: string[] = [];
  
  constructor() {
    makeObservable(this, {
      player: observable,
      isPlaying: observable,
      currentQuest: observable,
      enemies: observable,
      startGame: action,
      gainExperience: action,
      takeDamage: action,
      heal: action,
      earnCoins: action,
      addAchievement: action,
      levelUp: action,
      experienceToNextLevel: computed,
      healthPercentage: computed,
      isAlive: computed
    });
  }
  
  // ๐ŸŽฎ Start the game
  startGame = (playerName: string): void => {
    this.player.name = playerName;
    this.isPlaying = true;
    this.currentQuest = "๐Ÿ—ก๏ธ Defeat the goblin!";
    console.log(`๐ŸŽ‰ Welcome ${playerName}! Your adventure begins!`);
  };
  
  // โญ Gain experience points
  gainExperience = (xp: number): void => {
    this.player.experience += xp;
    console.log(`โœจ Gained ${xp} XP! Total: ${this.player.experience}`);
    
    // ๐Ÿ“ˆ Check for level up
    while (this.player.experience >= this.experienceToNextLevel) {
      this.levelUp();
    }
  };
  
  // ๐Ÿ’” Take damage
  takeDamage = (damage: number): void => {
    const actualDamage = Math.min(damage, this.player.health);
    this.player.health -= actualDamage;
    console.log(`๐Ÿ’ฅ Took ${actualDamage} damage! Health: ${this.player.health}/${this.player.maxHealth}`);
    
    if (!this.isAlive) {
      console.log("๐Ÿ’€ Game Over! Better luck next time!");
      this.isPlaying = false;
    }
  };
  
  // ๐Ÿ’š Heal player
  heal = (amount: number): void => {
    const healAmount = Math.min(amount, this.player.maxHealth - this.player.health);
    this.player.health += healAmount;
    console.log(`๐Ÿ’š Healed ${healAmount} HP! Health: ${this.player.health}/${this.player.maxHealth}`);
  };
  
  // ๐Ÿ’ฐ Earn coins
  earnCoins = (amount: number): void => {
    this.player.coins += amount;
    console.log(`๐Ÿ’ฐ Earned ${amount} coins! Total: ${this.player.coins}`);
  };
  
  // ๐Ÿ† Add achievement
  addAchievement = (achievement: string): void => {
    if (!this.player.achievements.includes(achievement)) {
      this.player.achievements.push(achievement);
      console.log(`๐Ÿ† Achievement unlocked: ${achievement}`);
    }
  };
  
  // ๐Ÿ“ˆ Level up!
  levelUp = (): void => {
    this.player.level += 1;
    const oldMaxHealth = this.player.maxHealth;
    this.player.maxHealth += 20; // Increase max health
    this.player.health = this.player.maxHealth; // Full heal on level up
    
    console.log(`๐ŸŽŠ Level up! You are now level ${this.player.level}!`);
    console.log(`๐Ÿ’ช Max health increased from ${oldMaxHealth} to ${this.player.maxHealth}`);
    
    // ๐Ÿ† Level-based achievements
    if (this.player.level === 5) {
      this.addAchievement("๐ŸŒŸ Apprentice Hero");
    } else if (this.player.level === 10) {
      this.addAchievement("๐Ÿ—ก๏ธ Skilled Warrior");
    }
  };
  
  // ๐Ÿ“Š Computed: XP needed for next level
  get experienceToNextLevel(): number {
    return this.player.level * 100; // 100 XP per level
  };
  
  // ๐Ÿ“Š Computed: Health percentage
  get healthPercentage(): number {
    return Math.round((this.player.health / this.player.maxHealth) * 100);
  };
  
  // ๐Ÿ’— Computed: Is player alive?
  get isAlive(): boolean {
    return this.player.health > 0;
  };
  
  // ๐Ÿ“‹ Display player stats
  displayStats = (): void => {
    console.log("๐Ÿ“Š Player Stats:");
    console.log(`  ๐Ÿ‘ค Name: ${this.player.name}`);
    console.log(`  ๐Ÿ“ˆ Level: ${this.player.level}`);
    console.log(`  โญ Experience: ${this.player.experience}/${this.experienceToNextLevel}`);
    console.log(`  ๐Ÿ’š Health: ${this.player.health}/${this.player.maxHealth} (${this.healthPercentage}%)`);
    console.log(`  ๐Ÿ’ฐ Coins: ${this.player.coins}`);
    console.log(`  ๐Ÿ† Achievements: ${this.player.achievements.join(", ")}`);
  };
}

// ๐ŸŽฎ Let's play!
const game = new GameStore();
game.startGame("TypeScript Warrior");
game.gainExperience(50);
game.earnCoins(25);
game.takeDamage(30);
game.heal(15);
game.displayStats();

๐Ÿš€ Advanced Concepts

๐Ÿง™โ€โ™‚๏ธ Advanced Topic 1: Custom Observable Types

When youโ€™re ready to level up, try custom observable patterns:

import { makeObservable, observable, action, computed } from 'mobx';

// ๐ŸŽฏ Advanced notification system
interface Notification {
  id: string;
  message: string;
  type: 'info' | 'success' | 'warning' | 'error';
  emoji: string;
  timestamp: Date;
  read: boolean;
}

class NotificationStore {
  notifications = new Map<string, Notification>(); // ๐Ÿ“ฌ Observable Map
  maxNotifications = 10; // ๐Ÿ”ข Limit notifications
  
  constructor() {
    makeObservable(this, {
      notifications: observable,
      maxNotifications: observable,
      addNotification: action,
      markAsRead: action,
      clearAll: action,
      unreadCount: computed,
      recentNotifications: computed
    });
  }
  
  // โž• Add notification with auto-cleanup
  addNotification = (message: string, type: Notification['type']): void => {
    const emojis = {
      info: 'โ„น๏ธ',
      success: 'โœ…',
      warning: 'โš ๏ธ',
      error: 'โŒ'
    };
    
    const notification: Notification = {
      id: Date.now().toString(),
      message,
      type,
      emoji: emojis[type],
      timestamp: new Date(),
      read: false
    };
    
    this.notifications.set(notification.id, notification);
    
    // ๐Ÿงน Auto-cleanup old notifications
    if (this.notifications.size > this.maxNotifications) {
      const oldestId = Array.from(this.notifications.keys())[0];
      this.notifications.delete(oldestId);
    }
    
    console.log(`${notification.emoji} ${message}`);
  };
  
  // ๐Ÿ‘๏ธ Mark notification as read
  markAsRead = (id: string): void => {
    const notification = this.notifications.get(id);
    if (notification) {
      notification.read = true;
    }
  };
  
  // ๐Ÿงน Clear all notifications
  clearAll = (): void => {
    this.notifications.clear();
    console.log('๐Ÿงน All notifications cleared!');
  };
  
  // ๐Ÿ“Š Computed: Unread count
  get unreadCount(): number {
    return Array.from(this.notifications.values())
      .filter(n => !n.read).length;
  };
  
  // ๐Ÿ“Š Computed: Recent notifications (last 5)
  get recentNotifications(): Notification[] {
    return Array.from(this.notifications.values())
      .sort((a, b) => b.timestamp.getTime() - a.timestamp.getTime())
      .slice(0, 5);
  };
}

๐Ÿ—๏ธ Advanced Topic 2: Reaction Patterns

For the brave developers, hereโ€™s how to create reactive side effects:

import { makeObservable, observable, action, reaction, when } from 'mobx';

// ๐Ÿš€ Advanced reactive patterns
class ReactiveStore {
  temperature = 20; // ๐ŸŒก๏ธ Temperature in Celsius
  humidity = 50; // ๐Ÿ’ง Humidity percentage
  
  constructor() {
    makeObservable(this, {
      temperature: observable,
      humidity: observable,
      setTemperature: action,
      setHumidity: action
    });
    
    // ๐Ÿ”ฅ React to temperature changes
    reaction(
      () => this.temperature,
      (temp) => {
        if (temp > 30) {
          console.log('๐Ÿ”ฅ It\'s getting hot! Turn on AC!');
        } else if (temp < 10) {
          console.log('๐ŸงŠ Brr! Turn on heating!');
        }
      }
    );
    
    // ๐Ÿ’ง React to humidity changes
    when(
      () => this.humidity > 80,
      () => {
        console.log('๐Ÿ’ง High humidity detected! Turn on dehumidifier!');
      }
    );
  }
  
  setTemperature = (temp: number): void => {
    this.temperature = temp;
  };
  
  setHumidity = (humidity: number): void => {
    this.humidity = humidity;
  };
}

โš ๏ธ Common Pitfalls and Solutions

๐Ÿ˜ฑ Pitfall 1: Forgetting to Mark Actions

// โŒ Wrong way - modifying observable without action!
class BadCounter {
  count = 0;
  
  constructor() {
    makeObservable(this, {
      count: observable
      // ๐Ÿ’ฅ Missing action annotation!
    });
  }
  
  increment(): void {
    this.count++; // ๐Ÿšซ This might not trigger reactions properly!
  }
}

// โœ… Correct way - always mark your actions!
class GoodCounter {
  count = 0;
  
  constructor() {
    makeObservable(this, {
      count: observable,
      increment: action // โœจ Action properly marked!
    });
  }
  
  increment = (): void => {
    this.count++; // โœ… Safe and reactive!
  };
}

๐Ÿคฏ Pitfall 2: Modifying State Outside Actions

// โŒ Dangerous - direct state modification!
const counter = new GoodCounter();
counter.count = 100; // ๐Ÿ’ฅ Bypassing MobX tracking!

// โœ… Safe - use actions!
const counter = new GoodCounter();
// Create a proper action for this:
class BetterCounter extends GoodCounter {
  setValue = action((value: number) => {
    this.count = value; // โœ… Proper action!
  });
}

๐Ÿ˜… Pitfall 3: Overusing Observables

// โŒ Making everything observable unnecessarily
class OverEngineeredStore {
  private apiKey = "secret"; // ๐Ÿšซ This doesn't need to be observable!
  
  constructor() {
    makeObservable(this, {
      apiKey: observable // โŒ Waste of resources!
    });
  }
}

// โœ… Only observe what changes
class EfficinetStore {
  private apiKey = "secret"; // โœ… Regular property
  userData = null; // ๐Ÿ“Š This should be observable
  
  constructor() {
    makeObservable(this, {
      userData: observable // โœ… Only what needs to react
    });
  }
}

๐Ÿ› ๏ธ Best Practices

  1. ๐ŸŽฏ Be Selective: Only make properties observable if they need to trigger reactions
  2. ๐Ÿ“ Use Actions: Always modify observable state through actions
  3. ๐Ÿ›ก๏ธ Enable Strict Mode: Configure MobX to enforce action usage
  4. ๐ŸŽจ Computed Values: Use computed for derived state that updates automatically
  5. โœจ Keep It Simple: Donโ€™t over-engineer your observable structures

Hereโ€™s a perfect setup:

import { configure } from 'mobx';

// ๐Ÿ›ก๏ธ Enable strict mode for better practices
configure({
  enforceActions: "always",
  computedRequiresReaction: true,
  reactionRequiresObservable: true,
  observableRequiresReaction: true,
  disableErrorBoundaries: true
});

๐Ÿงช Hands-On Exercise

๐ŸŽฏ Challenge: Build a Task Management System

Create a type-safe task management system with MobX observables:

๐Ÿ“‹ Requirements:

  • โœ… Task items with title, completion status, priority, and due date
  • ๐Ÿท๏ธ Categories for tasks (work, personal, urgent)
  • ๐Ÿ‘ค Task assignment to different users
  • ๐Ÿ“Š Progress tracking and statistics
  • ๐Ÿ” Filtering and sorting capabilities
  • ๐ŸŽจ Each task needs an emoji based on category!

๐Ÿš€ Bonus Points:

  • Add drag-and-drop priority reordering
  • Implement automatic overdue task detection
  • Create productivity statistics (tasks per day, completion rate)
  • Add task dependencies (canโ€™t start until another is done)

๐Ÿ’ก Solution

๐Ÿ” Click to see solution
import { makeObservable, observable, action, computed } from 'mobx';

// ๐ŸŽฏ Our type-safe task system!
interface Task {
  id: string;
  title: string;
  completed: boolean;
  priority: 'low' | 'medium' | 'high';
  category: 'work' | 'personal' | 'urgent';
  assignee?: string;
  dueDate?: Date;
  createdAt: Date;
}

interface User {
  id: string;
  name: string;
  emoji: string;
}

class TaskManager {
  tasks: Task[] = [];
  users: User[] = [
    { id: '1', name: 'Alice', emoji: '๐Ÿ‘ฉโ€๐Ÿ’ป' },
    { id: '2', name: 'Bob', emoji: '๐Ÿ‘จโ€๐Ÿ”ง' },
    { id: '3', name: 'Charlie', emoji: '๐Ÿ‘จโ€๐ŸŽจ' }
  ];
  
  // ๐Ÿ” Filter settings
  selectedCategory: Task['category'] | 'all' = 'all';
  selectedPriority: Task['priority'] | 'all' = 'all';
  showCompleted = true;
  
  constructor() {
    makeObservable(this, {
      tasks: observable,
      users: observable,
      selectedCategory: observable,
      selectedPriority: observable,
      showCompleted: observable,
      addTask: action,
      toggleTask: action,
      deleteTask: action,
      updateTask: action,
      setFilter: action,
      filteredTasks: computed,
      completionRate: computed,
      overdueTasks: computed,
      tasksByCategory: computed,
      productivity: computed
    });
  }
  
  // โž• Add a new task
  addTask = (taskData: Omit<Task, 'id' | 'createdAt'>): void => {
    const categoryEmojis = {
      work: '๐Ÿ’ผ',
      personal: '๐Ÿ ',
      urgent: '๐Ÿšจ'
    };
    
    const newTask: Task = {
      ...taskData,
      id: Date.now().toString(),
      createdAt: new Date()
    };
    
    this.tasks.push(newTask);
    console.log(`โœ… Added task: ${categoryEmojis[newTask.category]} ${newTask.title}`);
  };
  
  // โœ… Toggle task completion
  toggleTask = (taskId: string): void => {
    const task = this.tasks.find(t => t.id === taskId);
    if (task) {
      task.completed = !task.completed;
      console.log(`${task.completed ? 'โœ…' : '๐Ÿ“'} ${task.title} ${task.completed ? 'completed' : 'reopened'}`);
    }
  };
  
  // ๐Ÿ—‘๏ธ Delete task
  deleteTask = (taskId: string): void => {
    const taskIndex = this.tasks.findIndex(t => t.id === taskId);
    if (taskIndex !== -1) {
      const task = this.tasks[taskIndex];
      this.tasks.splice(taskIndex, 1);
      console.log(`๐Ÿ—‘๏ธ Deleted task: ${task.title}`);
    }
  };
  
  // โœ๏ธ Update task
  updateTask = (taskId: string, updates: Partial<Task>): void => {
    const task = this.tasks.find(t => t.id === taskId);
    if (task) {
      Object.assign(task, updates);
      console.log(`โœ๏ธ Updated task: ${task.title}`);
    }
  };
  
  // ๐Ÿ” Set filters
  setFilter = (category: typeof this.selectedCategory, priority: typeof this.selectedPriority, showCompleted: boolean): void => {
    this.selectedCategory = category;
    this.selectedPriority = priority;
    this.showCompleted = showCompleted;
  };
  
  // ๐Ÿ“Š Computed: Filtered tasks
  get filteredTasks(): Task[] {
    return this.tasks.filter(task => {
      const categoryMatch = this.selectedCategory === 'all' || task.category === this.selectedCategory;
      const priorityMatch = this.selectedPriority === 'all' || task.priority === this.selectedPriority;
      const completedMatch = this.showCompleted || !task.completed;
      
      return categoryMatch && priorityMatch && completedMatch;
    });
  };
  
  // ๐Ÿ“ˆ Computed: Completion rate
  get completionRate(): number {
    if (this.tasks.length === 0) return 100;
    const completed = this.tasks.filter(t => t.completed).length;
    return Math.round((completed / this.tasks.length) * 100);
  };
  
  // โฐ Computed: Overdue tasks
  get overdueTasks(): Task[] {
    const now = new Date();
    return this.tasks.filter(task => 
      !task.completed && 
      task.dueDate && 
      task.dueDate < now
    );
  };
  
  // ๐Ÿท๏ธ Computed: Tasks by category
  get tasksByCategory(): Record<Task['category'], number> {
    return this.tasks.reduce((acc, task) => {
      acc[task.category] = (acc[task.category] || 0) + 1;
      return acc;
    }, {} as Record<Task['category'], number>);
  };
  
  // ๐Ÿ“Š Computed: Productivity stats
  get productivity(): { today: number; thisWeek: number; average: number } {
    const now = new Date();
    const today = new Date(now.getFullYear(), now.getMonth(), now.getDate());
    const weekAgo = new Date(today.getTime() - 7 * 24 * 60 * 60 * 1000);
    
    const completedToday = this.tasks.filter(t => 
      t.completed && t.createdAt >= today
    ).length;
    
    const completedThisWeek = this.tasks.filter(t => 
      t.completed && t.createdAt >= weekAgo
    ).length;
    
    const averagePerDay = this.tasks.length > 0 ? 
      Math.round(this.tasks.filter(t => t.completed).length / 7) : 0;
    
    return {
      today: completedToday,
      thisWeek: completedThisWeek,
      average: averagePerDay
    };
  };
  
  // ๐Ÿ“‹ Display dashboard
  displayDashboard = (): void => {
    console.log("๐Ÿ“Š Task Dashboard:");
    console.log(`  ๐Ÿ“ Total Tasks: ${this.tasks.length}`);
    console.log(`  โœ… Completed: ${this.tasks.filter(t => t.completed).length}`);
    console.log(`  ๐Ÿ“ˆ Completion Rate: ${this.completionRate}%`);
    console.log(`  โฐ Overdue: ${this.overdueTasks.length}`);
    console.log(`  ๐Ÿท๏ธ By Category:`);
    Object.entries(this.tasksByCategory).forEach(([category, count]) => {
      const emojis = { work: '๐Ÿ’ผ', personal: '๐Ÿ ', urgent: '๐Ÿšจ' };
      console.log(`    ${emojis[category as Task['category']]} ${category}: ${count}`);
    });
    
    const { today, thisWeek, average } = this.productivity;
    console.log(`  ๐Ÿ“ˆ Productivity:`);
    console.log(`    Today: ${today} tasks`);
    console.log(`    This week: ${thisWeek} tasks`);
    console.log(`    Daily average: ${average} tasks`);
  };
}

// ๐ŸŽฎ Test the system!
const taskManager = new TaskManager();

// Add some tasks
taskManager.addTask({
  title: "Learn MobX with TypeScript",
  completed: false,
  priority: "high",
  category: "personal",
  dueDate: new Date(Date.now() + 86400000) // Tomorrow
});

taskManager.addTask({
  title: "Fix production bug",
  completed: false,
  priority: "urgent",
  category: "work",
  assignee: "1"
});

taskManager.addTask({
  title: "Buy groceries",
  completed: true,
  priority: "low",
  category: "personal"
});

taskManager.displayDashboard();

๐ŸŽ“ Key Takeaways

Youโ€™ve learned so much! Hereโ€™s what you can now do:

  • โœ… Create MobX observables with confidence ๐Ÿ’ช
  • โœ… Manage reactive state that updates automatically ๐Ÿ›ก๏ธ
  • โœ… Use actions and computed values properly ๐ŸŽฏ
  • โœ… Debug observable issues like a pro ๐Ÿ›
  • โœ… Build reactive applications with TypeScript! ๐Ÿš€

Remember: MobX makes state management simple and intuitive. Your data becomes alive and reactive! ๐Ÿค

๐Ÿค Next Steps

Congratulations! ๐ŸŽ‰ Youโ€™ve mastered MobX Observable State with TypeScript!

Hereโ€™s what to do next:

  1. ๐Ÿ’ป Practice with the task management exercise above
  2. ๐Ÿ—๏ธ Build a reactive app using MobX observables
  3. ๐Ÿ“š Move on to our next tutorial: โ€œMobX Actions and Reactions: State Mutationsโ€
  4. ๐ŸŒŸ Share your reactive creations with the community!

Remember: Every MobX expert was once a beginner. Keep coding, keep learning, and most importantly, enjoy watching your state come alive! ๐Ÿš€


Happy coding! ๐ŸŽ‰๐Ÿš€โœจ