Skip to content

Transformer Math

Module 51 · AI Engineering

🗄️ State Management

Two state systems coexist — one triggers re-renders, one doesn't. Mix them up and the terminal freezes.

Status:

Claude Code has two separate state systems, and that's intentional. React state (AppState) drives the UI — changing it triggers re-renders. Module state (plain getters/setters) tracks things like cost and session ID without touching the screen. In a terminal app where re-rendering means redrawing the entire display, this split is a critical performance optimization.

  • React state (AppState.tsx): context provider, memoized selectors, triggers re-renders
  • Module state (bootstrap/state.ts): plain getter/setter functions, no re-renders
  • Session persistence: conversation transcript serialized to ~/.claude/projects/<slug>/<id>.jsonl
🎮

Dual State Architecture

What you are seeing

The two state systems side by side: React state on the left (changes trigger UI updates) and module state on the right (changes are invisible to the UI).

What to try

Notice which state changes would cause a re-render (settings, model, permissions) vs which would not (cost, token count, session ID).

# React State (AppState) — triggers re-renders

settings.model = "claude-sonnet-4-20250514" → UI updates model badge

permissions = "default" → UI updates permission indicator

tasks = [{id: 1, status: "running"}] → UI updates task list

mcpConnections = ["db-server"] → UI updates MCP status

# Module State (plain vars) — NO re-renders

session_id = "abc-123" → no UI change (internal bookkeeping)

total_cost += 0.003 → no UI change (tracked silently)

cwd = "/Users/me/project" → no UI change (used by tools)

token_count += 1500 → no UI change (internal counter)

# Why two systems?

cost changes ~50x per task → 50 unnecessary re-renders if in React

terminal re-render = full screen redraw = expensive

💡

The Intuition

What you’re seeing: two columns — React state (every write triggers a UI redraw) vs module state (silent writes, no redraw). What to try: follow which state types land in each column and why.

Dual State ArchitectureReact State (AppState)useState / zustand — triggers re-renderChange here → UI updates immediatelysettings.modele.g. "claude-sonnet-4-20250514"permissionse.g. "default" | "bypassPermissions"thinkingBudgete.g. 10000 tokenscurrentDirectorye.g. /home/user/projectDrives visual feedback to the user|Module State (Session)plain object — silent updatesChange here → NO re-render triggeredsessionCostaccumulated token spend (never re-renders)sessionIdUUID — set once, never changesconversationIdlinks this session to a threadtokenCountrolling count, sampled at intervalsDrives cost tracking & telemetry

Which State Belongs Where?

The rule: if the user needs to see the change immediately, it goes in React (AppState). If it's internal bookkeeping the UI doesn't display, it stays in a plain module variable.

State changeNeeds UI update?Which system?Why?
Permission mode changedYesAppState (React)User sees the change
Cost counter +$0.001NoModule stateRe-rendering for 0.1 cent is wasteful
New task createdYesAppState (React)User tracks progress
Session ID setNoModule stateInternal bookkeeping

The Problem

An AI agent tracks many values: model name, API cost, token counts, session ID, permissions, current directory, MCP connections. In a typical React app, you'd put everything in one store. But Claude Code runs in a terminal — every React re-render means redrawing the entire screen. If the cost counter (which increments on every API call) lives in React state, you get 50+ unnecessary screen redraws per task.

React State (AppState)

A context provider that wraps the React tree. Components access it via useAppState() with memoized selectors — a component that only reads settings.modelwon't re-render when permissions change. This holds UI-visible state: settings (which includes model and theme), permissions, tasks, MCP connections.

💡 Tip · The custom store uses memoized selectors instead of Redux or Zustand — it's a deliberately minimal implementation. Each component subscribes to exactly the slice it needs, preventing cascade re-renders when unrelated state changes.

Module State

Plain TypeScript variables with getter/setter functions. No subscriptions, no observers, no React integration. Read synchronously when needed: getSessionId(), getTotalCost(). This holds high-frequency bookkeeping state: session ID, cost, current working directory, model usage stats.

Session Persistence

