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:
- Type Safety ๐: TypeScript ensures your observable state is properly typed
- Better IDE Support ๐ป: Autocomplete and refactoring for reactive code
- Automatic Derivations ๐: Computed values update automatically
- 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
- ๐ฏ Be Selective: Only make properties observable if they need to trigger reactions
- ๐ Use Actions: Always modify observable state through actions
- ๐ก๏ธ Enable Strict Mode: Configure MobX to enforce action usage
- ๐จ Computed Values: Use computed for derived state that updates automatically
- โจ 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:
- ๐ป Practice with the task management exercise above
- ๐๏ธ Build a reactive app using MobX observables
- ๐ Move on to our next tutorial: โMobX Actions and Reactions: State Mutationsโ
- ๐ 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! ๐๐โจ