-
Notifications
You must be signed in to change notification settings - Fork 0
Open
Description
Problem
`useStatePersistence` (67 lines) hardcodes knowledge of 3 specific stores (uiStore, feedStore, endpointStore), their specific state shapes, and the `PersistedState` interface. Adding a new persisted field requires editing the hook, the interface, and both save/restore logic paths.
Additional issues:
- Single shared debounce timer — a high-frequency store (feed filters) resets the timer for low-frequency ones (panel layout)
- Single monolithic localStorage key per project — a corrupt `feedFilters` corrupts `activeSection`
- No schema versioning — shape changes on deploy silently corrupt or discard state
- Restore logic calls store internals directly (`setActiveSection`, `setSidebarCollapsed`, etc.)
- Zoom persistence in uiStore bypasses the system entirely with raw localStorage calls
Proposed Interface
~20 lines wrapping Zustand's built-in `persist` middleware with a dynamic project-scoped key:
// src/shared/lib/persistence.ts
export function projectStorage<T>(baseKey: string): PersistStorage<T>
// Resolved key at runtime: ads:project:{projectId}:{baseKey}
export const globalStorage: PersistStorage<unknown>
// For non-project-scoped state (zoom, theme)Usage — adding persistence to a store is one middleware addition:
export const useFeedStore = create<FeedState>()(
persist(
(set, get) => ({ /* ...store unchanged... */ }),
{
name: 'feed-filters',
storage: projectStorage('feed-filters'),
partialize: (s) => ({ filters: s.filters }), // persist only filters
}
)
)Project switching triggers rehydration:
// In projectStore.switchProject:
await useFeedStore.persist.rehydrate()
await useUiStore.persist.rehydrate()`useStatePersistence` hook is deleted entirely.
Dependency Strategy
- Category: In-process + local-substitutable (localStorage)
- `projectStorage` reads `useProjectStore.getState().activeProjectId` at read/write time
- Each store owns its own persistence config — no central hook knows about all stores
- Zustand's `persist` middleware handles serialization, debouncing, and hydration
- Each slice gets its own localStorage key — isolation prevents cross-corruption
- `projectStore` itself uses `globalStorage` or no persistence (avoids circular dependency)
Testing Strategy
- New boundary tests: Create store with `persist` + `projectStorage`, set state, switch project ID, verify new key written. Verify `partialize` excludes ephemeral state. Verify `rehydrate()` restores correct project's state.
- Old tests to delete: Any tests for `useStatePersistence` hook (deleted entirely)
- Test environment: Standard Vitest; can use in-memory storage adapter for Zustand persist
Implementation Recommendations
- Each store should declare what to persist via `partialize` — adding a field is one line
- `projectStorage` should hide the dynamic key computation and project ID lookup
- `globalStorage` should be a thin alias for naming consistency
- Export a `rehydrateProjectStores()` helper listing all project-scoped stores, called from project switch
- The `version` option on Zustand persist handles schema migration (discard on mismatch, or use `migrate` callback for complex cases)
- Zoom persistence in uiStore migrates from raw localStorage to `globalStorage` + `partialize`
- The hook `useStatePersistence` is deleted — 67 lines removed, zero lines of orchestration code remain
Reactions are currently unavailable
Metadata
Metadata
Assignees
Labels
No labels