MobX State Management Patterns
Quick Guide: Use MobX for complex client state needing automatic dependency tracking, computed values, and fine-grained reactivity. UsemakeAutoObservablefor stores,observerfrommobx-react-litefor React components, andrunInAction/flowfor async state updates. Never use MobX for server state -- use your data-fetching solution instead.
<critical_requirements>
CRITICAL: Before Using This Skill
All code must follow project conventions in CLAUDE.md (kebab-case, named exports, import ordering, import type, named constants)
(You MUST call makeAutoObservable(this) in EVERY class store constructor - or use makeObservable with explicit annotations for subclassed stores)
(You MUST wrap ALL state mutations after await in runInAction() - or use flow with generator functions instead of async/await)
(You MUST wrap EVERY React component that reads observables in observer() from mobx-react-lite)
(You MUST always dispose reactions (autorun, reaction, when) to prevent memory leaks)
</critical_requirements>
Auto-detection: MobX, makeAutoObservable, makeObservable, observable, observer, mobx-react-lite, runInAction, flow, computed, autorun, reaction, useLocalObservable
When to use:
- Complex client state with computed derivations and automatic dependency tracking
- Class-based or factory-function stores with observable properties
- Fine-grained reactivity where only affected components re-render
- State that benefits from transparent reactive programming (spreadsheet-like derivations)
When NOT to use:
- Server/API data (use your data-fetching solution)
- Simple local UI state (use
useState) - Lightweight shared state without computed needs (simpler state solutions exist)
- State that should be URL-shareable (use
searchParams)
<philosophy>
Philosophy
MobX embraces a core principle: "Anything that can be derived from the application state, should be derived. Automatically." It uses transparent reactive programming where observables track dependencies at runtime and only notify exactly the computations and components that depend on changed values.
MobX uses mutable observables with automatic tracking. This means less boilerplate than immutable/reducer-based approaches but requires understanding how reactivity works -- specifically, MobX tracks property access during tracked function execution, not variable assignments.
When to Use MobX
- Complex domain models with many derived/computed values
- Applications where class-based stores provide natural organization
- Scenarios requiring fine-grained reactivity (large lists, frequent updates)
- Teams comfortable with mutable state and OOP patterns
When NOT to Use MobX
- Server state management (use your data-fetching solution)
- Simple shared UI state without derivations (lighter alternatives exist)
- Projects preferring immutable state patterns
- Simple component-local state (
useStateis sufficient)
</philosophy>
<patterns>
Core Patterns
Pattern 1: Store Creation with makeAutoObservable
makeAutoObservable infers annotations automatically: properties become observable, getters become computed, methods become action, and generator functions become flow. It cannot be used on classes with super or that are subclassed.
class TodoStore {
todos: Todo[] = [];
filter: "active" | "completed" | "all" = "all";
constructor() {
makeAutoObservable(this); // auto-infers all annotations
}
get activeTodos(): Todo[] {
return this.todos.filter((todo) => todo.status === ACTIVE_STATUS);
}
addTodo(title: string): void {
this.todos.push({ id: crypto.randomUUID(), title, status: ACTIVE_STATUS });
}
}
Use autoBind: true option to auto-bind methods for safe callback passing. Pass overrides as second argument to exclude properties (e.g., injected dependencies) from observability.
See [examples/core.md](examples/core.md#pattern-1-store-creation-with-makeautoobservable) for complete examples with autoBind and overrides.
Pattern 2: Store Creation with makeObservable
makeObservable requires explicit annotation of each property. Required for classes using extends (inheritance) -- makeAutoObservable throws on subclasses.
class BaseEntityStore<T extends Entity> {
entities: T[] = [];
constructor() {
makeObservable(this, {
entities: observable,
entityCount: computed,
addEntity: action,
});
}
get entityCount(): number {
return this.entities.length;
}
}
See [examples/core.md](examples/core.md#pattern-2-store-creation-with-makeobservable) for base/subclass examples.
Pattern 3: Factory Function Stores
Factory functions with makeAutoObservable avoid this and new complexity, compose easily, and can hide private members via closures.
function createTimerStore(): TimerStore {
return makeAutoObservable({
secondsPassed: INITIAL_SECONDS,
get minutesPassed(): number {
return Math.floor(this.secondsPassed / SECONDS_PER_MINUTE);
},
tick(): void {
this.secondsPassed++;
},
});
}
See [examples/core.md](examples/core.md#pattern-3-factory-function-stores) for typed factory examples.
Pattern 4: React Integration with observer
The observer HOC from mobx-react-lite makes React components reactive. It automatically tracks which observables are read during render and re-renders only when those specific values change. observer auto-applies React.memo.
// observer tracks observables read during render
const TodoList = observer(function TodoList() {
return (
<ul>
{todoStore.filteredTodos.map((todo) => (
<TodoItem key={todo.id} todo={todo} /> {/* Pass objects, NOT primitives */}
))}
</ul>
);
});
Critical: Pass observable objects to child components, not destructured primitives. Extracting primitives before the observer boundary breaks fine-grained tracking.
See [examples/core.md](examples/core.md#pattern-4-react-integration-with-observer) for observer, Context-based DI, and anti-patterns.
Pattern 5: useLocalObservable for Local Component State
useLocalObservable creates a local observable store scoped to a component. Use for complex local state with computed values -- not for simple boolean toggles (useState suffices).
See [examples/core.md](examples/core.md#pattern-5-uselocalobservable-for-local-component-state) for multi-step form example.
Pattern 6: Computed Values
Computed values are derivations that automatically cache and recalculate when their dependencies change. Should be pure (no side effects). Use computed.struct for structural comparison when output shape matters more than reference.
get subtotal(): number {
return this.items.reduce((sum, item) => sum + item.price * item.quantity, 0);
}
get total(): number {
return this.subtotal + this.tax + this.shippingCost; // chains computed values
}
See [examples/advanced.md](examples/advanced.md#pattern-6-computed-values) for chained computeds and computed.struct.
Pattern 7: Actions and runInAction
Actions are the only place you should modify observable state. They batch mutations into transactions, so reactions only fire after the outermost action completes.
See [examples/advanced.md](examples/advanced.md#pattern-7-actions-and-runinaction) for batching and enforceActions examples.
Pattern 8: Async Patterns (flow and runInAction)
Code after await runs in a new tick and is NOT part of the original action. Two solutions:
// Option A: runInAction after await
async fetchUsers(): Promise<void> {
this.isLoading = true; // OK: before await
const users = await this.api.getUsers();
runInAction(() => { this.users = users; this.isLoading = false; }); // MUST wrap
}
// Option B