Session transcripts are stored as JSONL under ~/.claude/projects/<slug>/<id>.jsonl(note: ~/.claude/sessions/ is used only for concurrent-session PID tracking, not transcript storage). The /resume command reconstructs both layers: messages are replayed, module state is restored, and the system prompt is rebuilt fresh (since dynamic sections like git status may have changed since the session was saved).

✨ Insight · The boundary between the two systems is clear: if a human needs to see the change, it goes in React state. If it's internal bookkeeping, it goes in module state. When in doubt, start in module state — it's cheaper. Promote to React state only when you need UI reactivity.

Async Safety: Race Conditions in Module State

Module state is plain mutable variables — there is no concurrency protection. In the agent loop, multiple async operations can run concurrently: up to 10 parallel tool calls in a batch, background sub-agents, and streaming API responses. If two concurrent tool calls both call addCost(amount) simultaneously, the reads and writes to _totalCostinterleave safely in JavaScript's single-threaded event loop — JavaScript does not have true parallelism, so there is no torn read. But this would be a critical bug in a multi-threaded language (Go, Rust, Java). The design works because Node.js is single-threaded: even though await yields to other tasks, the actual reads and writes to module variables are atomic at the JavaScript level. This is a deliberate architectural constraint — it is why the agent harness is built on Node.js rather than a multi-threaded runtime.

Coordination During Save and Resume

Session save must capture both state systems at a consistent point-in-time snapshot. If the agent is mid-turn when the user runs /resume or the process is interrupted, module state (cost, cwd) and React state (settings, permissions) may be mid-update. The save routine serializes module state first (synchronous reads), then captures React state from the provider, and finally snapshots the messages array. On resume, the order reverses: messages load first (so tools have file context), then module state restores (so cost tracking continues from where it left off), then React state hydrates (triggering a single re-render that reflects the restored settings). The system prompt is always rebuilt fresh on resume — dynamic sections like git status and current date are re-evaluated, while the static sections hit the cache exactly as in a new session.

Quick Check

Why does Claude Code use module-level variables instead of React state for cost tracking?

📐

Key Code Patterns

React State — AppState Provider

typescript
interface AppState {
  // Provider pattern — wraps React tree
  settings: Settings;    // model, theme, preferences (settings.model, settings.theme)
  permissions: string;   // 'default' | 'bypass' | 'plan' | 'acceptEdits'
  tasks: Task[];         // background sub-agent tasks
  mcpConnections: string[];  // active MCP server connections
}

// Real implementation uses useSyncExternalStore for slice-based subscriptions:
// a component only re-renders when its selected slice changes.
// Plain useContext would re-render on ANY context update — not what we want.
function useAppState<T>(selector: (state: AppState) => T): T {
  const store = useContext(AppStoreContext);
  const get = () => selector(store.getState());
  // useSyncExternalStore: re-renders only when selected value changes (Object.is)
  return useSyncExternalStore(store.subscribe, get, get);
}

// Usage in components:
const model = useAppState(s => s.settings.model);  // re-renders on model change only
const perms = useAppState(s => s.permissions);      // re-renders on perm change only

Module State — Plain Getters/Setters

typescript
// Module state — no re-renders, no subscriptions
let _sessionId: string | null = null;
let _totalCost: number = 0.0;
let _cwd: string | null = null;
const _modelUsage: Record<string, number> = {};

function getSessionId(): string | null {
  return _sessionId;
}

function setSessionId(id: string): void {
  _sessionId = id;
}

function addCost(amount: number): void {
  _totalCost += amount;  // no UI update — just bookkeeping
}

function getTotalCost(): number {
  return _totalCost;  // read synchronously when needed
}

Session Persistence

typescript
function saveSession(sessionId: string): void {
  // Serialize both state systems to disk
  const data = {
    messages: getMessages(),
    appState: {
      settings: appState.settings,
      permissions: appState.permissions,
    },
    moduleState: {
      cost: getTotalCost(),
      cwd: getCwd(),
      modelUsage: getModelUsage(),
    },
  };
  writeJson(`~/.claude/sessions/${sessionId}.json`, data);
}

