[FE-STATE-009] fe-state: Add state normalization
- Created state normalization utility (stateNormalization.ts) with functions: * normalize/denormalize for converting arrays to normalized state * addToNormalized, updateInNormalized, removeFromNormalized * Helper functions for working with normalized state - Applied normalization to LibraryStore (items and favorites) - Updated storeSelectors to convert normalized state to arrays - Updated DashboardPage components to use new selectors - Updated tests to work with normalized state structure - Improved performance with O(1) lookups instead of O(n) array searches
This commit is contained in:
parent
aab04776ba
commit
b93a5ca149
7 changed files with 369 additions and 57 deletions
|
|
@ -8986,7 +8986,7 @@
|
|||
"description": "Normalize nested state structures for better performance",
|
||||
"owner": "frontend",
|
||||
"estimated_hours": 6,
|
||||
"status": "todo",
|
||||
"status": "completed",
|
||||
"files_involved": [],
|
||||
"implementation_steps": [
|
||||
{
|
||||
|
|
@ -9007,7 +9007,8 @@
|
|||
"Unit tests",
|
||||
"Integration tests"
|
||||
],
|
||||
"notes": ""
|
||||
"notes": "Implemented state normalization utility and applied to LibraryStore. Created normalization utilities (normalize, denormalize, addToNormalized, updateInNormalized, removeFromNormalized, etc.) and updated LibraryStore to use normalized state structure (byId + allIds pattern) for better performance. Updated selectors to convert normalized state to arrays for compatibility. Updated tests and components to use new selectors.",
|
||||
"completed_at": "2025-12-25T14:10:10.444700Z"
|
||||
},
|
||||
{
|
||||
"id": "FE-STATE-010",
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
import { useEffect } from 'react';
|
||||
import { useAuthStore } from '@/stores/auth';
|
||||
import { useLibraryStore } from '@/stores/library';
|
||||
import { useLibraryItems, useLibraryActions, useLibraryStatus } from '@/utils/storeSelectors';
|
||||
import {
|
||||
Card,
|
||||
CardContent,
|
||||
|
|
@ -13,7 +13,10 @@ import { Music, MessageSquare, Library, Users, Heart } from 'lucide-react';
|
|||
|
||||
function DashboardPage() {
|
||||
const { user } = useAuthStore();
|
||||
const { items, fetchItems, isLoading } = useLibraryStore();
|
||||
// FE-STATE-009: Use selector that returns denormalized array
|
||||
const items = useLibraryItems();
|
||||
const { fetchItems } = useLibraryActions();
|
||||
const { isLoading } = useLibraryStatus();
|
||||
|
||||
useEffect(() => {
|
||||
fetchItems({ limit: 5 });
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
import { useEffect } from 'react';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import { useAuthStore } from '@/stores/auth';
|
||||
import { useLibraryStore } from '@/stores/library';
|
||||
import { useLibraryItems, useLibraryActions, useLibraryStatus } from '@/utils/storeSelectors';
|
||||
import { useDashboard } from '@/features/dashboard/hooks/useDashboard';
|
||||
import {
|
||||
Card,
|
||||
|
|
@ -21,7 +21,10 @@ import { fr } from 'date-fns/locale';
|
|||
*/
|
||||
export function DashboardPage() {
|
||||
const { user } = useAuthStore();
|
||||
const { items, fetchItems, isLoading: isLoadingLibrary } = useLibraryStore();
|
||||
// FE-STATE-009: Use selector that returns denormalized array
|
||||
const items = useLibraryItems();
|
||||
const { fetchItems } = useLibraryActions();
|
||||
const { isLoading: isLoadingLibrary } = useLibraryStatus();
|
||||
const { stats, recentActivity, isLoading: isLoadingDashboard } = useDashboard();
|
||||
const navigate = useNavigate();
|
||||
|
||||
|
|
@ -29,9 +32,6 @@ export function DashboardPage() {
|
|||
fetchItems({ limit: 5 });
|
||||
}, [fetchItems]);
|
||||
|
||||
// Sécuriser items pour s'assurer que c'est toujours un tableau
|
||||
const safeItems = Array.isArray(items) ? items : [];
|
||||
|
||||
// FE-PAGE-001: Format stats with real data
|
||||
const formatNumber = (num: number): string => {
|
||||
if (num >= 1000000) {
|
||||
|
|
@ -233,7 +233,7 @@ export function DashboardPage() {
|
|||
</div>
|
||||
) : (
|
||||
<div className="space-y-3">
|
||||
{safeItems.slice(0, 3).map((item) => (
|
||||
{items.slice(0, 3).map((item) => (
|
||||
<div key={item.id} className="flex items-center space-x-3">
|
||||
<div className="w-10 h-10 bg-muted rounded flex items-center justify-center">
|
||||
<Music className="h-4 w-4 text-muted-foreground" />
|
||||
|
|
@ -248,7 +248,7 @@ export function DashboardPage() {
|
|||
</div>
|
||||
</div>
|
||||
))}
|
||||
{safeItems.length === 0 && (
|
||||
{items.length === 0 && (
|
||||
<p className="text-sm text-muted-foreground text-center py-4">
|
||||
Aucune piste dans votre bibliothèque
|
||||
</p>
|
||||
|
|
|
|||
|
|
@ -2,11 +2,23 @@ import { create } from 'zustand';
|
|||
import { persist, devtools } from 'zustand/middleware';
|
||||
import { apiClient } from '@/services/api/client';
|
||||
import { undoRedo } from '@/utils/undoRedo';
|
||||
import {
|
||||
normalize,
|
||||
denormalize,
|
||||
addToNormalized,
|
||||
updateInNormalized,
|
||||
removeFromNormalized,
|
||||
replaceNormalized,
|
||||
mergeNormalized,
|
||||
createEmptyNormalized,
|
||||
type NormalizedState,
|
||||
} from '@/utils/stateNormalization';
|
||||
import type { LibraryItem, PaginatedResponse, ApiError } from '@/types';
|
||||
|
||||
interface LibraryState {
|
||||
items: LibraryItem[];
|
||||
favorites: LibraryItem[];
|
||||
// FE-STATE-009: Normalized state for better performance
|
||||
items: NormalizedState<LibraryItem>;
|
||||
favorites: NormalizedState<LibraryItem>;
|
||||
isLoading: boolean;
|
||||
error: ApiError | null;
|
||||
pagination: {
|
||||
|
|
@ -47,9 +59,9 @@ export const useLibraryStore = create<LibraryState & LibraryActions & { undo: ()
|
|||
persist(
|
||||
undoRedo(
|
||||
(set, get) => ({
|
||||
// État initial
|
||||
items: [],
|
||||
favorites: [],
|
||||
// État initial - FE-STATE-009: Normalized state
|
||||
items: createEmptyNormalized<LibraryItem>(),
|
||||
favorites: createEmptyNormalized<LibraryItem>(),
|
||||
isLoading: false,
|
||||
error: null,
|
||||
pagination: {
|
||||
|
|
@ -88,8 +100,9 @@ export const useLibraryStore = create<LibraryState & LibraryActions & { undo: ()
|
|||
? data
|
||||
: [];
|
||||
|
||||
// FE-STATE-009: Normalize items for better performance
|
||||
set({
|
||||
items: itemsArray,
|
||||
items: normalize(itemsArray),
|
||||
pagination: {
|
||||
page: data.page || 1,
|
||||
limit: data.limit || limit,
|
||||
|
|
@ -101,9 +114,9 @@ export const useLibraryStore = create<LibraryState & LibraryActions & { undo: ()
|
|||
error: null,
|
||||
});
|
||||
} catch (error: any) {
|
||||
// En cas d'erreur, s'assurer que items reste un tableau vide
|
||||
// En cas d'erreur, s'assurer que items reste un état normalisé vide
|
||||
set({
|
||||
items: [],
|
||||
items: createEmptyNormalized<LibraryItem>(),
|
||||
error: error as ApiError,
|
||||
isLoading: false,
|
||||
});
|
||||
|
|
@ -130,15 +143,16 @@ export const useLibraryStore = create<LibraryState & LibraryActions & { undo: ()
|
|||
? data
|
||||
: [];
|
||||
|
||||
// FE-STATE-009: Normalize favorites for better performance
|
||||
set({
|
||||
favorites: favoritesArray,
|
||||
favorites: normalize(favoritesArray),
|
||||
isLoading: false,
|
||||
error: null,
|
||||
});
|
||||
} catch (error: any) {
|
||||
// En cas d'erreur, s'assurer que favorites reste un tableau vide
|
||||
// En cas d'erreur, s'assurer que favorites reste un état normalisé vide
|
||||
set({
|
||||
favorites: [],
|
||||
favorites: createEmptyNormalized<LibraryItem>(),
|
||||
error: error as ApiError,
|
||||
isLoading: false,
|
||||
});
|
||||
|
|
@ -162,8 +176,9 @@ export const useLibraryStore = create<LibraryState & LibraryActions & { undo: ()
|
|||
});
|
||||
const newItem = response.data;
|
||||
|
||||
// FE-STATE-009: Add to normalized state at position 0 (prepend)
|
||||
set((state) => ({
|
||||
items: [newItem, ...state.items],
|
||||
items: addToNormalized(state.items, newItem, 0),
|
||||
isLoading: false,
|
||||
error: null,
|
||||
}));
|
||||
|
|
@ -177,36 +192,62 @@ export const useLibraryStore = create<LibraryState & LibraryActions & { undo: ()
|
|||
},
|
||||
|
||||
// FE-STATE-005: Optimistic update for toggle favorite
|
||||
// FE-STATE-009: Using normalized state operations
|
||||
toggleFavorite: async (itemId: string) => {
|
||||
const previousState = get();
|
||||
const item = previousState.items.find((i) => i.id === itemId);
|
||||
const item = previousState.items.byId[itemId];
|
||||
const newFavoriteStatus = item ? !item.is_favorite : true;
|
||||
|
||||
// Optimistic update
|
||||
set((state) => ({
|
||||
items: state.items.map((i) =>
|
||||
i.id === itemId ? { ...i, is_favorite: newFavoriteStatus } : i,
|
||||
),
|
||||
favorites: newFavoriteStatus && item
|
||||
? [...state.favorites.filter((i) => i.id !== itemId), { ...item, is_favorite: true }]
|
||||
: state.favorites.filter((i) => i.id !== itemId),
|
||||
}));
|
||||
// Optimistic update - FE-STATE-009: Update in normalized state
|
||||
set((state) => {
|
||||
const updatedItems = updateInNormalized(state.items, itemId, {
|
||||
is_favorite: newFavoriteStatus,
|
||||
});
|
||||
|
||||
let updatedFavorites = state.favorites;
|
||||
if (newFavoriteStatus && item) {
|
||||
// Add to favorites
|
||||
const favoriteItem = { ...item, is_favorite: true };
|
||||
updatedFavorites = addToNormalized(
|
||||
removeFromNormalized(state.favorites, itemId),
|
||||
favoriteItem,
|
||||
);
|
||||
} else {
|
||||
// Remove from favorites
|
||||
updatedFavorites = removeFromNormalized(state.favorites, itemId);
|
||||
}
|
||||
|
||||
return {
|
||||
items: updatedItems,
|
||||
favorites: updatedFavorites,
|
||||
};
|
||||
});
|
||||
|
||||
try {
|
||||
const response = await apiClient.post<LibraryItem>(`/tracks/${itemId}/favorite`);
|
||||
const updatedItem = response.data;
|
||||
|
||||
// Apply server response
|
||||
set((state) => ({
|
||||
items: state.items.map((i) =>
|
||||
i.id === itemId
|
||||
? { ...i, is_favorite: updatedItem.is_favorite }
|
||||
: i,
|
||||
),
|
||||
favorites: updatedItem.is_favorite
|
||||
? [...state.favorites.filter((i) => i.id !== itemId), updatedItem]
|
||||
: state.favorites.filter((i) => i.id !== itemId),
|
||||
}));
|
||||
// Apply server response - FE-STATE-009: Update in normalized state
|
||||
set((state) => {
|
||||
const updatedItems = updateInNormalized(state.items, itemId, {
|
||||
is_favorite: updatedItem.is_favorite,
|
||||
});
|
||||
|
||||
let updatedFavorites = state.favorites;
|
||||
if (updatedItem.is_favorite) {
|
||||
updatedFavorites = addToNormalized(
|
||||
removeFromNormalized(state.favorites, itemId),
|
||||
updatedItem,
|
||||
);
|
||||
} else {
|
||||
updatedFavorites = removeFromNormalized(state.favorites, itemId);
|
||||
}
|
||||
|
||||
return {
|
||||
items: updatedItems,
|
||||
favorites: updatedFavorites,
|
||||
};
|
||||
});
|
||||
} catch (error: any) {
|
||||
// Rollback on error
|
||||
set(() => previousState);
|
||||
|
|
@ -216,13 +257,14 @@ export const useLibraryStore = create<LibraryState & LibraryActions & { undo: ()
|
|||
},
|
||||
|
||||
// FE-STATE-005: Optimistic update for delete item
|
||||
// FE-STATE-009: Using normalized state operations
|
||||
deleteItem: async (itemId: string) => {
|
||||
const previousState = get();
|
||||
|
||||
// Optimistic update - remove item immediately
|
||||
// Optimistic update - remove item immediately from normalized state
|
||||
set((state) => ({
|
||||
items: state.items.filter((item) => item.id !== itemId),
|
||||
favorites: state.favorites.filter((item) => item.id !== itemId),
|
||||
items: removeFromNormalized(state.items, itemId),
|
||||
favorites: removeFromNormalized(state.favorites, itemId),
|
||||
}));
|
||||
|
||||
try {
|
||||
|
|
@ -247,10 +289,10 @@ export const useLibraryStore = create<LibraryState & LibraryActions & { undo: ()
|
|||
|
||||
setError: (error) => set({ error }),
|
||||
|
||||
clearItems: () =>
|
||||
clearItems: () => {
|
||||
set({
|
||||
items: [],
|
||||
favorites: [],
|
||||
items: createEmptyNormalized<LibraryItem>(),
|
||||
favorites: createEmptyNormalized<LibraryItem>(),
|
||||
pagination: {
|
||||
page: 1,
|
||||
limit: 20,
|
||||
|
|
@ -258,6 +300,8 @@ export const useLibraryStore = create<LibraryState & LibraryActions & { undo: ()
|
|||
hasNext: false,
|
||||
hasPrev: false,
|
||||
},
|
||||
});
|
||||
},
|
||||
}),
|
||||
{
|
||||
// FE-STATE-006: Enable undo/redo for library store
|
||||
|
|
@ -265,17 +309,19 @@ export const useLibraryStore = create<LibraryState & LibraryActions & { undo: ()
|
|||
enabled: true,
|
||||
shouldTrack: (state, prevState) => {
|
||||
// Track changes to items and favorites (not loading/error states)
|
||||
// FE-STATE-009: Compare normalized state structures
|
||||
return (
|
||||
JSON.stringify(state.items) !== JSON.stringify(prevState?.items) ||
|
||||
JSON.stringify(state.favorites) !== JSON.stringify(prevState?.favorites)
|
||||
);
|
||||
},
|
||||
},
|
||||
),
|
||||
{
|
||||
name: 'library-storage',
|
||||
},
|
||||
),
|
||||
{
|
||||
name: 'library-storage',
|
||||
partialize: (state) => ({
|
||||
// FE-STATE-001: Persist favorites and filters for offline support
|
||||
// FE-STATE-009: Persist normalized favorites state
|
||||
favorites: state.favorites,
|
||||
filters: state.filters,
|
||||
// Don't persist items and pagination as they should be fetched fresh
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@ import { useUIStore } from '@/stores/ui';
|
|||
import { useChatStore } from '@/stores/chat';
|
||||
import { useLibraryStore } from '@/stores/library';
|
||||
import { usePlayerStore } from '@/stores/player';
|
||||
import { createEmptyNormalized } from '@/utils/stateNormalization';
|
||||
|
||||
// Mock localStorage
|
||||
const localStorageMock = {
|
||||
|
|
@ -153,8 +154,9 @@ describe('Zustand Stores', () => {
|
|||
describe('Library Store', () => {
|
||||
it('should have initial state', () => {
|
||||
const state = useLibraryStore.getState();
|
||||
expect(state.items).toEqual([]);
|
||||
expect(state.favorites).toEqual([]);
|
||||
// FE-STATE-009: Normalized state structure
|
||||
expect(state.items).toEqual(createEmptyNormalized());
|
||||
expect(state.favorites).toEqual(createEmptyNormalized());
|
||||
expect(state.isLoading).toBe(false);
|
||||
expect(state.error).toBeNull();
|
||||
});
|
||||
|
|
|
|||
247
apps/web/src/utils/stateNormalization.ts
Normal file
247
apps/web/src/utils/stateNormalization.ts
Normal file
|
|
@ -0,0 +1,247 @@
|
|||
/**
|
||||
* State Normalization Utilities
|
||||
* FE-STATE-009: Normalize nested state structures for better performance
|
||||
*
|
||||
* Provides utilities to normalize nested state structures (arrays of objects)
|
||||
* into flat structures with indexed lookups (byId + allIds pattern).
|
||||
* This improves performance by enabling O(1) lookups instead of O(n) array searches.
|
||||
*/
|
||||
|
||||
/**
|
||||
* Normalized state structure
|
||||
* Instead of storing items as arrays: [item1, item2, item3]
|
||||
* We store them as: { byId: { id1: item1, id2: item2, id3: item3 }, allIds: ['id1', 'id2', 'id3'] }
|
||||
*/
|
||||
export interface NormalizedState<T> {
|
||||
byId: Record<string, T>;
|
||||
allIds: string[];
|
||||
}
|
||||
|
||||
/**
|
||||
* Normalize an array of items into a normalized state structure
|
||||
* @param items Array of items with an 'id' property
|
||||
* @returns Normalized state with byId and allIds
|
||||
*/
|
||||
export function normalize<T extends { id: string }>(
|
||||
items: T[],
|
||||
): NormalizedState<T> {
|
||||
const byId: Record<string, T> = {};
|
||||
const allIds: string[] = [];
|
||||
|
||||
for (const item of items) {
|
||||
if (item.id) {
|
||||
byId[item.id] = item;
|
||||
allIds.push(item.id);
|
||||
}
|
||||
}
|
||||
|
||||
return { byId, allIds };
|
||||
}
|
||||
|
||||
/**
|
||||
* Denormalize a normalized state back into an array
|
||||
* @param normalized Normalized state structure
|
||||
* @returns Array of items in the order of allIds
|
||||
*/
|
||||
export function denormalize<T>(
|
||||
normalized: NormalizedState<T>,
|
||||
): T[] {
|
||||
return normalized.allIds
|
||||
.map((id) => normalized.byId[id])
|
||||
.filter((item): item is T => item !== undefined);
|
||||
}
|
||||
|
||||
/**
|
||||
* Add an item to normalized state
|
||||
* @param normalized Current normalized state
|
||||
* @param item Item to add
|
||||
* @param position Optional position to insert at (default: append)
|
||||
* @returns New normalized state
|
||||
*/
|
||||
export function addToNormalized<T extends { id: string }>(
|
||||
normalized: NormalizedState<T>,
|
||||
item: T,
|
||||
position?: number,
|
||||
): NormalizedState<T> {
|
||||
const { byId, allIds } = normalized;
|
||||
const newById = { ...byId, [item.id]: item };
|
||||
|
||||
let newAllIds: string[];
|
||||
if (position === undefined || position >= allIds.length) {
|
||||
// Append to end
|
||||
newAllIds = [...allIds, item.id];
|
||||
} else if (position <= 0) {
|
||||
// Prepend to start
|
||||
newAllIds = [item.id, ...allIds];
|
||||
} else {
|
||||
// Insert at position
|
||||
newAllIds = [
|
||||
...allIds.slice(0, position),
|
||||
item.id,
|
||||
...allIds.slice(position),
|
||||
];
|
||||
}
|
||||
|
||||
return { byId: newById, allIds: newAllIds };
|
||||
}
|
||||
|
||||
/**
|
||||
* Update an item in normalized state
|
||||
* @param normalized Current normalized state
|
||||
* @param itemId ID of item to update
|
||||
* @param updates Partial updates to apply
|
||||
* @returns New normalized state
|
||||
*/
|
||||
export function updateInNormalized<T extends { id: string }>(
|
||||
normalized: NormalizedState<T>,
|
||||
itemId: string,
|
||||
updates: Partial<T>,
|
||||
): NormalizedState<T> {
|
||||
const existing = normalized.byId[itemId];
|
||||
if (!existing) {
|
||||
return normalized;
|
||||
}
|
||||
|
||||
return {
|
||||
...normalized,
|
||||
byId: {
|
||||
...normalized.byId,
|
||||
[itemId]: { ...existing, ...updates },
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove an item from normalized state
|
||||
* @param normalized Current normalized state
|
||||
* @param itemId ID of item to remove
|
||||
* @returns New normalized state
|
||||
*/
|
||||
export function removeFromNormalized<T>(
|
||||
normalized: NormalizedState<T>,
|
||||
itemId: string,
|
||||
): NormalizedState<T> {
|
||||
const { [itemId]: removed, ...byId } = normalized.byId;
|
||||
const allIds = normalized.allIds.filter((id) => id !== itemId);
|
||||
|
||||
return { byId, allIds };
|
||||
}
|
||||
|
||||
/**
|
||||
* Replace all items in normalized state
|
||||
* @param normalized Current normalized state
|
||||
* @param items New array of items
|
||||
* @returns New normalized state
|
||||
*/
|
||||
export function replaceNormalized<T extends { id: string }>(
|
||||
normalized: NormalizedState<T>,
|
||||
items: T[],
|
||||
): NormalizedState<T> {
|
||||
return normalize(items);
|
||||
}
|
||||
|
||||
/**
|
||||
* Merge items into normalized state (add new, update existing)
|
||||
* @param normalized Current normalized state
|
||||
* @param items Items to merge
|
||||
* @returns New normalized state
|
||||
*/
|
||||
export function mergeNormalized<T extends { id: string }>(
|
||||
normalized: NormalizedState<T>,
|
||||
items: T[],
|
||||
): NormalizedState<T> {
|
||||
const newById = { ...normalized.byId };
|
||||
const newAllIds = [...normalized.allIds];
|
||||
const existingIds = new Set(normalized.allIds);
|
||||
|
||||
for (const item of items) {
|
||||
if (item.id) {
|
||||
newById[item.id] = item;
|
||||
if (!existingIds.has(item.id)) {
|
||||
newAllIds.push(item.id);
|
||||
existingIds.add(item.id);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return { byId: newById, allIds: newAllIds };
|
||||
}
|
||||
|
||||
/**
|
||||
* Get an item by ID from normalized state
|
||||
* @param normalized Normalized state
|
||||
* @param itemId ID to look up
|
||||
* @returns Item or undefined
|
||||
*/
|
||||
export function getById<T>(
|
||||
normalized: NormalizedState<T>,
|
||||
itemId: string,
|
||||
): T | undefined {
|
||||
return normalized.byId[itemId];
|
||||
}
|
||||
|
||||
/**
|
||||
* Get multiple items by IDs from normalized state
|
||||
* @param normalized Normalized state
|
||||
* @param itemIds Array of IDs to look up
|
||||
* @returns Array of items (may contain undefined for missing IDs)
|
||||
*/
|
||||
export function getByIds<T>(
|
||||
normalized: NormalizedState<T>,
|
||||
itemIds: string[],
|
||||
): (T | undefined)[] {
|
||||
return itemIds.map((id) => normalized.byId[id]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if an item exists in normalized state
|
||||
* @param normalized Normalized state
|
||||
* @param itemId ID to check
|
||||
* @returns True if item exists
|
||||
*/
|
||||
export function hasId<T>(
|
||||
normalized: NormalizedState<T>,
|
||||
itemId: string,
|
||||
): boolean {
|
||||
return itemId in normalized.byId;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the count of items in normalized state
|
||||
* @param normalized Normalized state
|
||||
* @returns Number of items
|
||||
*/
|
||||
export function getCount<T>(normalized: NormalizedState<T>): number {
|
||||
return normalized.allIds.length;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create an empty normalized state
|
||||
* @returns Empty normalized state
|
||||
*/
|
||||
export function createEmptyNormalized<T>(): NormalizedState<T> {
|
||||
return { byId: {}, allIds: [] };
|
||||
}
|
||||
|
||||
/**
|
||||
* Reorder items in normalized state
|
||||
* @param normalized Current normalized state
|
||||
* @param fromIndex Source index
|
||||
* @param toIndex Destination index
|
||||
* @returns New normalized state with reordered allIds
|
||||
*/
|
||||
export function reorderNormalized<T>(
|
||||
normalized: NormalizedState<T>,
|
||||
fromIndex: number,
|
||||
toIndex: number,
|
||||
): NormalizedState<T> {
|
||||
const newAllIds = [...normalized.allIds];
|
||||
const [removed] = newAllIds.splice(fromIndex, 1);
|
||||
newAllIds.splice(toIndex, 0, removed);
|
||||
|
||||
return {
|
||||
...normalized,
|
||||
allIds: newAllIds,
|
||||
};
|
||||
}
|
||||
|
||||
|
|
@ -11,6 +11,7 @@ import { useAuthStore } from '@/stores/auth';
|
|||
import { useUIStore } from '@/stores/ui';
|
||||
import { useLibraryStore } from '@/stores/library';
|
||||
import { useChatStore } from '@/stores/chat';
|
||||
import { denormalize } from '@/utils/stateNormalization';
|
||||
|
||||
/**
|
||||
* FE-STATE-008: Optimized selectors for AuthStore
|
||||
|
|
@ -79,12 +80,24 @@ export function useUIActions() {
|
|||
|
||||
/**
|
||||
* FE-STATE-008: Optimized selectors for LibraryStore
|
||||
* FE-STATE-009: Convert normalized state to arrays for compatibility
|
||||
*/
|
||||
export function useLibraryItems() {
|
||||
return useLibraryStore(useShallow((state) => state.items));
|
||||
return useLibraryStore(useShallow((state) => denormalize(state.items)));
|
||||
}
|
||||
|
||||
export function useLibraryFavorites() {
|
||||
return useLibraryStore(useShallow((state) => denormalize(state.favorites)));
|
||||
}
|
||||
|
||||
/**
|
||||
* FE-STATE-009: Get normalized state directly (for advanced use cases)
|
||||
*/
|
||||
export function useLibraryItemsNormalized() {
|
||||
return useLibraryStore(useShallow((state) => state.items));
|
||||
}
|
||||
|
||||
export function useLibraryFavoritesNormalized() {
|
||||
return useLibraryStore(useShallow((state) => state.favorites));
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Reference in a new issue