mrkeyoor.com_
Sat 08 Aug 21:57 UTC
npmWeb Frontendupdated 08 Aug 2026

mobx

MobX is a framework-independent state manager built around mutable observable objects. Code reads ordinary properties inside tracked functions, MobX records those reads, and later writes invalidate only the computed values, reactions, or UI components that used them. Classes, objects, arrays, maps, sets, computed getters, actions, and cancellable generator-based async flows are supported. React needs the separate mobx-react-lite or mobx-react binding. Version 7 targets modern JavaScript engines with native Proxy and Stage 3 decorators, although decorators remain optional.

Verdict

MobX remains excellent for rich mutable domain models and fine-grained UI updates, especially when state must live outside React. Skip it for simple shared state or server-cache problems, and budget time for its runtime tracking model and reaction lifecycle.

API stability4/5The central model of observable state, computed derivations, actions, autorun, reaction, and observer has survived many major releases, and the v7 migration guide describes version 7 as mostly cleanup for idiomatic v6 applications. The major still removes real surface area: legacy decorators, trace, non-Proxy operation, namespaced annotation properties, and several React binding helpers are gone. Teams staying on documented modern patterns get a steady API, while older code should treat a major upgrade as planned migration work rather than a routine version bump.
Docs5/5mobx.js.org covers the mental model, observable creation, actions, computed values, reactions, React integration, configuration, subclassing, debugging, and migration with runnable examples. The understanding-reactivity guide is unusually candid about missed updates caused by reads outside tracked functions, asynchronous callbacks, local references, console logging, and array indexing. Version 7 also has a dedicated migration page with replacement tables for every removed namespaced API and React helper. Some older external courses remain linked, but the first-party reference clearly marks current v7 guidance.
Maintenance5/5MobX 7.0.0 was published in July 2026, the repository was pushed on August 2, and the current changelog documents the coordinated MobX and React-binding release rather than an isolated republish. Recent work includes smaller production bundles, lazy observable and computed allocation, TypeScript fixes, memory-leak fixes, modern collection methods, and package-build corrections. The repository reports 67 open issues and pull requests across the monorepo, a manageable combined queue for a project with 28,205 stars and several maintained packages.
Ecosystem5/5MobX records 3,622,658 downloads for the measured week and the repository has 28,205 stars. Officially maintained React bindings cover both function and class components, while documented community integrations exist for Lit, Angular, Vue, Flutter, and supporting utilities. The core is independent of a renderer and ships both ESM and CommonJS entry points with TypeScript types, which makes stores portable across UI, Node, and tests. The ecosystem is smaller than Redux's, but it is established, multi-framework, and supported by mature debugging and utility packages.

Use it if

  • Your domain model is naturally expressed as long-lived classes and mutable objects with computed properties
  • You want fine-grained updates without writing selectors, reducers, immutable update code, or dependency arrays by hand
  • The same state and business rules must run outside React in Node, tests, workers, or another view layer
  • Your team is willing to learn observable reads, actions, derivations, and reaction disposal as architectural concepts
Skip it if

Setup reality

Installing mobx alone gives the framework-independent core with TypeScript declarations and no runtime dependencies, native addons, credentials, or required config file. Browser and Node targets must provide native Proxy; use MobX 6 if they do not. React is a separate install: mobx-react-lite handles function components, while mobx-react adds class components, and the v7-compatible binding releases require React 18 or later. Wrap every component that reads observables in observer and read the properties during render; a value destructured earlier or read later in a timeout is outside the tracked function and will not trigger updates. For classes, makeAutoObservable is the low-ceremony path. If you choose Stage 3 decorators, use TypeScript 5+ with experimentalDecorators removed or disabled, add accessor to observable fields, and do not call makeObservable for decorated fields. Non-decorator class builds should use spec-compliant field emit, including useDefineForClassFields: true in TypeScript. Decide on configure rules early: enforceActions defaults to observed, while always is stricter and often noisy in tests. Every autorun, reaction, and observer created manually holds references until disposed, so return cleanup functions from components and services. After each await, mutations need runInAction, an action callback, or flow. For server rendering, call enableStaticRendering(true) from the React binding to prevent observers from waiting forever for updates. The difficult setup is not installation; it is keeping reads inside tracked scopes, writes inside actions, and reaction ownership explicit.

Patterns

Require writes to happen inside actionsconfigure-actions

import {configure} from 'mobx';

configure({
  enforceActions: 'always',
  computedRequiresReaction: true,
  reactionRequiresObservable: true
});

The default enforceActions mode is observed. The always setting catches more mistakes but also requires action context during lazy creation and can be irritating in unit tests.

Create a class store with automatic annotationscreate-class-store

import {makeAutoObservable} from 'mobx';

class CartStore {
  items = [];

  constructor() {
    makeAutoObservable(this, {}, {autoBind: true});
  }

