Soloist.v2.0.0

File Structure

State Management Architecture

Keep-mounted views with FSM orchestration for persistent UI state

Overview

Soloist uses a sophisticated state management pattern to eliminate state loss during navigation

The state management system consists of three main components working together:

  • Global Zustand Stores - Centralized state management with persistence
  • Keep-Mounted Views - Components stay alive, hidden via CSS instead of unmounting
  • View Orchestrator FSM - Finite State Machine coordinating view transitions

The Problem We Solved

Before: Conditional Rendering

{currentView === "dashboard" ? (
  <Dashboard />
) : currentView === "soloist" ? (
  <Soloist />
) : ...
}

❌ Problem: When switching views, components completely unmount and remount, losing all local state like selected date, filters, scroll position, and form inputs.

After: Keep-Mounted Pattern

<ViewsWrapper>
  <ViewContainer view="dashboard" currentView={currentView}>
    <Dashboard />
  </ViewContainer>
  <ViewContainer view="soloist" currentView={currentView}>
    <Soloist />
  </ViewContainer>
  {/* All views stay mounted, hidden with CSS */}
</ViewsWrapper>

✅ Solution: All views remain mounted in the DOM. Only visibility changes via CSS. State persists across navigation!

Global Zustand Stores

Centralized state with persistence middleware

dashboardStore.ts
Primary

Manages all dashboard UI state including date selection, filters, tags, and template UI.

// State structure
interface DashboardState {
  selectedYear: number;
  selectedDate: string;
  availableTags: Tag[];
  selectedTags: Tag[];
  selectedLegend: string | null;
  showTemplates: boolean;
  isCreatingNewTemplate: boolean;
  refreshSubscription: boolean;
}

// Usage in components
const { selectedYear, selectedDate } = useDashboardStore();
const setSelectedYear = useDashboardStore(s => s.setSelectedYear);

feedStore.ts

Manages feed view state and coordinates with dashboard for template operations.

// Coordination methods
syncDateWithDashboard(dashboardDate: string): void
setShowTemplates(show: boolean): void
setIsCreatingTemplate(creating: boolean): void

viewOrchestrator.ts
FSM

Finite State Machine that coordinates view transitions with validation and cleanup.

// FSM States
type TransitionState = "idle" | "transitioning" | "active" | "error";

// State diagram
idle → transitioning → active
  ↑         ↓
  └─────error

// Usage
const { transitionTo, canTransitionTo } = useView();

if (canTransitionTo("soloist")) {
  await transitionTo("soloist"); // Validated transition with cleanup
}

Keep-Mounted View System

Components that never unmount

ViewContainer Component

<ViewContainer view="dashboard" currentView={currentView}>
  <Dashboard />
</ViewContainer>

// Implementation
- Uses absolute positioning (inset-0)
- Toggles 'hidden' class (CSS only, no unmounting)
- data-view and data-active attributes for debugging

ViewsWrapper Component

<ViewsWrapper>
  <ViewContainer view="dashboard" currentView={currentView}>
    <Dashboard />
  </ViewContainer>
  <ViewContainer view="soloist" currentView={currentView}>
    <Soloist />
  </ViewContainer>
  <ViewContainer view="testing" currentView={currentView}>
    <TestingPage />
  </ViewContainer>
  <ViewContainer view="waypoints" currentView={currentView}>
    <WaypointsPage />
  </ViewContainer>
</ViewsWrapper>

// All 4 views mounted simultaneously
// Only visibility controlled by CSS

Benefits

  • State Persistence: Selected dates, filters, form inputs survive navigation
  • Scroll Position: Each view remembers scroll position when you return
  • Instant Transitions: No re-rendering or data fetching on view switch
  • Form State: Partially filled forms remain intact when navigating away

View Orchestrator FSM

Coordinated transitions with validation

State Machine

┌──────┐
│ idle │ ← Initial state
└──┬───┘
   │ transitionTo()
   ↓
