Skip to content
React rc state-management 5 min read

MobX

MobX takes a different philosophy from Redux: instead of immutable snapshots and reducers, you keep plain mutable objects and let MobX track which observable values each component reads. When you change a value, only the components that actually used it re-render — automatically, with no selectors or dependency arrays. The model is “transparent functional reactive programming”: anything that can be derived from your state is derived for you, and kept consistent without manual wiring.

The mental model

MobX has three core concepts. Observables are the state — fields whose reads and writes MobX intercepts. Computeds are values derived from observables; they cache automatically and only recompute when their inputs change. Actions are the functions that mutate observables. React components become reactive by wrapping them in observer, which subscribes the component to exactly the observables it touched during render.

The big contrast with Redux: in Redux you must return new immutable objects from reducers and select slices to avoid extra renders. In MobX you mutate state directly (this.count++) and MobX figures out the rest. It is closer to Vue’s reactivity than to Redux’s discipline.

Installation

Install the core library plus the React bindings:

npm install mobx mobx-react-lite

mobx-react-lite is the modern, hooks-only binding for function components. (The older mobx-react adds class-component support you rarely need today.)

Creating an observable store

makeAutoObservable is the simplest way to define a store: it inspects your object and automatically marks fields as observable, methods as actions, and getters as computeds.

// stores/CartStore.js
import { makeAutoObservable } from 'mobx';

class CartStore {
  items = [];

  constructor() {
    makeAutoObservable(this);
  }

  // action — mutates observable state directly
  addItem(product) {
    const existing = this.items.find((i) => i.id === product.id);
    if (existing) {
      existing.qty += 1;
    } else {
      this.items.push({ ...product, qty: 1 });
    }
  }

  removeItem(id) {
    this.items = this.items.filter((i) => i.id !== id);
  }

  // computed — cached, recomputes only when items change
  get count() {
    return this.items.reduce((sum, i) => sum + i.qty, 0);
  }

  get total() {
    return this.items.reduce((sum, i) => sum + i.qty * i.price, 0);
  }
}

export const cartStore = new CartStore();

Notice we mutate directly: existing.qty += 1 and this.items.push(...). With Redux that would be a bug; in MobX it is the intended API. The count and total getters are computeds — reading them is cheap because MobX caches the result until items actually changes.

Reading state in components with observer

Wrap any component that reads observables in observer. That is the only step needed to make it reactive — no useSelector, no connect, no provider.

// Cart.jsx
import { observer } from 'mobx-react-lite';
import { cartStore } from './stores/CartStore';

const Cart = observer(function Cart() {
  return (
    <section>
      <h2>Cart ({cartStore.count} items)</h2>
      <ul>
        {cartStore.items.map((item) => (
          <li key={item.id}>
            {item.title} × {item.qty}
            <button onClick={() => cartStore.removeItem(item.id)}>Remove</button>
          </li>
        ))}
      </ul>
      <strong>Total: ${cartStore.total.toFixed(2)}</strong>
    </section>
  );
});

export default Cart;

Because this component read cartStore.count, cartStore.items, and cartStore.total, MobX subscribes it to exactly those. A component elsewhere that reads only cartStore.count will not re-render when you change an unrelated observable.

// CartBadge.jsx — re-renders only when count changes
import { observer } from 'mobx-react-lite';
import { cartStore } from './stores/CartStore';

const CartBadge = observer(() => <span className="badge">{cartStore.count}</span>);

export default CartBadge;

Forgetting observer is the number-one MobX gotcha. A component that reads observables but is not wrapped will render once and then silently stop updating. If a value looks frozen, check that the component is an observer.

Async actions

Async work needs a little care: any code that mutates observables after an await runs outside the original action, so MobX warns about it in strict mode. Wrap the post-await mutation in runInAction (or mark it with flow).

// stores/ProductStore.js
import { makeAutoObservable, runInAction } from 'mobx';

class ProductStore {
  products = [];
  loading = false;

  constructor() {
    makeAutoObservable(this);
  }

  async load() {
    this.loading = true;
    const res = await fetch('https://fakestoreapi.com/products?limit=3');
    const data = await res.json();
    runInAction(() => {
      this.products = data;
      this.loading = false;
    });
  }
}

export const productStore = new ProductStore();

Reacting outside React

autorun runs a function immediately and re-runs it whenever any observable it read changes — handy for logging, persistence, or syncing to localStorage without a component.

import { autorun } from 'mobx';
import { cartStore } from './stores/CartStore';

autorun(() => {
  console.log(`Cart now has ${cartStore.count} items, total $${cartStore.total}`);
});

cartStore.addItem({ id: 1, title: 'Keyboard', price: 49.99 });
cartStore.addItem({ id: 1, title: 'Keyboard', price: 49.99 });

Output:

Cart now has 0 items, total $0
Cart now has 1 items, total $49.99
Cart now has 2 items, total $99.98

MobX vs Redux at a glance

AspectMobXRedux Toolkit
State shapeMutable observable objectsImmutable plain objects
UpdatesMutate directly inside actionsReturn new state (Immer drafts)
Re-render controlAutomatic, per-observable trackingManual via selectors
Derived valuescomputed getters (auto-cached)createSelector memoization
BoilerplateVery lowModerate
Time-travel debuggingLimitedFirst-class via DevTools

Best Practices

  • Always wrap components that read observables in observer; this is what makes them reactive.
  • Use makeAutoObservable for stores so you do not have to annotate every field, getter, and method by hand.
  • Express anything derivable as a computed getter rather than recomputing it in render — it caches and stays consistent.
  • Mutate observables only inside actions; wrap post-await mutations in runInAction to satisfy strict mode.
  • Keep observable objects passed into observer components small and specific so MobX’s tracking can be precise.
  • Prefer one store per domain (cart, products, user) over a single mega-store; instantiate them once at module scope.
  • Read observables inside the observer’s render body, not in a parent that forwards them as props, or you lose fine-grained tracking.
Last updated June 14, 2026
Was this helpful?