MobX
Gestión de estado simple y escalable con observables y reacciones
MobX - Gestión de Estado Simple y Escalable
MobX es una biblioteca probada en batalla que hace que la gestión de estado sea simple y escalable aplicando transparentemente programación funcional reactiva (FRP). A diferencia de Redux, MobX usa estado observable y seguimiento automático de dependencias, haciéndolo sentir más "mágico" pero requiriendo menos código repetitivo.
Conceptos Básicos
- Estado Observable — Estado que MobX rastrea para cambios
- Acciones — Funciones que modifican el estado
- Valores Computados — Valores derivados que se actualizan automáticamente
- Reacciones — Efectos secundarios que se ejecutan cuando los observables cambian
Instalación
npm install mobx mobx-react-lite
Store Básico
import { makeAutoObservable } from 'mobx';
class CounterStore {
count = 0;
constructor() {
// Hace todas las propiedades observables y los métodos acciones
makeAutoObservable(this);
}
increment() {
this.count++;
}
decrement() {
this.count--;
}
reset() {
this.count = 0;
}
// Valor computado
get doubled() {
return this.count * 2;
}
}
// Crea instancia del store
const counterStore = new CounterStore();
export default counterStore;
Uso con React
import { observer } from 'mobx-react-lite';
import counterStore from './counterStore';
// Envuelve el componente con observer para reaccionar a cambios observables
const Counter = observer(() => {
return (
<div>
<p>Cuenta: {counterStore.count}</p>
<p>Doble: {counterStore.doubled}</p>
<button onClick={() => counterStore.increment()}>+</button>
<button onClick={() => counterStore.decrement()}>-</button>
<button onClick={() => counterStore.reset()}>Reiniciar</button>
</div>
);
});
export default Counter;
Ejemplo de Store de Tareas
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);
}
// Valores computados
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();
Uso del Store de Tareas
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')}>Todas ({stats.total})</button>
<button onClick={() => todoStore.setFilter('active')}>Activas ({stats.active})</button>
<button onClick={() => todoStore.setFilter('completed')}>Completadas ({stats.completed})</button>
</div>
<ul>
{filteredTodos.map(todo => (
<TodoItem key={todo.id} todo={todo} />
))}
</ul>
<button onClick={() => todoStore.clearCompleted()}>
Limpiar Completadas
</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">Agregar</button>
</form>
);
});
Acciones Asíncronas
import { makeAutoObservable, runInAction } from 'mobx';
class UserStore {
users = [];
loading = false;
error = null;
constructor() {
makeAutoObservable(this);
}
// Acción asíncrona - usa runInAction para actualizaciones de estado después de await
async fetchUsers() {
this.loading = true;
this.error = null;
try {
const response = await fetch('/api/users');
const data = await response.json();
// Debe usar runInAction para actualizaciones después de await
runInAction(() => {
this.users = data;
this.loading = false;
});
} catch (error) {
runInAction(() => {
this.error = error.message;
this.loading = false;
});
}
}
// Alternativa: Usa flow para generadores
*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;
}
}
}
Contexto e Inyección de Dependencias
import { createContext, useContext } from 'react';
import { TodoStore } from './todoStore';
import { UserStore } from './userStore';
// Crea un store raíz
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>
);
}
// Hook personalizado para acceder a stores
export function useStores() {
const store = useContext(StoreContext);
if (!store) {
throw new Error('useStores debe usarse dentro de StoreProvider');
}
return store;
}
// Uso en componentes
const TodoList = observer(() => {
const { todoStore } = useStores();
return (/* ... */);
});
MobX vs Redux
| Aspecto | MobX | Redux |
|---|---|---|
| Filosofía | Observable/reactivo | Inmutable/funcional |
| Código repetitivo | Mínimo | Más verboso |
| Actualizaciones de estado | Mutable (parece) | Solo inmutable |
| Curva de aprendizaje | Menor | Mayor |
| Depuración | Menos predecible | Time-travel, predecible |
💡 Mejores Prácticas
- • Usa makeAutoObservable para menos código repetitivo
- • Envuelve componentes React con observer()
- • Usa runInAction para actualizaciones de estado después de await
- • Mantén valores computados para datos derivados
- • Organiza stores por dominio/característica
- • Usa modo estricto en desarrollo