What is MobX?
MobX is a reactive state management library. You mark your data as observable, and MobX tracks everything that reads it. When the data changes, the values and components that depend on it update automatically. There are no selectors to write and no subscriptions to maintain.
The philosophy is almost the opposite of Redux. Where Redux makes every change explicit and traceable through actions and reducers, MobX makes changes implicit through automatic dependency tracking. You write normal object-oriented code, and the reactivity happens underneath.
Observables and makeAutoObservable
The simplest way to build a store is a class annotated with makeAutoObservable, which infers what should be observable, computed and an action.
// store.js
import { makeAutoObservable } from "mobx";
class TodoStore {
todos = [];
constructor() {
makeAutoObservable(this);
}
add(text) {
this.todos.push({ id: Date.now(), text, done: false });
}
toggle(id) {
const todo = this.todos.find((t) => t.id === id);
if (todo) todo.done = !todo.done;
}
get remaining() {
return this.todos.filter((t) => !t.done).length;
}
}
export const todoStore = new TodoStore();
makeAutoObservable treats fields as observables, getters as computed values and methods as actions. You write plain JavaScript — this.todos.push(...) — and MobX tracks it.
Computed values
A computed value derives data from observables. It is cached, and it recalculates only when one of its dependencies changes.
// cart.js
class Cart {
items = [];
constructor() {
makeAutoObservable(this);
}
get total() {
return this.items.reduce((sum, item) => sum + item.price, 0);
}
get isEmpty() {
return this.items.length === 0;
}
}
Because total is computed, reading it in a hundred places costs one calculation. It also stays consistent: change an item’s price and every reader sees the new total without any manual invalidation.
Actions
Actions are the functions that change state. Marking them explicitly lets MobX batch updates and keeps changes traceable.
// actions.js
import { action, makeObservable, observable } from "mobx";
class Settings {
theme = "dark";
fontSize = 16;
constructor() {
makeObservable(this, {
theme: observable,
fontSize: observable,
update: action,
});
}
update({ theme, fontSize }) {
if (theme) this.theme = theme;
if (fontSize) this.fontSize = fontSize;
}
}
With makeAutoObservable you get this automatically. The benefit of actions is batching: several changes inside one action produce a single reaction, not one per assignment. MobX can also enforce that state only changes inside actions, which catches accidental mutations.
Connecting to React with observer
The observer higher-order component subscribes a React component to the observables it reads during render.
// TodoList.jsx
import { observer } from "mobx-react-lite";
import { todoStore } from "./store";
export const TodoList = observer(() => {
return (
<div>
<p>{todoStore.remaining} remaining</p>
<ul>
{todoStore.todos.map((todo) => (
<li key={todo.id} onClick={() => todoStore.toggle(todo.id)}>
{todo.done ? "✓" : "○"} {todo.text}
</li>
))}
</ul>
<button onClick={() => todoStore.add("New task")}>Add</button>
</div>
);
});
The component never lists its dependencies. MobX records them as the component renders, and re-renders it when any of them change. Only components that actually read a changed observable update, which is fine-grained by construction.
Reactions and async state
A reaction runs a side effect when observed data changes, outside the render tree. Use them for logging, persistence or syncing with external systems.
// persist.js
import { autorun } from "mobx";
autorun(() => {
localStorage.setItem("theme", settings.theme);
});
Async actions follow the same pattern as any class method. MobX even ships flow for generator-based async flows, but a plain async method with runInAction for the final state update works well.
// users.js
import { makeAutoObservable, runInAction } from "mobx";
class UserStore {
users = [];
loading = false;
constructor() {
makeAutoObservable(this);
}
async fetchUsers() {
this.loading = true;
const res = await fetch("/api/users");
const users = await res.json();
runInAction(() => {
this.users = users;
this.loading = false;
});
}
}
MobX compared
MobX, Redux and Zustand all manage state, but the mental models differ.
- MobX tracks dependencies automatically. You write mutable-looking code and get fine-grained updates for free.
- Redux makes changes explicit through actions and reducers, which gives strict structure and powerful devtools.
- Zustand sits in between: a tiny store with explicit selectors and almost no ceremony.
If you like object-oriented stores and dislike writing selectors, MobX is a pleasure. If you prefer explicit, functional updates and a single immutable tree, Redux is a better fit. If you want something small and predictable, look at Zustand.
Best practices
- Use
makeAutoObservablefor stores andobserverfor components. - Keep derived data in
computedgetters instead of duplicating calculations. - Mark state changes as actions so updates batch and stay traceable.
- Keep stores as plain classes and avoid storing non-observable UI state in them.
- Use
runInActionfor state updates after anawait. - Keep server data in a data-fetching library rather than a MobX store.
- Do not read observables outside a reaction or an observer and expect updates.
Common mistakes
- Forgetting
observerand wondering why the component does not update. - Reading an observable once and caching the value, losing reactivity.
- Storing large non-observable objects and missing changes.
- Mutating state outside an action when strict mode is enabled.
- Overusing reactions for things that should be computed.
- Treating MobX as a server-state cache.
Where to go next
MobX is a mature, automatic approach to reactive state. Compare it with the explicit model of Redux Toolkit and the minimalism of Zustand, and move server data to TanStack Query. Then build a small store and watch how little wiring it takes to keep the UI in sync.