Skip to content

ADR-26: Zustand Client State Management ​

🇰🇷 한국어 버전

DateAuthorRepos
2026-01-23@KubrickCodeweb

Context ​

The Specvital web platform requires ephemeral client-side state that differs from existing state management solutions:

State CategoryManaged ByExamples
Server StateTanStack QueryRepository data, analysis results, user profile
URL StatenuqsDashboard filters, search queries, view mode
Client StateNeededBackground task tracking, session ephemeral data

Problem ​

Background operations (analysis, spec generation) had critical limitations with modal-based UI:

  1. State lost when modals closed
  2. Polling stopped on page navigation
  3. No cross-page visibility of running tasks

User quote from Issue #240: "Users cannot track spec generation progress after closing modals or navigating away."

Constraints ​

  • App Router RSC Boundary: State management must respect client/server component boundary
  • Hydration Mismatch Risk: SSR + client state requires careful handling
  • Bundle Size Sensitivity: Dashboard performance is critical
  • Existing Stack Integration: Must coexist with TanStack Query and nuqs

Decision ​

Adopt Zustand for global client-side state management.

Rationale ​

  1. Completes state management trifecta: TanStack Query (server), nuqs (URL), Zustand (client)
  2. Minimal bundle impact (~3 KB gzipped)
  3. No Provider wrapper required
  4. Built-in middleware: persist, devtools
  5. Native useSyncExternalStore for React 18+ optimization
  6. TypeScript-first with full type inference

Options Considered ​

Option A: Zustand (Selected) ​

Characteristics:

  • ~3 KB gzipped bundle size
  • No Provider wrapper required
  • Hooks-based API with selective subscriptions

Strengths:

  • Zero boilerplate, immediate productivity
  • Works anywhere in component tree without setup
  • Redux DevTools compatible
  • Built-in persist middleware for storage sync

Weaknesses:

  • Less structured than Redux for very large codebases
  • Team must establish conventions (no enforced patterns)

Option B: Redux Toolkit ​

Characteristics:

  • ~10-15 KB gzipped
  • Provider wrapper required
  • RTK Query for server state

Evaluation: Rejected. RTK Query overlaps with existing TanStack Query. Additional boilerplate and provider requirements not justified for remaining client state needs.

Option C: React Context + useReducer ​

Characteristics:

  • 0 KB (built-in)
  • Provider wrapper required
  • Manual optimization needed

Evaluation: Rejected. Re-render issues with frequently-changing state. Context is designed for low-frequency updates, not dynamic state like task tracking.

Option D: Jotai (Atomic State) ​

Characteristics:

  • ~3.5 KB gzipped
  • Atom-based composition
  • Provider optional but recommended

Evaluation: Viable but not selected. Atom-based model requires different mental model. Zustand's store-based approach is more intuitive for simple task tracking needs.

Implementation ​

Store Structure ​

typescript
type TaskStoreState = {
  tasks: Map<string, BackgroundTask>;
};

type TaskStoreActions = {
  addTask: (task: Omit<BackgroundTask, "createdAt">) => void;
  updateTask: (id: string, updates: Partial<BackgroundTask>) => void;
  removeTask: (id: string) => void;
  clearCompletedTasks: () => void;
};

Key Files ​

FilePurpose
lib/background-tasks/task-store.tsMain Zustand store
lib/background-tasks/hooks.tsCustom hooks for consumers
lib/background-tasks/task-store.spec.tsUnit tests
lib/background-tasks/components/task-badge.tsxBadge with active task count

Patterns Used ​

PatternImplementation
Singleton StoreSingle useTaskStore with create()
Derived SelectorsuseActiveTasks computes filtered results
Shallow ComparisonuseShallow prevents unnecessary re-renders
Outside React AccessuseTaskStore.getState() for non-component usage
Manual PersistenceCustom sessionStorage sync (not persist middleware)

Persistence ​

Custom sessionStorage implementation instead of Zustand's persist middleware:

  • Storage key: specvital:background-tasks
  • SSR-safe with typeof window === "undefined" check
  • Silent error handling for corrupted storage or quota exceeded

Consequences ​

Positive ​

  • Minimal Integration Friction: No Provider needed, works immediately
  • Developer Experience: Single file per store with collocated state + actions
  • Performance: Selective subscriptions prevent unnecessary re-renders
  • Debugging: Redux DevTools compatibility

Negative ​

  • Team Consistency Risk: No enforced patterns
    • Mitigation: Document store organization conventions, code review enforcement
  • Limited Middleware Ecosystem: Fewer community middlewares than Redux
    • Mitigation: Core needs (persist, devtools) are covered
  • Next.js SSR Considerations: Store is module-level
    • Mitigation: Follow official Zustand Next.js guide

References ​

Internal ​

External ​

  • 8664cbc: feat(background-tasks): add global task store with persistence
  • ecb4434: feat(background-tasks): integrate Account Badge and Tasks Dropdown
  • fc99ce5: feat(background-tasks): add Dashboard active tasks section
  • 2e1e6df: refactor(dashboard): migrate use-reanalyze hook to TanStack Query polling

Open-source test coverage insights