Claude Code for MobX: Observable State for React — Claude Skills 360 Blog
Blog / Frontend / Claude Code for MobX: Observable State for React
Frontend

Claude Code for MobX: Observable State for React

Published: March 20, 2027
Read time: 8 min read
By: Claude Skills 360

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.

Keep Reading

Frontend

Claude Code for Chart.js Advanced: Custom Plugins and Mixed Charts

Advanced Chart.js patterns with Claude Code — chart.register() for tree-shaking, mixed chart types combining bar and line, custom plugin API with beforeDraw and afterDatasetsDraw hooks, ScriptableContext for computed colors, ChartDataLabels plugin for value labels, chartjs-plugin-zoom for pan and zoom, custom gradient fills via ctx.createLinearGradient, ChartJS annotation plugin for threshold lines, streaming data with chartjs-plugin-streaming, and react-chartjs-2 with useRef and chart instance.

6 min read Jun 27, 2027
Frontend

Claude Code for Nivo: Rich SVG and Canvas Charts

Build rich data visualizations with Nivo and Claude Code — ResponsiveLine and ResponsiveBar for adaptive charts, ResponsiveHeatMap for matrix data, ResponsiveTreeMap for hierarchal data, ResponsiveSunburst for nested proportions, ResponsiveChord for relationship diagrams, ResponsiveCalendar for activity heat maps, ResponsiveNetwork for force graphs, NivoTheme for consistent styling, tooltip customization with sliceTooltip, and motion config for spring animations.

6 min read Jun 26, 2027
Frontend

Claude Code for Victory Charts: React Native and Web Charts

Build cross-platform charts with Victory and Claude Code — VictoryChart, VictoryLine, VictoryBar, and VictoryScatter for web and React Native, VictoryPie for donut charts, VictoryArea for stacked areas, VictoryAxis for custom axes, VictoryTooltip and VictoryVoronoiContainer for hover tooltips, VictoryBrushContainer for range selection, VictoryZoomContainer for pan and zoom, VictoryLegend for series labels, custom theme with VictoryTheme, and VictoryStack for grouped bars.

6 min read Jun 25, 2027

Put these ideas into practice

Claude Skills 360 gives you production-ready skills for everything in this article — and 2,350+ more. Start free or go all-in.

Back to Blog

Get 360 skills free