Getting started
Installation
npm install mobx mobx-react-lite
For class component support:
npm install mobx mobx-react
makeAutoObservable (Recommended)
import { makeAutoObservable } from "mobx";
class Timer {
secondsPassed = 0;
constructor() {
makeAutoObservable(this);
}
increaseTimer() {
this.secondsPassed += 1;
}
}
Automatically infers observable, computed, and action annotations.
makeObservable (Explicit)
import { makeObservable, observable, action } from "mobx";
class Todo {
id = Math.random();
title = "";
finished = false;
constructor(title) {
makeObservable(this, {
title: observable,
finished: observable,
toggle: action,
});
this.title = title;
}
toggle() {
this.finished = !this.finished;
}
}
Requires explicit annotations for each field.
TypeScript Configuration
{
"compilerOptions": {
"useDefineForClassFields": true
}
}
Required for MobX 6+ with TypeScript.
Observable State
Annotations Reference
| Annotation | Description |
|---|---|
observable |
Track state changes |
observable.ref |
Only track reassignment |
observable.shallow |
Track collection membership only |
observable.struct |
Structural comparison |
computed |
Derived value (getter) |
computed.struct |
Structural equality |
action |
Modifies state |
action.bound |
Auto-bind this |
flow |
Async generator |
Factory Function Pattern
import { makeAutoObservable } from "mobx";
function createDoubler(value) {
return makeAutoObservable({
value,
get double() {
return this.value * 2;
},
increment() {
this.value++;
},
});
}
const doubler = createDoubler(5);
doubler.increment(); // value = 6, double = 12
Plain objects can be made observable.
Observable Collections
import { observable } from "mobx";
const todos = observable.array([{ title: "Learn MobX", done: false }]);
const counts = observable.map({
apples: 5,
bananas: 3,
});
const tags = observable.set(["mobx", "react"]);
Collections have additional methods:
todos.clear();
todos.replace([...newItems]);
todos.remove(item);
observable() Factory
import { observable } from "mobx";
const state = observable({
count: 0,
get double() {
return this.count * 2;
},
increment() {
this.count++;
},
});
Creates observable proxy for plain objects.
Actions
Basic Actions
import { makeAutoObservable } from "mobx";
class Store {
count = 0;
constructor() {
makeAutoObservable(this);
}
increment() {
this.count++;
}
decrement() {
this.count--;
}
}
Actions modify observable state and batch reactions.
action.bound
import { makeObservable, observable, action } from "mobx";
class Store {
count = 0;
constructor() {
makeObservable(this, {
count: observable,
increment: action.bound,
});
}
increment() {
this.count++;
}
}
const store = new Store();
const handler = store.increment;
handler(); // this is bound correctly
Auto-binds this to the instance.
runInAction (Inline Actions)
import { observable, runInAction } from "mobx";
const state = observable({ count: 0 });
runInAction(() => {
state.count++;
state.count++;
});
For immediate one-off state modifications.
Async with runInAction
class Store {
state = "pending";
projects = [];
async fetchProjects() {
this.state = "pending";
try {
const projects = await fetch("/api/projects");
runInAction(() => {
this.projects = projects;
this.state = "done";
});
} catch (e) {
runInAction(() => {
this.state = "error";
});
}
}
}
Wrap state updates after await.
Async with flow (Recommended)
import { flow, makeAutoObservable } from "mobx";
class Store {
projects = [];
state = "pending";
constructor() {
makeAutoObservable(this, {
fetchProjects: flow,
});
}
*fetchProjects() {
this.state = "pending";
try {
const projects = yield fetch("/api/projects");
this.state = "done";
this.projects = projects;
} catch (error) {
this.state = "error";
}
}
}
Generator function, use yield instead of await.
Computed Values
Basic Computed
import { makeAutoObservable } from "mobx";
class TodoList {
todos = [];
constructor() {
makeAutoObservable(this);
}
get unfinishedCount() {
return this.todos.filter((t) => !t.finished).length;
}
}
Computed values are cached and lazy.
Computed with Setter
class Person {
firstName = "";
lastName = "";
constructor() {
makeAutoObservable(this);
}
get fullName() {
return `${this.firstName} ${this.lastName}`;
}
set fullName(value) {
const [firstName, lastName] = value.split(" ");
this.firstName = firstName;
this.lastName = lastName;
}
}
Computed can have setters for two-way binding.
Computed Options
import { computed, makeObservable } from "mobx";
class Store {
constructor() {
makeObservable(this, {
total: computed.struct,
});
}
get total() {
return { sum: this.items.reduce((a, b) => a + b, 0) };
}
}
computed.struct uses structural equality.
Reactions
autorun
import { autorun, makeAutoObservable } from "mobx";
class Store {
count = 0;
constructor() {
makeAutoObservable(this);
}
}
const store = new Store();
const dispose = autorun(() => {
console.log(`Count: ${store.count}`);
});
// Later: cleanup
dispose();
Runs immediately, re-runs when observables change.
reaction
import { reaction } from "mobx";
const dispose = reaction(
() => store.count,
(count, previousCount) => {
console.log(`Changed from ${previousCount} to ${count}`);
},
);
Fine-grained control over what triggers the effect.
when
import { when } from "mobx";
// With callback
const dispose = when(
() => store.count > 10,
() => console.log("Count exceeded 10!"),
);
// As Promise
await when(() => store.count > 10);
console.log("Count exceeded 10!");
Runs once when predicate becomes true.
Reaction Options
import { autorun } from "mobx";
autorun(
() => {
console.log(store.count);
},
{
delay: 300,
name: "CountLogger",
onError: (error) => console.error(error),
},
);
Common options: delay, name, onError.
React Integration
observer HOC
import { observer } from "mobx-react-lite";
const TimerView = observer(({ timer }) => (
<span>Seconds passed: {timer.secondsPassed}</span>
));
Makes component reactive, auto-applies React.memo.
useLocalObservable
import { observer } from "mobx-react-lite";
import { useLocalObservable } from "mobx-react-lite";
const Counter = observer(() => {
const counter = useLocalObservable(() => ({
count: 0,
increment() {
this.count++;
},
}));
return <button onClick={counter.increment}>Count: {counter.count}</button>;
});
Local observable state in functional components.
Observer Component (Inline)
import { Observer } from "mobx-react-lite";
function App() {
return (
<div onClick={() => store.count++}>
<Observer>{() => <span>{store.count}</span>}</Observer>
</div>
);
}
For observing in callbacks or limited scope.
Context Pattern
import React from "react";
import { observer } from "mobx-react-lite";
const StoreContext = React.createContext(null);
export const StoreProvider = ({ children }) => {
const store = React.useMemo(() => new RootStore(), []);
return (
<StoreContext.Provider value={store}>{children}</StoreContext.Provider>
);
};
export const useStore = () => React.useContext(StoreContext);
const Component = observer(() => {
const store = useStore();
return <div>{store.count}</div>;
});
Recommended pattern for distributing stores.
SSR Configuration
import { enableStaticRendering } from "mobx-react-lite";
enableStaticRendering(typeof window === "undefined");
Call once in server-side entry point.
Patterns
RootStore Pattern
class RootStore {
constructor() {
this.userStore = new UserStore(this);
this.todoStore = new TodoStore(this);
}
}
class TodoStore {
constructor(rootStore) {
this.rootStore = rootStore;
makeAutoObservable(this);
}
get currentUser() {
return this.rootStore.userStore.currentUser;
}
}
Central store with cross-store references.
Configuration
import { configure } from "mobx";
configure({
enforceActions: "always",
computedRequiresReaction: true,
reactionRequiresObservable: true,
observableRequiresReaction: true,
disableErrorBoundaries: false,
});
Strict mode for development.
Best Practices
- Use
makeAutoObservablefor new code - Wrap async updates with
runInActionorflow - Dispose reactions to prevent memory leaks
- Use
observeron all components reading observables - Enable strict mode during development
- Prefer
flowoverasync/awaitfor async actions
Domain Store Example
class TodoStore {
todos = [];
constructor() {
makeAutoObservable(this, {
fetchTodos: flow,
});
}
get completedCount() {
return this.todos.filter((t) => t.done).length;
}
addTodo(title) {
this.todos.push({ title, done: false });
}
*fetchTodos() {
this.todos = yield fetchTodosFromAPI();
}
}
Observable state + computed + actions.
Debugging
import { trace, spy } from "mobx";
// In computed or reaction
computed(() => {
trace();
return store.total;
});
// Global spy
spy((event) => {
console.log(event);
});
Use trace() to debug why computed/reaction runs.
Also see
- MobX Documentation - Official docs
- MobX GitHub - Source code and issues
- mobx-react-lite - Lightweight React bindings
- MobX-State-Tree - Opinionated MobX-powered state container