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