---
name: web-state-mobx
description: MobX observable state management patterns with mobx-react-lite. Use when implementing reactive client state with observables, computed values, actions, and the observer HOC.
---

# MobX State Management Patterns

> **Quick Guide:** Use MobX for complex client state needing automatic dependency tracking, computed values, and fine-grained reactivity. Use `makeAutoObservable` for stores, `observer` from `mobx-react-lite` for React components, and `runInAction`/`flow` for 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 (`useState` is 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.

```typescript
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.

```typescript
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.

```typescript
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`.

```typescript
// 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.

```typescript
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:

```typescript
// 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](examples/advanced.md#pattern-8-async-patterns-flow-and-runinaction) for complete examples.

---

### Pattern 9: Reactions (autorun, reaction, when)

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 changes
- **`reaction`**: 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](examples/advanced.md#pattern-9-reactions-autorun-reaction-when) for all three reaction types with React cleanup patterns.

---

### Pattern 10: Root Store Pattern

The root store pattern organizes multiple domain and UI stores into a single coordinator that enables cross-store communication via shared reference.

```typescript
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](examples/architecture.md#pattern-10-root-store-pattern) for full root store with domain stores, UI store, provider, and convenience hooks.

---

### Pattern 11: TypeScript Integration

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](examples/architecture.md#pattern-11-typescript-integration) for typed stores and private field examples.

---

### Pattern 12: Performance Optimization

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](examples/architecture.md#pattern-12-performance-optimization) for list rendering, `toJS`, and `configure()` examples.

</patterns>

---

**Detailed Resources:**

- [examples/core.md](examples/core.md) - Store creation, observer, useLocalObservable
- [examples/advanced.md](examples/advanced.md) - Computed values, actions, async, reactions
- [examples/architecture.md](examples/architecture.md) - Root store, TypeScript, performance

---

<decision_framework>

## Decision Framework

### makeAutoObservable vs makeObservable

```
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)
```

### Async Pattern: flow vs runInAction

```
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)
```

### Reaction Type Selection

```
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
```

### When to Use MobX vs Alternatives

```
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>

## RED FLAGS

**High Priority Issues:**

- **Mutating observables outside actions** -- breaks MobX enforceActions, causes unpredictable state updates
- **Missing `observer` wrapper on components reading observables** -- component will not re-render when state changes (most common MobX bug)
- **Not disposing reactions** -- autorun, reaction, when all return disposers that MUST be called to prevent memory leaks
- **State mutation after `await` without `runInAction`** -- code after await is NOT in the original action, will fail with enforceActions

**Medium Priority Issues:**

- Using `mobx-react` instead of `mobx-react-lite` (heavier, includes class component support you likely do not need)
- Destructuring primitives from observables outside tracked functions (breaks reactivity tracking)
- Using `makeAutoObservable` on subclassed stores (will throw -- use `makeObservable` instead)
- Creating side effects in computed values (computeds must be pure derivations)
- Overusing reactions where computed values would suffice

**Common Mistakes:**

- Reading observables in `setTimeout`/`setInterval` callbacks without proper tracking
- Passing extracted primitive values instead of observable objects to child components (breaks fine-grained tracking)
- Forgetting `autoBind: true` when passing store methods as callbacks (leads to lost `this` context)
- Using rest destructuring (`...store`) on observables (touches all properties, makes component overly reactive)
- Not using `toJS()` when passing observable data to non-MobX-aware libraries

**Gotchas and Edge Cases:**

- `observer` auto-applies `React.memo` -- never wrap an observer component in `memo` again (redundant)
- Computed values suspend when not observed -- accessing them outside reactions causes recalculation every time (use `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 tracked
- `reaction` does NOT run on initialization (unlike `autorun`) -- use `fireImmediately: true` option if needed
- Generator functions are automatically inferred as `flow` 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 overrides
- `action.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 methods
- MobX tracks property access ("arrows"), not values -- reassigning a variable that held an observable reference does NOT trigger reactions
- Reactions accept a `signal: AbortSignal` option as an alternative to manual disposer calls -- useful when tying reaction lifetime to an `AbortController`

</red_flags>

---

<critical_reminders>

## 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>
