92 lines
3.2 KiB
Markdown
92 lines
3.2 KiB
Markdown
|
|
# 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:
|
||
|
|
```typescript
|
||
|
|
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
|