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.tsPrimary
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.tsFSM
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 debuggingViewsWrapper 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 CSSBenefits
- ✓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 activeViewProvider 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 hookTransition 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 IntegrationPhase 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
- • 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
- • 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
- • Created viewOrchestrator.ts FSM with devtools
- • Built ViewProvider context with hooks
- • Updated layout.tsx with ViewProvider wrapper
- • Migrated sidebar to use orchestrator transitions
- • Extend schema with userPreferences table
- • Create savePreferences and loadPreferences mutations
- • Build useViewSync hook with debouncing
- • Enable cross-device state synchronization
- • Create ViewStateMonitor debug component
- • Build state consistency validators
- • Add auto-fix for common state mismatches
- • Enhanced devtools integration
Key Files Reference
/renderer/store/dashboardStore.tsPrimary dashboard state store with persist middleware
/renderer/store/feedStore.tsFeed view state with dashboard coordination methods
/renderer/store/viewOrchestrator.tsFSM orchestrator with transition validation and history
/renderer/components/ViewContainer.tsxView wrapper with keep-mounted pattern implementation
/renderer/providers/ViewProvider.tsxContext provider with useView and useViewSafe hooks
/renderer/app/dashboard/page.tsxRefactored dashboard using keep-mounted pattern
/renderer/app/layout.tsxRoot layout with ViewProvider integration