Skip to Content

Change Detection

Overview

onChangeAppState (src/state/onChangeAppState.ts) implements a side-effect system that watches for changes to specific state properties and executes handlers in response. While React integration handles UI updates, this module handles everything else — disk persistence, notifications, external service communication, and derived-state recomputation.

Change detection is separate from React’s rendering cycle. It runs synchronously after each store mutation, before any component re-renders occur.

Change Detection Pipeline

After every state mutation, the system captures a snapshot of the previous state, compares it against the current state on registered property paths, and dispatches matching handlers.

Handler Registration

Change handlers are registered for specific state paths. Each registration binds a property path to a callback:

onChangeAppState("messages", (prev, next) => { if (prev.length !== next.length) { persistMessages(next); } }); onChangeAppState("permissions.pending", (prev, next) => { if (next.length > prev.length) { notifyNewPermissionRequest(next[next.length - 1]); } });

Handlers receive the previous and current value of the watched property, enabling fine-grained comparison logic within each handler.

Diff Algorithm

Change detection uses shallow comparison on the registered property paths:

  1. Before a mutation, snapshot the current value at each registered path.
  2. After the mutation, compare each path’s new value against its snapshot.
  3. If Object.is(prev, next) returns false, the path is considered changed.
  4. Invoke all handlers registered for changed paths.

This approach is efficient for primitive values and reference-checked objects. For arrays and nested objects, handlers must perform their own deep comparison if needed.

Side Effect Categories

CategoryPurposeExample
PersistenceWrite state to disk for session recoverySave conversation history after new messages
NotificationsAlert the user of important eventsShow permission request toast
Derived StateRecompute values that depend on changed stateUpdate unread count after message changes
External SyncCommunicate with background servicesNotify agent orchestrator of task completion

Persistence Flow

Persistence is the most common side effect. Conversation history, user preferences, and task state are all persisted through this pipeline.

Debouncing

Rapid state changes (e.g., streaming tokens arriving every few milliseconds) would cause excessive side effects without debouncing:

onChangeAppState("messages", debounce((prev, next) => { persistMessages(next); }, 500));

The debounce window varies by handler:

Handler TypeDebounce WindowRationale
Persistence500msBatch rapid writes, avoid disk thrashing
Notifications100msQuick feedback, but deduplicate bursts
Derived state0ms (synchronous)Must be immediately consistent
External sync1000msNetwork calls are expensive

Error Handling

Side-effect handlers are wrapped in try-catch blocks to prevent a failing handler from breaking the notification chain:

for (const handler of matchedHandlers) { try { handler(prevValue, nextValue); } catch (error) { logError("Change handler failed", { path, error }); // Continue to next handler — do not propagate } }

A failing persistence handler means state may not be saved to disk. The system logs the error but does not retry automatically — the next successful mutation will overwrite the file with the latest state.

Design Patterns

  • Observer Pattern — Handlers observe specific state paths and react to changes.
  • Pub/Sub — The change-detection system acts as a publish-subscribe bus where state paths are topics.
  • Debounce — Rapid mutations are batched to prevent excessive side-effect execution.
  • Store Architecture — The store whose mutations trigger change detection.
  • React Integration — The parallel system that handles UI updates.
  • Selectors — Memoized computations that may be recomputed as derived-state side effects.
Last updated on