MobX
Simple and scalable state management with observables and reactions
MobX - Simple, Scalable State Management
MobX is a battle-tested library that makes state management simple and scalable by transparently applying functional reactive programming (FRP). Unlike Redux, MobX uses observable state and automatic tracking of dependencies, making it feel more "magical" but requiring less boilerplate.
Core Concepts
- Observable State — State that MobX tracks for changes
- Actions — Functions that modify state
- Computed Values — Derived values that update automatically
- Reactions — Side effects that run when observables change
Installation
npm install mobx mobx-react-lite
Basic Store
import { makeAutoObservable } from 'mobx';
class CounterStore {
count = 0;
constructor() {
// Makes all properties observable and methods actions
makeAutoObservable(this);
}
increment() {
this.count++;
}
decrement() {
this.count--;
}
reset() {
this.count = 0;
}
// Computed value
get doubled() {
return this.count * 2;
}
}
// Create store instance
const counterStore = new CounterStore();
export default counterStore;
Using with React
import { observer } from 'mobx-react-lite';
import counterStore from './counterStore';
// Wrap component with observer to react to observable changes
const Counter = observer(() => {
return (
<div>
<p>Count: {counterStore.count}</p>
<p>Doubled: {counterStore.doubled}</p>
<button onClick={() => counterStore.increment()}>+</button>
<button onClick={() => counterStore.decrement()}>-</button>
<button onClick={() => counterStore.reset()}>Reset</button>
</div>
);
});
export default Counter;
Todo Store Example
import { makeAutoObservable, runInAction } from 'mobx';
class Todo {
id = Math.random();
text = '';
completed = false;
constructor(text) {
makeAutoObservable(this);
this.text = text;
}
toggle() {
this.completed = !this.completed;
}
}
class TodoStore {
todos = [];
filter = 'all';
constructor() {
makeAutoObservable(this);
}
addTodo(text) {
this.todos.push(new Todo(text));
}
removeTodo(id) {
this.todos = this.todos.filter(todo => todo.id !== id);
}
setFilter(filter) {
this.filter = filter;
}
clearCompleted() {
this.todos = this.todos.filter(todo => !todo.completed);
}
// Computed values
get filteredTodos() {
switch (this.filter) {
case 'active':
return this.todos.filter(t => !t.completed);
case 'completed':
return this.todos.filter(t => t.completed);
default:
return this.todos;
}
}
get stats() {
return {
total: this.todos.length,
completed: this.todos.filter(t => t.completed).length,
active: this.todos.filter(t => !t.completed).length,
};
}
}
export const todoStore = new TodoStore();
Using the Todo Store
import { observer } from 'mobx-react-lite';
import { todoStore } from './todoStore';
const TodoList = observer(() => {
const { filteredTodos, stats, filter } = todoStore;
return (
<div>
<AddTodo />
<div>
<button onClick={() => todoStore.setFilter('all')}>All ({stats.total})</button>
<button onClick={() => todoStore.setFilter('active')}>Active ({stats.active})</button>
<button onClick={() => todoStore.setFilter('completed')}>Done ({stats.completed})</button>
</div>
<ul>
{filteredTodos.map(todo => (
<TodoItem key={todo.id} todo={todo} />
))}
</ul>
<button onClick={() => todoStore.clearCompleted()}>
Clear Completed
</button>
</div>
);
});
const TodoItem = observer(({ todo }) => (
<li>
<input
type="checkbox"
checked={todo.completed}
onChange={() => todo.toggle()}
/>
<span style={{ textDecoration: todo.completed ? 'line-through' : 'none' }}>
{todo.text}
</span>
<button onClick={() => todoStore.removeTodo(todo.id)}>×</button>
</li>
));
const AddTodo = observer(() => {
const [text, setText] = useState('');
const handleSubmit = (e) => {
e.preventDefault();
if (text.trim()) {
todoStore.addTodo(text);
setText('');
}
};
return (
<form onSubmit={handleSubmit}>
<input value={text} onChange={(e) => setText(e.target.value)} />
<button type="submit">Add</button>
</form>
);
});
Async Actions
import { makeAutoObservable, runInAction } from 'mobx';
class UserStore {
users = [];
loading = false;
error = null;
constructor() {
makeAutoObservable(this);
}
// Async action - use runInAction for state updates after await
async fetchUsers() {
this.loading = true;
this.error = null;
try {
const response = await fetch('/api/users');
const data = await response.json();
// Must use runInAction for updates after await
runInAction(() => {
this.users = data;
this.loading = false;
});
} catch (error) {
runInAction(() => {
this.error = error.message;
this.loading = false;
});
}
}
// Alternative: Use flow for generators
*fetchUsersFlow() {
this.loading = true;
try {
const response = yield fetch('/api/users');
this.users = yield response.json();
} catch (error) {
this.error = error.message;
} finally {
this.loading = false;
}
}
}
Context and Dependency Injection
import { createContext, useContext } from 'react';
import { TodoStore } from './todoStore';
import { UserStore } from './userStore';
// Create a root store
class RootStore {
constructor() {
this.todoStore = new TodoStore(this);
this.userStore = new UserStore(this);
}
}
const StoreContext = createContext(null);
export function StoreProvider({ children }) {
const store = new RootStore();
return (
<StoreContext.Provider value={store}>
{children}
</StoreContext.Provider>
);
}
// Custom hook for accessing stores
export function useStores() {
const store = useContext(StoreContext);
if (!store) {
throw new Error('useStores must be used within StoreProvider');
}
return store;
}
// Usage in components
const TodoList = observer(() => {
const { todoStore } = useStores();
return (/* ... */);
});
MobX vs Redux
| Aspect | MobX | Redux |
|---|---|---|
| Philosophy | Observable/reactive | Immutable/functional |
| Boilerplate | Minimal | More verbose |
| State updates | Mutable (looks like) | Immutable only |
| Learning curve | Lower | Higher |
| Debugging | Less predictable | Time-travel, predictable |
💡 Best Practices
- • Use makeAutoObservable for less boilerplate
- • Wrap React components with observer()
- • Use runInAction for state updates after await
- • Keep computed values for derived data
- • Organize stores by domain/feature
- • Use strict mode in development