┌──────────────┐
│ transitioning│ ← Transition in progress
└──────┬───────┘
       │ success
       ↓
   ┌────────┐
   │ active │ ← Stable state
   └────┬───┘
        │ error occurs
        ↓
    ┌───────┐
    │ error │ ← Error state
    └───┬───┘
        │ clearError()
        └─→ back to active

ViewProvider Context

// Context API
interface ViewContextValue {
  currentView: ViewType;
  transitionTo: (view: ViewType) => Promise<void>;
  canTransitionTo: (view: ViewType) => boolean;
  isTransitioning: boolean;
  lastError: string | null;
  clearError: () => void;
}

// Usage hooks
const { currentView, transitionTo } = useView();
const currentView = useCurrentView(); // selector hook

Transition Logic

  • 1.Validation: Check if transition is allowed via canTransitionTo guard
  • 2.Cleanup: Run preTransitionCleanup hook for current view
  • 3.Transition: Update state to transitioning, then to new view
  • 4.Recording: Track transition in history (last 10 transitions)
  • 5.Completion: Set state to active, ready for next transition

Convex Integration
Phase 4 - Planned

Cloud sync for cross-device state persistence

Schema Extension

// convex/schema.ts
userPreferences: defineTable({
  userId: v.string(),
  currentView: v.string(),
  dashboardPreferences: v.object({
    selectedYear: v.number(),
    selectedDate: v.string(),
    selectedTags: v.array(v.object({
      id: v.string(),
      name: v.string(),
      color: v.string(),
    })),
  }),
  lastSyncedAt: v.number(),
}).index("by_userId", ["userId"])

Sync Hook

// hooks/useViewSync.ts
- Debounced sync (300ms) to avoid excessive writes
- Bidirectional: localStorage ↔ Convex
- Load preferences on app start
- Save changes automatically
- Cross-device state consistency

Future Benefits

  • Cross-Device: Same state on desktop and mobile
  • Cloud Backup: State survives cache clear or reinstall
  • Offline First: localStorage as cache, sync when online

Implementation Phases

Complete
Phase 1: Global Stores Foundation
  • • Created dashboardStore.ts with persist middleware
  • • Enhanced feedStore.ts with template coordination
  • • Migrated 8 useState instances from dashboard page
  • • Added Tag type support with TagColors union
Complete
Phase 2: Keep-Mounted View System
  • • Created ViewContainer and ViewsWrapper components
  • • Refactored dashboard page from conditional to keep-mounted
  • • Fixed RightSidebar positioning (sibling to ViewsWrapper)
  • • All 4 views now stay mounted, CSS visibility only
Complete
Phase 3: View Orchestration FSM
  • • Created viewOrchestrator.ts FSM with devtools
  • • Built ViewProvider context with hooks
  • • Updated layout.tsx with ViewProvider wrapper
  • • Migrated sidebar to use orchestrator transitions
Planned
Phase 4: Convex Integration
  • • Extend schema with userPreferences table
  • • Create savePreferences and loadPreferences mutations
  • • Build useViewSync hook with debouncing
  • • Enable cross-device state synchronization
Planned
Phase 5: Monitoring & Validation
  • • Create ViewStateMonitor debug component
  • • Build state consistency validators
  • • Add auto-fix for common state mismatches
  • • Enhanced devtools integration

Key Files Reference

/renderer/store/dashboardStore.ts

Primary dashboard state store with persist middleware

/renderer/store/feedStore.ts

Feed view state with dashboard coordination methods

/renderer/store/viewOrchestrator.ts

FSM orchestrator with transition validation and history

/renderer/components/ViewContainer.tsx

View wrapper with keep-mounted pattern implementation

/renderer/providers/ViewProvider.tsx

Context provider with useView and useViewSafe hooks

/renderer/app/dashboard/page.tsx

Refactored dashboard using keep-mounted pattern

/renderer/app/layout.tsx

Root layout with ViewProvider integration