State Management in MobX: Patterns and Libraries
MobX is a battle-tested state management library that brings the power of transparent functional reactive programming to JavaScript applications. Unlike Redux, which enforces a single source of truth through immutable state and explicit actions, MobX embraces mutable state and automatic dependency tracking. This tutorial walks through the core concepts, practical patterns, and complementary libraries that make MobX a compelling choice for React and beyond.
What Is MobX?
MobX is built on three foundational ideas: observable state, derived values, and reactions. State is the source of truth; derived values (called computeds) are values that depend on observable state; and reactions (like React components or autoruns) automatically respond when state changes. The library tracks which observables each reaction reads, and only re-runs the reaction when those specific observables change.
This fine-grained tracking means you don't need to manually wire selectors or memoize values. MobX figures out the dependency graph for you at runtime.
Why MobX Matters
- Boilerplate-free updates: Mutate state directly and the UI updates automatically.
- Optimal re-renders: Components only re-render when the specific data they read changes.
- Composable stores: Domain logic lives in plain classes, easy to test and reuse.
- Framework agnostic core: Works with React, Vue, vanilla JS, or any UI layer.
Core Building Blocks
Observables
The observable function (or makeObservable/makeAutoObservable for classes) marks a value as reactive. When you read it inside a tracked context and later mutate it, every observer is notified.
import { makeAutoObservable } from "mobx";
class CounterStore {
count = 0;
constructor() {
makeAutoObservable(this);
}
increment() {
this.count++;
}
reset() {
this.count = 0;
}
}
const counter = new CounterStore();
counter.increment();
console.log(counter.count); // 1
makeAutoObservable automatically infers which properties are observable, which are computed, and which are actions based on their type. For more control, use makeObservable with an explicit map.
Computed Values
Computed values are derived from observable state and are cached until their dependencies change. They are declared with getters in a class.
import { makeAutoObservable } from "mobx";
class TodoStore {
todos = [];
constructor() {
makeAutoObservable(this);
}
addTodo(text) {
this.todos.push({ text, done: false });
}
get remainingCount() {
return this.todos.filter((t) => !t.done).length;
}
get completedCount() {
return this.todos.length - this.remainingCount;
}
}
The remainingCount getter is only recomputed when todos or any done flag changes. Calling it repeatedly returns the cached value.
Actions
Actions are functions that mutate state. MobX batches state changes inside actions so that observers are notified once at the end, not on every mutation. With makeAutoObservable, all methods are treated as actions by default.
import { makeObservable, observable, action } from "mobx";
class CartStore {
items = [];
constructor() {
makeObservable(this, {
items: observable,
addItem: action,
removeItem: action,
clear: action,
total: computed,
});
}
addItem(product) {
this.items.push(product);
}
removeItem(id) {
this.items = this.items.filter((i) => i.id !== id);
}
clear() {
this.items = [];
}
get total() {
return this.items.reduce((sum, i) => sum + i.price, 0);
}
}
For asynchronous mutations, wrap the mutating portion in runInAction or use flow for generator-based async actions.
Integrating with React
The observer HOC
The mobx-react-lite package provides the observer function, which turns a React component into a reactive one. The component re-renders only when the observables it reads change.
import { observer } from "mobx-react-lite";
import { counter } from "./stores/counter";
const CounterView = observer(() => {
return (
<div>
<p>Count: {counter.count}</p>
<button onClick={() => counter.increment()}>+</button>
<button onClick={() => counter.reset()}>Reset</button>
</div>
);
});
export default CounterView;
Passing Stores via Context
A common pattern is to instantiate stores at the app root and provide them through React Context. This keeps components decoupled from singleton imports.
import { createContext, useContext } from "react";
import { CounterStore } from "./CounterStore";
import { TodoStore } from "./TodoStore";
export const stores = {
counter: new CounterStore(),
todos: new TodoStore(),
};
const StoreContext = createContext(stores);
export const useStores = () => useContext(StoreContext);
export const StoreProvider = ({ children }) => (
<StoreContext.Provider value={stores}>{children}</StoreContext.Provider>
);
Then consume them in any component:
import { observer } from "mobx-react-lite";
import { useStores } from "./StoreProvider";
const TodoList = observer(() => {
const { todos } = useStores();
return (
<ul>
{todos.todos.map((t, idx) => (
<li key={idx}>{t.text}</li>
))}
</ul>
);
});
Common Patterns
Root Store Composition
For larger apps, group related stores under a single root store. This makes it easy to inject cross-store references and to serialize the entire application state.
import { CounterStore } from "./CounterStore";
import { TodoStore } from "./TodoStore";
import { UserStore } from "./UserStore";
export class RootStore {
counter;
todos;
user;
constructor() {
this.counter = new CounterStore();
this.todos = new TodoStore();
this.user = new UserStore(this); // pass root for cross-store access
}
}
Async Actions with Flow
The flow helper uses generators to handle async operations cleanly, automatically wrapping yielded promises in actions.
import { makeAutoObservable, flow } from "mobx";
class UserStore {
user = null;
loading = false;
error = null;
constructor() {
makeAutoObservable(this, { fetchUser: flow }, { autoBind: true });
}
fetchUser = flow(function* (id) {
this.loading = true;
this.error = null;
try {
const response = yield fetch(`/api/users/${id}`);
this.user = yield response.json();
} catch (err) {
this.error = err.message;
} finally {
this.loading = false;
}
});
}
Local Observable State in Components
For component-local state that needs to be observable, use the useLocalObservable hook.
import { useLocalObservable, observer } from "mobx-react-lite";
const SearchBox = observer(() => {
const state = useLocalObservable(() => ({
query: "",
setQuery(value) {
this.query = value;
},
get isLong() {
return this.query.length > 20;
},
}));
return (
<div>
<input
value={state.query}
onChange={(e) => state.setQuery(e.target.value)}
/>
{state.isLong && <span>Query is quite long</span>}
</div>
);
});
Complementary Libraries
mobx-state-tree (MST)
MST is a structured state container built on top of MobX. It introduces typed models, snapshots, patches, and time-travel debugging. MST is ideal when you need serialization, validation, or a more opinionated architecture.
import { types, flow } from "mobx-state-tree";
const Todo = types.model("Todo", {
id: types.identifier,
text: types.string,
done: types.boolean,
});
const TodoStore = types
.model("TodoStore", {
todos: types.array(Todo),
})
.actions((self) => ({
add(text) {
self.todos.push({ id: String(Date.now()), text, done: false });
},
toggle(id) {
const todo = self.todos.find((t) => t.id === id);
if (todo) todo.done = !todo.done;
},
}))
.views((self) => ({
get remaining() {
return self.todos.filter((t) => !t.done).length;
},
}));
const store = TodoStore.create({ todos: [] });
store.add("Learn MST");
console.log(store.remaining); // 1
mobx-react
For class components or older React versions, the mobx-react package provides observer, Provider, and inject. Modern apps generally prefer mobx-react-lite, but mobx-react still works for both function and class components.
mobx-utils
mobx-utils adds handy utilities such as fromPromise, observableResource, now, and keepAlive. For example, fromPromise wraps a promise in an observable with state, value, and error fields.
import { fromPromise } from "mobx-utils";
import { observer } from "mobx-react-lite";
const UserProfile = observer(({ userId }) => {
const request = fromPromise(fetch(`/api/users/${userId}`).then((r) => r.json()));
if (request.state === "pending") return <p>Loading...</p>;
if (request.state === "rejected") return <p>Error: {request.reason.message}</p>;
return <div>{request.value.name}</div>;
});
mst-react or mobx-react-form
For form-heavy applications, mobx-react-form provides field-level observables, validation, and bindings. It integrates well with both MobX and MST stores.
Best Practices
- Use classes for stores: They give you clear structure, encapsulation, and easy TypeScript typing.
- Keep actions atomic: Group related mutations inside a single action so observers receive one notification.
- Prefer computed over derived state in components: Move derived logic into store getters so it is cached and reusable.
- Avoid reading observables outside reactive contexts: Reading an observable outside an
observerorautorunwon't subscribe to changes. - Use
makeAutoObservablefor simple stores: Switch tomakeObservablewhen you need explicit control over inheritance or async generators. - Don't over-observe: Mark only the data that actually drives UI as observable. Plain constants and config can stay non-reactive.
- Dispose reactions: When using
autorun,reaction, orwhenoutside components, always call the returned disposer to prevent memory leaks. - Consider MST for complex domains: If you need snapshots, patches, or runtime type validation, MST pays for itself quickly.
When to Choose MobX
MobX shines in applications with rich, interdependent domain models โ dashboards, editors, real-time tools, and complex forms. Its automatic dependency tracking eliminates the selector boilerplate of Redux while keeping re-renders surgical. For simpler apps, React's built-in state may suffice, but as state grows in complexity, MobX's class-based stores and fine-grained reactivity scale gracefully.
Conclusion
MobX offers a pragmatic, low-ceremony approach to state management that scales from a single component's local state to a multi-store domain architecture. By combining observable state, computed derivations, and batched actions with libraries like mobx-react-lite, mobx-state-tree, and mobx-utils, you can build reactive applications that stay maintainable as they grow. The key is to model your state as plain classes, keep mutations inside actions, and let MobX handle the dependency graph โ so you can focus on your domain logic rather than wiring up re-renders by hand.