  add(item) {
    this.items.push(item);
  }

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

export const cart = new CartStore();

makeAutoObservable infers fields as observable, getters as computed, and methods as actions. It cannot be used on classes with super or subclasses; use makeObservable there.

Control annotations with makeObservableannotate-store-explicitly

import {actionBound, computed, makeObservable, observable, observableRef} from 'mobx';

class SearchStore {
  query = '';
  results = [];

  constructor() {
    makeObservable(this, {
      query: observable,
      results: observableRef,
      resultCount: computed,
      setQuery: actionBound
    });
  }

  get resultCount() { return this.results.length; }
  setQuery(value) { this.query = value; }
}

MobX 7 uses named exports such as observableRef and actionBound. The MobX 6 forms observable.ref and action.bound were removed.

Make a plain object observablecreate-observable-object

import {observable, runInAction} from 'mobx';

const session = observable({
  user: null,
  preferences: {theme: 'system'}
});

runInAction(() => {
  session.user = {id: 'u1', name: 'Ada'};
  session.preferences.theme = 'dark';
});

Observable objects are deep by default and Proxy-backed in v7. Use observableRef when a field should track replacement without converting its assigned value.

Re-enter an action after awaitingupdate-after-await

import {runInAction} from 'mobx';

async function loadUser(store, id) {
  store.loading = true;
  try {
    const response = await fetch(`/api/users/${id}`);
    if (!response.ok) throw new Error(`HTTP ${response.status}`);
    const user = await response.json();
    runInAction(() => { store.user = user; });
  } finally {
    runInAction(() => { store.loading = false; });
  }
}

An action only covers the current synchronous call stack. Code after await needs runInAction, another action, or a MobX flow.

Model cancellable async work with flowrun-async-flow

import {flow, observable} from 'mobx';

const state = observable({projects: [], loading: false});

const loadProjects = flow(function* () {
  state.loading = true;
  try {
    const response = yield fetch('/api/projects');
    if (!response.ok) throw new Error(`HTTP ${response.status}`);
    state.projects = yield response.json();
  } finally {
    state.loading = false;
  }
});

const task = loadProjects();
// task.cancel();

flow keeps each generator continuation in action context and returns a cancellable promise. Cancellation is cooperative and still runs finally blocks.

Run and dispose an effectautorun-effect

import {autorun} from 'mobx';

const dispose = autorun(() => {
  localStorage.setItem('theme', store.preferences.theme);
});

// when the owner is destroyed
dispose();

autorun executes immediately and tracks observable reads made during that synchronous execution. Reads inside a promise or timer callback are not tracked, and undisposed reactions can leak.

React only when a selected value changesreaction-specific-value

import {reaction} from 'mobx';

const dispose = reaction(
  () => store.search.query.trim(),
  (query, previousQuery) => {
    analytics.track('search_changed', {query, previousQuery});
  },
  {fireImmediately: false}
);

Unlike autorun, reaction tracks only the first function and does not run the effect initially unless fireImmediately is true. Dispose it with its owning service or component.

Wait until observable state matches a conditionwait-for-state

import {when} from 'mobx';

await when(
  () => session.status === 'authenticated',
  {timeout: 10_000}
);
startAuthenticatedWork();

The promise rejects on timeout. If the caller can be abandoned earlier, use the AbortSignal option or the effect form of when so you can dispose it explicitly.

Use an observable map for keyed recordstrack-dynamic-keys

import {autorun, observable, runInAction} from 'mobx';

const users = observable.map();
const dispose = autorun(() => {
  console.log(users.get('u1')?.name ?? 'missing');
});

runInAction(() => {
  users.set('u1', {name: 'Ada'});
});

Observable maps can track a key before it exists, which makes them a clear fit for dynamic dictionaries. Remember to dispose the autorun.

Render observable state in Reactrender-react-store

import {observer} from 'mobx-react-lite';

export const CartSummary = observer(function CartSummary({cart}) {
  return (
    <button onClick={() => cart.clear()}>
      {cart.items.length} items, {cart.total.toFixed(2)}
    </button>
  );
});

Install mobx-react-lite separately for function components. Read observable properties during the observer render; passing an already extracted plain value loses that dependency.

Inspect what a reaction actually tracksdebug-dependencies

import {autorun, getDependencyTree} from 'mobx';

const dispose = autorun(() => {
  console.log(store.currentUser.name);
});

console.dir(getDependencyTree(dispose), {depth: null});
dispose();

MobX 7 removed trace. Use getDependencyTree, getObserverTree, spy, or developer tools when an update runs too often or not at all.

Alternatives

PackageRegistryPick it when
zustandnpmYou want a smaller hook-oriented React store with explicit selectors and little ceremony
@reduxjs/toolkitnpmYou value explicit events, immutable state, serializable tooling, and a widely standardized team architecture
xstatenpmYour hard problem is modeling legal states, events, guards, and workflows rather than general object reactivity
valtionpmYou like proxy-based mutable state but want a smaller, React-focused API