MobX Complete Guide | Simple Reactive State Management
이 글의 핵심
MobX is a simple, scalable state management solution that makes state management transparent through reactive programming. It automatically tracks dependencies and updates components.
Introduction
MobX is a battle-tested library that makes state management simple and scalable by transparently applying functional reactive programming (FRP). The philosophy is simple: anything that can be derived from the application state, should be derived automatically.
The Problem
Traditional state management:
// Redux: Too much boilerplate
const reducer = (state, action) => {
switch (action.type) {
case 'INCREMENT':
return { ...state, count: state.count + 1 };
default:
return state;
}
};
// Context: Re-renders entire tree
const [state, setState] = useState({ count: 0, name: '' });
// Changing count re-renders all consumers
The Solution
With MobX:
import { makeObservable, observable, action } from 'mobx';
class Store {
count = 0;
constructor() {
makeObservable(this, {
count: observable,
increment: action,
});
}
increment() {
this.count++;
}
}
1. Installation
npm install mobx mobx-react-lite
2. Core Concepts
Observable State
import { makeObservable, observable, action } from 'mobx';
class TodoStore {
todos = [];
constructor() {
makeObservable(this, {
todos: observable,
addTodo: action,
});
}
addTodo(text) {
this.todos.push({ id: Date.now(), text, done: false });
}
}
With makeAutoObservable (Simpler)
import { makeAutoObservable } from 'mobx';
class TodoStore {
todos = [];
constructor() {
makeAutoObservable(this); // Automatically marks everything
}
addTodo(text) {
this.todos.push({ id: Date.now(), text, done: false });
}
get completedCount() {
return this.todos.filter(t => t.done).length;
}
}
3. React Integration
observer Component
import { observer } from 'mobx-react-lite';
import { todoStore } from './stores';
const TodoList = observer(() => {
return (
<div>
{todoStore.todos.map(todo => (
<div key={todo.id}>{todo.text}</div>
))}
</div>
);
});
Creating Store Context
// stores/TodoStore.ts
import { makeAutoObservable } from 'mobx';
class TodoStore {
todos = [];
constructor() {
makeAutoObservable(this);
}
addTodo(text: string) {
this.todos.push({ id: Date.now(), text, done: false });
}
toggleTodo(id: number) {
const todo = this.todos.find(t => t.id === id);
if (todo) todo.done = !todo.done;
}
}
export const todoStore = new TodoStore();
// App.tsx
import { observer } from 'mobx-react-lite';
import { todoStore } from './stores/TodoStore';
const App = observer(() => {
const [text, setText] = useState('');
const handleAdd = () => {
todoStore.addTodo(text);
setText('');
};
return (
<div>
<input value={text} onChange={e => setText(e.target.value)} />
<button onClick={handleAdd}>Add</button>
<ul>
{todoStore.todos.map(todo => (
<li key={todo.id} onClick={() => todoStore.toggleTodo(todo.id)}>
{todo.done ? '✓' : '○'} {todo.text}
</li>
))}
</ul>
</div>
);
});
4. Computed Values
import { makeAutoObservable, computed } from 'mobx';
class CartStore {
items = [];
constructor() {
makeAutoObservable(this);
}
get total() {
return this.items.reduce((sum, item) => sum + item.price * item.quantity, 0);
}
get itemCount() {
return this.items.reduce((sum, item) => sum + item.quantity, 0);
}
}
Computed values are cached:
const cart = new CartStore();
cart.items.push({ price: 10, quantity: 2 });
console.log(cart.total); // Computes: 20
console.log(cart.total); // Cached: 20 (doesn't recompute)
cart.items[0].quantity = 3;
console.log(cart.total); // Recomputes: 30
5. Actions
Synchronous Actions
import { makeAutoObservable, action } from 'mobx';
class UserStore {
users = [];
constructor() {
makeAutoObservable(this);
}
addUser(user) {
this.users.push(user);
}
removeUser(id) {
this.users = this.users.filter(u => u.id !== id);
}
}
Async Actions (runInAction)
import { makeAutoObservable, runInAction } from 'mobx';
class UserStore {
users = [];
loading = false;
constructor() {
makeAutoObservable(this);
}
async fetchUsers() {
this.loading = true;
try {
const res = await fetch('/api/users');
const users = await res.json();
runInAction(() => {
this.users = users;
this.loading = false;
});
} catch (error) {
runInAction(() => {
this.loading = false;
});
}
}
}
flow (Alternative for Async)
import { makeAutoObservable, flow } from 'mobx';
class UserStore {
users = [];
loading = false;
constructor() {
makeAutoObservable(this, {
fetchUsers: flow,
});
}
*fetchUsers() {
this.loading = true;
try {
const res = yield fetch('/api/users');
const users = yield res.json();
this.users = users;
} finally {
this.loading = false;
}
}
}
6. Reactions
autorun
import { autorun } from 'mobx';
const store = new TodoStore();
autorun(() => {
console.log('Total todos:', store.todos.length);
});
store.addTodo('Learn MobX'); // Logs: Total todos: 1
reaction
import { reaction } from 'mobx';
reaction(
() => store.todos.length, // What to track
(count) => {
console.log('Todo count changed:', count);
}
);
when
import { when } from 'mobx';
when(
() => store.todos.length > 5,
() => console.log('More than 5 todos!')
);
7. Real-World Example: Shopping Cart
// stores/CartStore.ts
import { makeAutoObservable } from 'mobx';
interface CartItem {
id: number;
name: string;
price: number;
quantity: number;
}
class CartStore {
items: CartItem[] = [];
constructor() {
makeAutoObservable(this);
}
addItem(product: { id: number; name: string; price: number }) {
const existing = this.items.find(item => item.id === product.id);
if (existing) {
existing.quantity++;
} else {
this.items.push({ ...product, quantity: 1 });
}
}
removeItem(id: number) {
this.items = this.items.filter(item => item.id !== id);
}
updateQuantity(id: number, quantity: number) {
const item = this.items.find(item => item.id === id);
if (item) {
item.quantity = quantity;
}
}
get total() {
return this.items.reduce((sum, item) => sum + item.price * item.quantity, 0);
}
get itemCount() {
return this.items.reduce((sum, item) => sum + item.quantity, 0);
}
clear() {
this.items = [];
}
}
export const cartStore = new CartStore();
// components/Cart.tsx
import { observer } from 'mobx-react-lite';
import { cartStore } from '../stores/CartStore';
export const Cart = observer(() => {
return (
<div>
<h2>Shopping Cart ({cartStore.itemCount} items)</h2>
{cartStore.items.map(item => (
<div key={item.id}>
<span>{item.name}</span>
<span>${item.price}</span>
<input
type="number"
value={item.quantity}
onChange={e => cartStore.updateQuantity(item.id, +e.target.value)}
/>
<button onClick={() => cartStore.removeItem(item.id)}>Remove</button>
</div>
))}
<div>Total: ${cartStore.total.toFixed(2)}</div>
<button onClick={() => cartStore.clear()}>Clear Cart</button>
</div>
);
});
8. Multiple Stores
// stores/RootStore.ts
import { makeAutoObservable } from 'mobx';
import { UserStore } from './UserStore';
import { CartStore } from './CartStore';
class RootStore {
userStore: UserStore;
cartStore: CartStore;
constructor() {
this.userStore = new UserStore(this);
this.cartStore = new CartStore(this);
makeAutoObservable(this);
}
}
export const rootStore = new RootStore();
// Using React Context
import { createContext, useContext } from 'react';
import { rootStore } from './stores/RootStore';
const StoreContext = createContext(rootStore);
export const useStore = () => useContext(StoreContext);
// In component
const { userStore, cartStore } = useStore();
9. Best Practices
1. Use makeAutoObservable
// Good
class Store {
count = 0;
constructor() {
makeAutoObservable(this);
}
}
// Verbose (avoid unless needed)
class Store {
count = 0;
constructor() {
makeObservable(this, {
count: observable,
increment: action,
});
}
}
2. Use runInAction for Async
// Good
async fetchData() {
const data = await api.getData();
runInAction(() => {
this.data = data;
});
}
// Bad: modifying state outside action
async fetchData() {
const data = await api.getData();
this.data = data; // Warning!
}
3. Avoid Mutating Arrays Directly
// Good
addItem(item) {
this.items.push(item);
}
removeItem(id) {
this.items = this.items.filter(i => i.id !== id);
}
// Also good (with MobX)
removeItem(id) {
this.items.splice(
this.items.findIndex(i => i.id === id),
1
);
}
10. Performance Tips
1. Use observer on Component Level
// Good: Only this component re-renders
const TodoItem = observer(({ todo }) => {
return <div>{todo.text}</div>;
});
// Bad: Parent re-renders all children
const TodoList = observer(() => {
return store.todos.map(todo => <TodoItem todo={todo} />);
});
2. Dereference Values Late
// Good: Only re-renders when name changes
const UserName = observer(({ user }) => {
return <div>{user.name}</div>;
});
// Bad: Re-renders when any user property changes
const UserName = observer(({ userName }) => {
return <div>{userName}</div>;
});
Summary
MobX makes state management simple and transparent:
- Observable state automatically tracked
- Actions to modify state
- Computed values cached and derived
- Reactions for side effects
- Less boilerplate than Redux
Key Takeaways:
- Use
makeAutoObservablefor simplicity - Use
runInActionfor async updates - Use
observeron React components - Computed values are cached automatically
- Less code, more productivity
Next Steps:
- Compare with Zustand
- Learn Redux Toolkit
- Try React Context
Resources: