TechLead

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