MobX makes application state observable — any component that reads observable data automatically re-renders when that data changes. makeObservable(this, { field: observable, computed: computed, method: action }) annotates a class. makeAutoObservable(this) infers all annotations automatically. observer(Component) makes a React component reactive to observable reads. computed values are memoized and only recompute when dependencies change. action wraps mutations to batch updates and enforce strict mode. runInAction applies synchronous mutations inside async functions. flow is the MobX-native async generator pattern. autorun(fn) runs a side effect whenever observables inside change. reaction(data, effect) separates dependency tracking from the side effect. useLocalObservable creates component-local observable state. Claude Code generates MobX store classes, observer React components, computed values, async flow actions, and reaction-based side effects for complex reactive state.
CLAUDE.md for MobX
## MobX Stack
- Version: mobx >= 6.13, mobx-react-lite >= 3.4
- Store: makeAutoObservable(this) in constructor — auto-annotates
- React: observer(Component) wrapper + useLocalObservable for local state
- Async: flow(function* () { this.data = yield fetch(...) }) or runInAction
- Computed: get total() { return this.items.reduce(...) } — auto-memoized
- Side effects: autorun(() => {...}) for immediate | reaction(() => data, effect) for targeted
- Strict: configure({ enforceActions: "always" }) — mutations only in actions
- DI: React.createContext(new Store()) — inject via useStore() hook
Store Classes
// stores/order-store.ts — MobX observable store
import { makeAutoObservable, runInAction, flow, computed } from "mobx"
interface Order {
id: string
customerId: string
status: "pending" | "processing" | "shipped" | "delivered" | "cancelled"
totalCents: number
items: OrderItem[]
createdAt: string
}
interface OrderItem {
productId: string
name: string
priceCents: number
quantity: number
}
type OrderFilter = "all" | "pending" | "processing" | "shipped" | "delivered"
export class OrderStore {
orders: Order[] = []
filter: OrderFilter = "all"
searchQuery = ""
isLoading = false
error: string | null = null
selectedOrderId: string | null = null
constructor() {
makeAutoObservable(this) // Auto-annotates all fields and methods
}
// Computed — memoized, recalculates only when orders/filter/searchQuery change
get filteredOrders(): Order[] {
let result = this.orders
if (this.filter !== "all") {
result = result.filter(o => o.status === this.filter)
}
if (this.searchQuery) {
const q = this.searchQuery.toLowerCase()
result = result.filter(o =>
o.id.includes(q) || o.customerId.toLowerCase().includes(q)
)
}
return result
}
get selectedOrder(): Order | null {
if (!this.selectedOrderId) return null
return this.orders.find(o => o.id === this.selectedOrderId) ?? null
}
get totalRevenue(): number {
return this.orders
.filter(o => o.status !== "cancelled")
.reduce((sum, o) => sum + o.totalCents, 0)
}
get ordersByStatus(): Record<string, number> {
const counts: Record<string, number> = {}
for (const order of this.orders) {
counts[order.status] = (counts[order.status] ?? 0) + 1
}
return counts
}
// Actions — sync mutations
setFilter(filter: OrderFilter) {
this.filter = filter
this.selectedOrderId = null
}
setSearchQuery(query: string) {
this.searchQuery = query
}
selectOrder(id: string | null) {
this.selectedOrderId = id
}
updateOrderStatus(orderId: string, status: Order["status"]) {
const order = this.orders.find(o => o.id === orderId)
if (order) order.status = status
}
// Async action using flow (MobX generator pattern)
loadOrders = flow(function* (this: OrderStore) {
this.isLoading = true
this.error = null
try {
const response: Response = yield fetch("/api/orders")
const data: { orders: Order[] } = yield response.json()
this.orders = data.orders
} catch (e) {
this.error = e instanceof Error ? e.message : "Failed to load orders"
} finally {
this.isLoading = false
}
})
// Async with runInAction alternative (for non-generator style)
async cancelOrder(orderId: string) {
try {
await fetch(`/api/orders/${orderId}/cancel`, { method: "POST" })
runInAction(() => {
const order = this.orders.find(o => o.id === orderId)
if (order) order.status = "cancelled"
})
} catch (e) {
runInAction(() => {
this.error = e instanceof Error ? e.message : "Failed to cancel order"
})
}
}
}
// Cart store — separate concern
export class CartStore {
items: Map<string, { productId: string; name: string; priceCents: number; quantity: number }> = new Map()
constructor() {
makeAutoObservable(this)
}
get totalItems(): number {
let total = 0
this.items.forEach(item => { total += item.quantity })
return total
}
get totalCents(): number {
let total = 0
this.items.forEach(item => { total += item.priceCents * item.quantity })
return total
}
addItem(product: { id: string; name: string; priceCents: number }) {
const existing = this.items.get(product.id)
if (existing) {
existing.quantity++
} else {
this.items.set(product.id, {
productId: product.id,
name: product.name,
priceCents: product.priceCents,
quantity: 1,
})
}
}
removeItem(productId: string) {
this.items.delete(productId)
}
clear() {
this.items.clear()
}
}
React Integration
// stores/root-store.ts — DI via Context
import { createContext, useContext } from "react"
import { OrderStore } from "./order-store"
import { CartStore } from "./order-store"
class RootStore {
orders = new OrderStore()
cart = new CartStore()
}
const StoreContext = createContext<RootStore | null>(null)
const rootStore = new RootStore()
export function StoreProvider({ children }: { children: React.ReactNode }) {
return <StoreContext.Provider value={rootStore}>{children}</StoreContext.Provider>
}
export function useRootStore(): RootStore {
const store = useContext(StoreContext)
if (!store) throw new Error("useRootStore must be used within StoreProvider")
return store
}
export const useOrderStore = () => useRootStore().orders
export const useCartStore = () => useRootStore().cart
// components/OrdersList.tsx — observer component
import { observer } from "mobx-react-lite"
import { useEffect } from "react"
import { useOrderStore } from "@/stores/root-store"
export const OrdersList = observer(function OrdersList() {
const orderStore = useOrderStore()
useEffect(() => {
orderStore.loadOrders()
}, [orderStore])
if (orderStore.isLoading) return <div>Loading orders...</div>
if (orderStore.error) return <div className="text-red-500">{orderStore.error}</div>
return (
<div className="space-y-4">
<div className="flex gap-2">
<input
value={orderStore.searchQuery}
onChange={e => orderStore.setSearchQuery(e.target.value)}
placeholder="Search orders..."
className="border rounded px-3 py-2 text-sm flex-1"
/>
{(["all", "pending", "processing", "shipped"] as const).map(f => (
<button
key={f}
onClick={() => orderStore.setFilter(f)}
className={`px-3 py-1 text-sm rounded ${
orderStore.filter === f ? "bg-primary text-white" : "border"
}`}
>
{f}
</button>
))}
</div>
<div className="text-sm text-muted-foreground">
{orderStore.filteredOrders.length} orders · ${(orderStore.totalRevenue / 100).toFixed(2)} revenue
</div>
{orderStore.filteredOrders.map(order => (
<div
key={order.id}
onClick={() => orderStore.selectOrder(order.id)}
className="border rounded-lg p-4 cursor-pointer hover:bg-accent"
>
<div className="flex justify-between">
<span className="font-mono text-sm">#{order.id.slice(-8)}</span>
<span className="text-sm font-medium">${(order.totalCents / 100).toFixed(2)}</span>
</div>
<span className="text-xs text-muted-foreground">{order.status}</span>
</div>
))}
</div>
)
})
autorun and reaction
// lib/store-effects.ts — reactions for side effects
import { autorun, reaction, when } from "mobx"
import type { OrderStore } from "@/stores/order-store"
export function setupStoreEffects(orderStore: OrderStore) {
// autorun: runs immediately, then on any observable change inside
const disposePersist = autorun(() => {
// Runs whenever filter or searchQuery changes — persist to URL/localStorage
const params = new URLSearchParams()
if (orderStore.filter !== "all") params.set("status", orderStore.filter)
if (orderStore.searchQuery) params.set("q", orderStore.searchQuery)
window.history.replaceState(null, "", `?${params}`)
})
// reaction: separate what to track from the effect
const disposeErrorLog = reaction(
() => orderStore.error, // Only re-runs when error changes
error => {
if (error) console.error("[OrderStore]", error)
}
)
// when: one-time reaction when condition becomes true
when(
() => orderStore.orders.length > 0,
() => console.log(`Loaded ${orderStore.orders.length} orders`)
)
// Return cleanup
return () => {
disposePersist()
disposeErrorLog()
}
}
For the Zustand alternative when a simpler non-class store with hooks-first API is preferred — Zustand avoids class syntax and decorators, using plain function stores with set() for updates, and is generally considered easier to type with TypeScript, see the Zustand Advanced guide. For the Jotai alternative when fine-grained atom-level subscriptions are needed without a single class store — Jotai atoms subscribe components at the individual atom level, which scales better for very large apps where many independent state pieces exist, see the Jotai guide. The Claude Skills 360 bundle includes MobX skill sets covering stores, observer components, and async flows. Start with the free tier to try MobX store generation.