function resumeSession(sessionId: string): void {
  // Reconstruct both state systems from disk
  const data = readJson(`~/.claude/sessions/${sessionId}.json`);
  setMessages(data.messages);
  restoreAppState(data.appState);
  setTotalCost(data.moduleState.cost);
  setCwd(data.moduleState.cwd);
  // System prompt rebuilt fresh — dynamic sections may have changed
}
🔧

Break It — See What Happens

Everything in React state (single store)
No memoized selectors (every component re-renders on any change)
📊

Real-World Numbers

MetricValue
React state fieldsSettings, model, permissions, tasks, MCP connections
Module state fieldssessionId, cwd, totalCost, modelUsage
Cost updates per task~50+ (one per API call)
Re-renders saved~50+ per task (by keeping cost in module state)
Session storage~/.claude/projects/<slug>/<id>.jsonl
State libraryCustom store (not Redux, not Zustand)
✨ Insight · The custom store is intentionally minimal — no middleware, no devtools, no time-travel debugging. For a terminal app with a small state surface, the overhead of a full state library (Redux: ~7KB, Zustand: ~1KB) isn't justified. The custom implementation is ~100 lines and does exactly what's needed: selective subscriptions with memoized equality checks.
🧠

Key Takeaways

What to remember for interviews

  1. 1React state (AppState) drives UI re-renders and holds user-visible data like settings, permissions, and task list; module state is plain getter/setter variables that never trigger a re-render.
  2. 2In a terminal app, every React re-render means a full screen redraw — cost tracking increments ~50 times per task, so putting it in React state would cause 50 unnecessary redraws.
  3. 3Memoized selectors ensure a component only re-renders when its specific slice of AppState changes, not on any state update anywhere in the tree.
  4. 4Session transcripts are stored as JSONL under ~/.claude/projects/<slug>/<id>.jsonl (not ~/.claude/sessions/, which is only used for concurrent-session PID tracking); on /resume, messages replay, module state restores, then React state hydrates.
  5. 5Node.js's single-threaded event loop makes plain module variables safe for concurrent tool calls — reads and writes are atomic at the JS level with no torn reads.
📚

Further Reading

  • Claude Code (source) Production implementation of the dual state architecture described in this module.
  • React Context API (useContext) React's built-in context API — used as the store transport; slice-based subscriptions are achieved via useSyncExternalStore, not useContext alone.
  • Zustand A minimal state library for React — similar memoized selector pattern, different implementation.
  • Redux Selector Pattern (Reselect) The canonical memoized selector library — the pattern behind AppState slice subscriptions that prevent unnecessary re-renders.
  • XState: State Machines for JavaScript Formal state machines for agent control flow — the alternative to ad-hoc React state for tracking agent lifecycle (idle → running → waiting → done).
  • Immer: Immutable State Made Simple Copy-on-write immutable update library — used in agent state managers to safely update deeply nested AppState without mutation bugs.
Trade-off

Claude Code tracks cost and session ID in module state (plain variables) rather than React state. What is the primary reason?

Claude Code tracks cost and session ID in module state (plain variables) rather than React state. What is the primary reason?
Derivation

What does `useAppState(s => s.settings.model)` prevent compared to subscribing to the full AppState?

What does `useAppState(s => s.settings.model)` prevent compared to subscribing to the full AppState?
Recall

When resuming a session with `/resume`, which part of state is rebuilt fresh rather than replayed from the saved transcript?

When resuming a session with `/resume`, which part of state is rebuilt fresh rather than replayed from the saved transcript?
Trade-off

A user told the agent &quot;always use tabs, not spaces&quot; early in a long session. Context compaction fires at 80% usage. What happens to this preference?

A user told the agent &quot;always use tabs, not spaces&quot; early in a long session. Context compaction fires at 80% usage. What happens to this preference?
🎯

Interview Questions

Difficulty:
Company:

Showing 4 of 4

Why would you use two state systems instead of one unified store?

★☆☆
GoogleMeta

Design a state system where some state triggers UI updates and some doesn't.

★★★
Anthropic

How would you implement session persistence and resumption for a stateful agent?

★★☆
GoogleAnthropic

How do you ensure critical state survives context window compaction without bloating the prompt?

★★★
Anthropic