src/skills/web-state-mobx/SKILL.md
MobX observable state management patterns with mobx-react-lite. Use when implementing reactive client state with observables, computed values, actions, and the observer HOC.
npx skillsauth add agents-inc/skills web-state-mobxInstall this skill globally with one command. Works with Claude Code, Cursor, and Windsurf.
3 of 9 scanners reported clean
Some scanners were skipped, did not run, or reported a non-clean status. Review each row below.
Quick Guide: Use MobX for complex client state needing automatic dependency tracking, computed values, and fine-grained reactivity. Use
makeAutoObservablefor 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>
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:
When NOT to use:
useState)searchParams)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.
useState is sufficient)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 for complete examples with autoBind and overrides.
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 for base/subclass examples.
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 for typed factory examples.
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 for observer, Context-based DI, and anti-patterns.
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 for multi-step form example.
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 for chained computeds and computed.struct.
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 for batching and enforceActions examples.
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 (recommended): flow with generators - no wrapping needed
*fetchUsers() {
this.isLoading = true;
this.users = yield this.api.getUsers(); // auto-wrapped in action context
this.isLoading = false;
}
flow returns a cancellable promise with .cancel(). makeAutoObservable auto-infers generators as flow. Use flowResult() to cast the generator return for TypeScript type inference; import CancellablePromise from "mobx" for the return type.
See examples/advanced.md for complete examples.
Reactions bridge reactive MobX state to imperative side effects. ALL reactions return a disposer -- you MUST call it to prevent memory leaks.
autorun: Runs immediately and re-runs whenever any read observable changesreaction: Data function + effect function -- effect only runs when data function return value changes (not on init)when: Runs once when predicate becomes true, then auto-disposes. Without an effect function, returns a Promise.Reactions only track observables read synchronously -- not in setTimeout, promises, or after await.
See examples/advanced.md for all three reaction types with React cleanup patterns.
The root store pattern organizes multiple domain and UI stores into a single coordinator that enables cross-store communication via shared reference.
class RootStore {
userStore: UserStore;
todoStore: TodoStore;
constructor(transportLayer: TransportLayer) {
this.userStore = new UserStore(this, transportLayer);
this.todoStore = new TodoStore(this, transportLayer);
}
}
Provide the root store via React Context (dependency injection, NOT state management).
See examples/architecture.md for full root store with domain stores, UI store, provider, and convenience hooks.
MobX has first-class TypeScript support. Use makeAutoObservable<Store, "privateField"> to annotate private fields. Class stores get type inference automatically; factory functions should return typed interfaces.
See examples/architecture.md for typed stores and private field examples.
MobX provides fine-grained reactivity, but component structure matters. Use many small observer components and dereference observables as late as possible (pass objects, not extracted primitives).
See examples/architecture.md for list rendering, toJS, and configure() examples.
Detailed Resources:
<decision_framework>
Does the store class use inheritance (extends)?
|-- YES --> makeObservable (explicit annotations required)
|-- NO --> Is the store subclassed by other stores?
|-- YES --> makeObservable (makeAutoObservable forbids subclassing)
|-- NO --> makeAutoObservable (less boilerplate, auto-inference)
Is the async operation complex with multiple yields?
|-- YES --> flow (generator function, cancellable, cleaner)
|-- NO --> Is cancellation needed?
|-- YES --> flow (returns promise with .cancel())
|-- NO --> runInAction (simpler for single await)
Need to run effect immediately and on every change?
|-- YES --> autorun
|-- NO --> Need to run effect only when specific data changes?
|-- YES --> reaction (data function + effect function)
|-- NO --> Need to run effect once when condition is true?
|-- YES --> when
|-- NO --> Reconsider if you need a reaction at all
Is it server data (from API)?
|-- YES --> Not MobX's scope. Use your data-fetching solution.
|-- NO --> Is it simple local UI state (one component)?
|-- YES --> useState
|-- NO --> Does it need computed/derived values?
|-- YES --> Do you prefer OOP / class-based stores?
| |-- YES --> MobX
| |-- NO --> Consider your state management solution's derived selectors
|-- NO --> Is it lightweight shared state?
|-- YES --> A simpler state solution may suffice
|-- NO --> MobX (fine-grained reactivity scales well)
</decision_framework>
<red_flags>
High Priority Issues:
observer wrapper on components reading observables -- component will not re-render when state changes (most common MobX bug)await without runInAction -- code after await is NOT in the original action, will fail with enforceActionsMedium Priority Issues:
mobx-react instead of mobx-react-lite (heavier, includes class component support you likely do not need)makeAutoObservable on subclassed stores (will throw -- use makeObservable instead)Common Mistakes:
setTimeout/setInterval callbacks without proper trackingautoBind: true when passing store methods as callbacks (leads to lost this context)...store) on observables (touches all properties, makes component overly reactive)toJS() when passing observable data to non-MobX-aware librariesGotchas and Edge Cases:
observer auto-applies React.memo -- never wrap an observer component in memo again (redundant)keepAlive option if needed, but watch for memory leaks)autorun tracks only synchronous reads -- observables read in async callbacks, promises, or after await are NOT trackedreaction does NOT run on initialization (unlike autorun) -- use fireImmediately: true option if neededflow by makeAutoObservable -- do not also wrap them in flow(). However, some transpiler configurations cannot detect generators; if flow does not work as expected, specify flow explicitly in overridesaction.bound and autoBind: true are NOT the same as arrow function class fields -- arrow functions cannot be overridden in subclasses. flow.bound works the same way for generator methodssignal: AbortSignal option as an alternative to manual disposer calls -- useful when tying reaction lifetime to an AbortController</red_flags>
<critical_reminders>
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)
Failure to follow these rules will break MobX reactivity, cause memory leaks, or produce stale data.
</critical_reminders>
development
Material Design component library for Vue 3
development
VitePress 1.x — Vue-powered static site generator for documentation sites, built on Vite
tools
Docusaurus 3.x documentation framework — site configuration, docs/blog plugins, sidebars, versioning, MDX, swizzling, and deployment
development
TanStack Form patterns - useForm, form.Field, validators, arrays, linked fields, createFormHook, type safety