tftsr-devops_investigation/docs/architecture/adrs/ADR-006-zustand-state-management.md
Shaun Arman fdb4fc03b9 docs(architecture): add C4 diagrams, ADRs, and architecture overview
Comprehensive architecture documentation covering:

- docs/architecture/README.md: Full C4 model diagrams (system context,
  container, component), data flow sequences, security architecture,
  AI provider class diagram, CI/CD pipeline, and deployment diagrams.
  All diagrams use Mermaid for version-controlled diagram-as-code.

- docs/architecture/adrs/ADR-001: Tauri vs Electron decision rationale
- docs/architecture/adrs/ADR-002: SQLCipher encryption choices and
  cipher_page_size=16384 rationale for Apple Silicon
- docs/architecture/adrs/ADR-003: Provider trait + factory pattern
- docs/architecture/adrs/ADR-004: Regex + Aho-Corasick PII detection
- docs/architecture/adrs/ADR-005: Auto-generate encryption keys at
  runtime (documents the fix from PR #24)
- docs/architecture/adrs/ADR-006: Zustand state management rationale

- docs/wiki/Architecture.md: Updated module table (14 migrations, not
  10), corrected integrations description, updated startup sequence to
  reflect key auto-generation, added links to new ADR docs.

- README.md: Fixed stale database paths (tftsr → trcaa) and updated
  env var descriptions to reflect auto-generation behavior.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-07 09:35:35 -05:00

3.2 KiB

ADR-006: Zustand for Frontend State Management

Status: Accepted Date: 2025-Q3 Deciders: sarman


Context

The React frontend manages three distinct categories of state:

  1. Ephemeral session state: Current issue, AI chat messages, PII spans, 5-whys progress — exists for the duration of one triage session, should not survive page reload
  2. Persisted settings: Theme, active AI provider, PII pattern toggles — should survive app restart, stored locally
  3. Cached server data: Issue history, search results — loaded from DB on demand, invalidated on changes

Decision

Use Zustand for all three state categories, with selective persistence via localStorage for settings only.


Rationale

Alternatives considered:

Option Pros Cons
Zustand (chosen) Minimal boilerplate, built-in persist middleware, TypeScript-first Smaller ecosystem than Redux
Redux Toolkit Battle-tested, DevTools support Verbose boilerplate for simple state
React Context No dependency Performance issues with frequent updates (chat messages)
Jotai Atomic state, minimal Less familiar pattern
TanStack Query Excellent for async server state Overkill for Tauri IPC (not HTTP)

Store architecture decisions:

sessionStore — NOT persisted:

  • Chat messages accumulate quickly; persisting would bloat localStorage
  • Session is per-issue; loading a different issue should reset all session state
  • reset() method called on navigation away from triage

settingsStore — Persisted to localStorage as "tftsr-settings":

  • Theme, active provider, PII pattern toggles — user preference, should survive restart
  • AI providers themselves are NOT persisted here — only active_provider string
  • Actual ProviderConfig (with encrypted API keys) lives in the backend DB, loaded via load_ai_providers()

historyStore — NOT persisted (server-cache pattern):

  • Always loaded fresh from DB on History page mount
  • Search results replaced on each query
  • No stale-data risk

Persistence Details

The settings store persists to localStorage:

persist(
  (set, get) => ({ ...storeImpl }),
  {
    name: 'tftsr-settings',
    partialize: (state) => ({
      theme: state.theme,
      active_provider: state.active_provider,
      pii_enabled_patterns: state.pii_enabled_patterns,
      // NOTE: ai_providers excluded — stored in encrypted backend DB
    })
  }
)

Why localStorage and not a Tauri store plugin:

  • Settings are non-sensitive (theme, provider name, pattern toggles)
  • tauri-plugin-store would add IPC overhead for every settings read
  • localStorage survives across WebView reloads without async overhead

Consequences

Positive:

  • Minimal boilerplate — stores are ~50 LOC each
  • zustand/middleware/persist handles localStorage serialization
  • Subscribing to partial state prevents unnecessary re-renders
  • No Provider wrapping required — stores accessed via hooks anywhere

Negative:

  • No Redux DevTools integration (Zustand has its own devtools but less mature)
  • localStorage persistence means settings are WebView-profile-scoped (fine for single-user app)
  • Manual cache invalidation in historyStore after issue create/delete