feat(web): add Zustand store migration strategy
- Document migration approach in ZUSTAND_MIGRATION_STRATEGY.md - Add persistWithMigration utility for future stores - Add version and migrate to authStore, library, ui, cartStore, playerStore
This commit is contained in:
parent
791eedccae
commit
75c027c5bd
7 changed files with 137 additions and 0 deletions
63
apps/web/docs/ZUSTAND_MIGRATION_STRATEGY.md
Normal file
63
apps/web/docs/ZUSTAND_MIGRATION_STRATEGY.md
Normal file
|
|
@ -0,0 +1,63 @@
|
|||
# Zustand Store Migration Strategy
|
||||
|
||||
When the persisted store schema changes, we need a migration strategy to avoid corruption or runtime errors.
|
||||
|
||||
## Approach
|
||||
|
||||
1. **Version in storage key**: Each store uses a versioned storage key (e.g. `auth-storage-v1`).
|
||||
2. **`migrate` callback**: The Zustand `persist` middleware accepts a `migrate` function that transforms old state to new schema.
|
||||
3. **Fallback reset**: If migration fails, reset to default state rather than crash.
|
||||
|
||||
## migrate Function Signature
|
||||
|
||||
```ts
|
||||
(state: unknown, version: number) => NewState
|
||||
```
|
||||
|
||||
- `state`: The raw persisted value from localStorage (may be from an older version).
|
||||
- `version`: The version number from the persisted state (0 if never set).
|
||||
|
||||
## Example
|
||||
|
||||
```ts
|
||||
persist(
|
||||
(set) => ({ ... }),
|
||||
{
|
||||
name: 'auth-storage',
|
||||
version: 1,
|
||||
migrate: (persistedState, version) => {
|
||||
if (version === 0) {
|
||||
// Old schema: { user, token, ... }
|
||||
// New schema: { isAuthenticated, isLoading, error }
|
||||
return {
|
||||
isAuthenticated: !!persistedState?.user,
|
||||
isLoading: false,
|
||||
error: null,
|
||||
};
|
||||
}
|
||||
return persistedState as AuthState;
|
||||
},
|
||||
}
|
||||
)
|
||||
```
|
||||
|
||||
## Stores Using Persist
|
||||
|
||||
- `authStore` — auth-storage
|
||||
- `library` — library-storage
|
||||
- `ui` — ui-storage
|
||||
- `cartStore` — cart-storage
|
||||
- `playerStore` — player-storage
|
||||
|
||||
## When to Bump Version
|
||||
|
||||
Bump `version` when:
|
||||
- Adding/removing persisted fields
|
||||
- Changing the shape of persisted data
|
||||
- Renaming keys
|
||||
|
||||
## Testing Migration
|
||||
|
||||
1. Manually set old state in localStorage.
|
||||
2. Reload the app.
|
||||
3. Verify the store hydrates correctly.
|
||||
|
|
@ -360,12 +360,24 @@ export const useAuthStore = create<AuthStore>()(
|
|||
),
|
||||
{
|
||||
name: 'auth-storage',
|
||||
version: 1,
|
||||
partialize: (state) => ({
|
||||
// Action 4.1.1.5: user field removed - user data managed by React Query
|
||||
isAuthenticated: state.isAuthenticated,
|
||||
// Note: Les tokens sont stockés dans TokenStorage (localStorage direct)
|
||||
// User data is now managed by React Query (useUser hook), not persisted here
|
||||
}),
|
||||
migrate: (persistedState: unknown, version: number) => {
|
||||
if (version === 0 || !persistedState) {
|
||||
const s = persistedState as { isAuthenticated?: boolean } | null;
|
||||
return {
|
||||
isAuthenticated: !!s?.isAuthenticated,
|
||||
isLoading: false,
|
||||
error: null,
|
||||
};
|
||||
}
|
||||
return persistedState as AuthState;
|
||||
},
|
||||
},
|
||||
),
|
||||
) as UseBoundStore<StoreApi<AuthStore>>;
|
||||
|
|
|
|||
|
|
@ -240,6 +240,7 @@ export const usePlayerStore = create<PlayerStore>()(
|
|||
}),
|
||||
{
|
||||
name: 'player-storage',
|
||||
version: 1,
|
||||
partialize: (state) => ({
|
||||
volume: state.volume,
|
||||
muted: state.muted,
|
||||
|
|
@ -249,6 +250,18 @@ export const usePlayerStore = create<PlayerStore>()(
|
|||
currentIndex: state.currentIndex,
|
||||
currentTrack: state.currentTrack,
|
||||
}),
|
||||
migrate: (persistedState: unknown) => {
|
||||
const s = persistedState as Partial<PlayerStore> | null;
|
||||
return {
|
||||
volume: s?.volume ?? 100,
|
||||
muted: s?.muted ?? false,
|
||||
repeat: (s?.repeat as 'off' | 'one' | 'all') ?? 'off',
|
||||
shuffle: s?.shuffle ?? false,
|
||||
queue: s?.queue ?? [],
|
||||
currentIndex: s?.currentIndex ?? -1,
|
||||
currentTrack: s?.currentTrack ?? null,
|
||||
};
|
||||
},
|
||||
},
|
||||
),
|
||||
);
|
||||
|
|
|
|||
|
|
@ -94,6 +94,11 @@ export const useCartStore = create<CartStore>()(
|
|||
}),
|
||||
{
|
||||
name: 'veza-cart-storage',
|
||||
version: 1,
|
||||
migrate: (persistedState: unknown) => {
|
||||
const s = persistedState as { items?: CartItem[] } | null;
|
||||
return { items: s?.items ?? [] };
|
||||
},
|
||||
},
|
||||
),
|
||||
);
|
||||
|
|
|
|||
|
|
@ -43,11 +43,16 @@ export const useLibraryStore = create<LibraryStore>()(
|
|||
}),
|
||||
{
|
||||
name: 'library-storage',
|
||||
version: 1,
|
||||
partialize: (state) => ({
|
||||
// FE-STATE-001: Persist filters for offline support
|
||||
// NOTE: favorites removed - React Query handles persistence
|
||||
filters: state.filters,
|
||||
}),
|
||||
migrate: (persistedState: unknown) => {
|
||||
const s = persistedState as { filters?: Record<string, string> } | null;
|
||||
return { filters: s?.filters ?? {} };
|
||||
},
|
||||
},
|
||||
),
|
||||
{
|
||||
|
|
|
|||
|
|
@ -106,11 +106,20 @@ export const useUIStore = create<UIStore>()(
|
|||
),
|
||||
{
|
||||
name: 'ui-storage',
|
||||
version: 1,
|
||||
partialize: (state) => ({
|
||||
theme: state.theme,
|
||||
language: state.language,
|
||||
sidebarOpen: state.sidebarOpen,
|
||||
}),
|
||||
migrate: (persistedState: unknown) => {
|
||||
const s = persistedState as Partial<UIStore> | null;
|
||||
return {
|
||||
theme: s?.theme ?? 'dark',
|
||||
language: s?.language ?? 'en',
|
||||
sidebarOpen: s?.sidebarOpen ?? true,
|
||||
};
|
||||
},
|
||||
},
|
||||
),
|
||||
{
|
||||
|
|
|
|||
30
apps/web/src/utils/persistWithMigration.ts
Normal file
30
apps/web/src/utils/persistWithMigration.ts
Normal file
|
|
@ -0,0 +1,30 @@
|
|||
/**
|
||||
* Helper to create persist config with version and migrate.
|
||||
* Use when adding new persisted stores to ensure future schema changes are handled.
|
||||
*
|
||||
* @example
|
||||
* ```ts
|
||||
* persist(
|
||||
* (set) => ({ ... }),
|
||||
* persistWithMigration('my-storage', 1, (state, version) => state ?? defaultState)
|
||||
* )
|
||||
* ```
|
||||
*/
|
||||
export function persistWithMigration<T>(
|
||||
name: string,
|
||||
version: number,
|
||||
migrate: (state: unknown, version: number) => T,
|
||||
partialize?: (state: T) => Partial<T>,
|
||||
): {
|
||||
name: string;
|
||||
version: number;
|
||||
migrate: (state: unknown, version: number) => T;
|
||||
partialize?: (state: T) => Partial<T>;
|
||||
} {
|
||||
return {
|
||||
name,
|
||||
version,
|
||||
migrate,
|
||||
...(partialize && { partialize }),
|
||||
};
|
||||
}
|
||||
Loading…
Reference in a new issue