- Deleted apps/web/src/utils/optimisticStoreUpdates.ts (unused file) - File was unused - no imports found in codebase - Mutations already use React Query's onMutate pattern - No TypeScript errors after deletion - Actions 4.4.1.2 and 4.4.1.3 complete
345 lines
11 KiB
Text
345 lines
11 KiB
Text
/**
|
|
* Store Selectors
|
|
* FE-STATE-008: Optimize state selectors to prevent unnecessary re-renders
|
|
*
|
|
* Provides optimized selectors for Zustand stores to prevent unnecessary re-renders.
|
|
* Use these selectors instead of accessing the entire store.
|
|
*/
|
|
|
|
// CRITICAL FIX: Ensure React.Children is available before importing zustand/react/shallow
|
|
// The issue is that with-selector accesses React.Children at module import time
|
|
// We need to ensure React is fully initialized before this module is evaluated
|
|
|
|
// CRITICAL: ALL imports must be at the top to avoid "Cannot access before initialization" errors
|
|
// Import React FIRST to ensure it's available before useShallow
|
|
import React from 'react';
|
|
import { useShallow } from 'zustand/react/shallow';
|
|
import { useQueryClient } from '@tanstack/react-query';
|
|
import { useAuthStore } from '@/features/auth/store/authStore';
|
|
import { useUser } from '@/features/auth/hooks/useUser';
|
|
import { useUIStore, type UIStore } from '@/stores/ui';
|
|
import { useLibraryStore, type LibraryStore } from '@/stores/library';
|
|
import { useChatStore, type ChatStore } from '@/stores/chat';
|
|
import {
|
|
useLibraryItems as useLibraryItemsQuery,
|
|
useLibraryFavorites as useLibraryFavoritesQuery,
|
|
libraryQueryKeys,
|
|
type LibraryItemsParams,
|
|
} from '@/features/library/hooks/useLibraryItems';
|
|
import type { User, LibraryItem, ChatMessage, Conversation } from '@/types';
|
|
import type { ApiError } from '@/types/api';
|
|
|
|
// CRITICAL FIX: Make React available globally AFTER all imports
|
|
// This ensures React.Children exists before any other module tries to access it
|
|
// NOTE: This runs after imports are evaluated, so React should already be loaded
|
|
if (typeof window !== 'undefined') {
|
|
(window as any).React = React;
|
|
// Verify React.Children exists
|
|
if (React && !React.Children) {
|
|
console.error('[CRITICAL] React.Children not available after import');
|
|
}
|
|
}
|
|
|
|
/**
|
|
* FE-STATE-008: Optimized selectors for AuthStore
|
|
*
|
|
* These hooks only re-render when the selected values actually change.
|
|
*
|
|
* Action 4.1.1.3: Migrated to use React Query hook instead of Zustand store
|
|
*/
|
|
export function useAuthUser(): User | null {
|
|
const { data: user } = useUser();
|
|
return user ?? null;
|
|
}
|
|
|
|
export function useAuthStatus(): {
|
|
isAuthenticated: boolean;
|
|
isLoading: boolean;
|
|
error: ApiError | null;
|
|
} {
|
|
return useAuthStore(
|
|
useShallow((state) => ({
|
|
isAuthenticated: state.isAuthenticated,
|
|
isLoading: state.isLoading,
|
|
error: state.error,
|
|
})),
|
|
);
|
|
}
|
|
|
|
export function useAuthActions() {
|
|
return useAuthStore(
|
|
useShallow((state) => ({
|
|
login: state.login,
|
|
register: state.register,
|
|
logout: state.logout,
|
|
refreshUser: state.refreshUser,
|
|
checkAuthStatus: state.checkAuthStatus,
|
|
clearError: state.clearError,
|
|
setLoading: state.setLoading,
|
|
})),
|
|
);
|
|
}
|
|
|
|
/**
|
|
* FE-STATE-008: Optimized selectors for UIStore
|
|
*/
|
|
export function useUITheme(): 'light' | 'dark' | 'system' {
|
|
return useUIStore(useShallow((state: UIStore) => state.theme));
|
|
}
|
|
|
|
export function useUILanguage(): 'en' | 'fr' {
|
|
return useUIStore(useShallow((state: UIStore) => state.language));
|
|
}
|
|
|
|
export function useUISidebar() {
|
|
return useUIStore(
|
|
useShallow((state: UIStore) => ({
|
|
sidebarOpen: state.sidebarOpen,
|
|
setSidebarOpen: state.setSidebarOpen,
|
|
})),
|
|
);
|
|
}
|
|
|
|
export function useUINotifications() {
|
|
return useUIStore(
|
|
useShallow((state: UIStore) => ({
|
|
notifications: state.notifications,
|
|
addNotification: state.addNotification,
|
|
removeNotification: state.removeNotification,
|
|
markNotificationAsRead: state.markNotificationAsRead,
|
|
clearNotifications: state.clearNotifications,
|
|
})),
|
|
);
|
|
}
|
|
|
|
export function useUIActions() {
|
|
return useUIStore(
|
|
useShallow((state: UIStore) => ({
|
|
setTheme: state.setTheme,
|
|
setLanguage: state.setLanguage,
|
|
setSidebarOpen: state.setSidebarOpen,
|
|
})),
|
|
);
|
|
}
|
|
|
|
/**
|
|
* FE-STATE-008: Optimized selectors for LibraryStore
|
|
* FE-STATE-009: Convert normalized state to arrays for compatibility
|
|
*
|
|
* NOTE: These selectors now use React Query hooks instead of Zustand store.
|
|
* They maintain the same interface for backward compatibility.
|
|
*/
|
|
export function useLibraryItems(
|
|
params: LibraryItemsParams = {},
|
|
): LibraryItem[] {
|
|
const { data } = useLibraryItemsQuery(params);
|
|
return data?.items ?? [];
|
|
}
|
|
|
|
export function useLibraryFavorites(): LibraryItem[] {
|
|
const { data } = useLibraryFavoritesQuery();
|
|
return data ?? [];
|
|
}
|
|
|
|
/**
|
|
* FE-STATE-009: Get normalized state directly (for advanced use cases)
|
|
*
|
|
* @deprecated Domain data (items, favorites) have been migrated to React Query.
|
|
* Use useLibraryItems() or useLibraryFavorites() instead.
|
|
*/
|
|
export function useLibraryItemsNormalized() {
|
|
// Domain data removed from store - return empty normalized state for backward compatibility
|
|
return { byId: {}, allIds: [] };
|
|
}
|
|
|
|
/**
|
|
* @deprecated Domain data (items, favorites) have been migrated to React Query.
|
|
* Use useLibraryFavorites() instead.
|
|
*/
|
|
export function useLibraryFavoritesNormalized() {
|
|
// Domain data removed from store - return empty normalized state for backward compatibility
|
|
return { byId: {}, allIds: [] };
|
|
}
|
|
|
|
export function useLibraryFilters() {
|
|
return useLibraryStore(
|
|
useShallow((state: LibraryStore) => ({
|
|
filters: state.filters,
|
|
setFilters: state.setFilters,
|
|
})),
|
|
);
|
|
}
|
|
|
|
export function useLibraryPagination(params: LibraryItemsParams = {}) {
|
|
const { data } = useLibraryItemsQuery(params);
|
|
return {
|
|
page: data?.page ?? 1,
|
|
limit: data?.limit ?? 20,
|
|
total: data?.total ?? 0,
|
|
// Map snake_case from API to camelCase for backward compatibility
|
|
hasNext: data?.has_next ?? false,
|
|
hasPrev: data?.has_prev ?? false,
|
|
// Also provide snake_case for API compatibility
|
|
has_next: data?.has_next ?? false,
|
|
has_prev: data?.has_prev ?? false,
|
|
};
|
|
}
|
|
|
|
export function useLibraryStatus(): {
|
|
isLoading: boolean;
|
|
error: ApiError | null;
|
|
} {
|
|
const itemsQuery = useLibraryItemsQuery({});
|
|
const favoritesQuery = useLibraryFavoritesQuery();
|
|
|
|
return {
|
|
isLoading: itemsQuery.isLoading || favoritesQuery.isLoading,
|
|
error: (itemsQuery.error || favoritesQuery.error) as ApiError | null,
|
|
};
|
|
}
|
|
|
|
export function useLibraryActions() {
|
|
const queryClient = useQueryClient();
|
|
|
|
return {
|
|
fetchItems: async (params?: LibraryItemsParams) => {
|
|
await queryClient.refetchQueries({
|
|
queryKey: libraryQueryKeys.items(params),
|
|
});
|
|
},
|
|
fetchFavorites: async () => {
|
|
await queryClient.refetchQueries({
|
|
queryKey: libraryQueryKeys.favorites(),
|
|
});
|
|
},
|
|
uploadFile: async (
|
|
file: File,
|
|
metadata: { title: string; description?: string },
|
|
) => {
|
|
// TODO: Migrate to React Query mutation with optimistic update
|
|
// For now, call API and invalidate queries
|
|
const { apiClient } = await import('@/services/api/client');
|
|
const formData = new FormData();
|
|
formData.append('file', file);
|
|
formData.append('title', metadata.title);
|
|
if (metadata.description) {
|
|
formData.append('description', metadata.description);
|
|
}
|
|
await apiClient.post('/tracks', formData, {
|
|
headers: { 'Content-Type': 'multipart/form-data' },
|
|
});
|
|
await queryClient.invalidateQueries({ queryKey: libraryQueryKeys.all });
|
|
},
|
|
toggleFavorite: async (itemId: string) => {
|
|
// TODO: Migrate to React Query mutation with optimistic update
|
|
// For now, call API and invalidate queries
|
|
const { apiClient } = await import('@/services/api/client');
|
|
await apiClient.post(`/tracks/${itemId}/favorite`);
|
|
await queryClient.invalidateQueries({ queryKey: libraryQueryKeys.all });
|
|
},
|
|
deleteItem: async (itemId: string) => {
|
|
// TODO: Migrate to React Query mutation with optimistic update
|
|
// For now, call API and invalidate queries
|
|
const { apiClient } = await import('@/services/api/client');
|
|
await apiClient.delete(`/tracks/${itemId}`);
|
|
await queryClient.invalidateQueries({ queryKey: libraryQueryKeys.all });
|
|
},
|
|
clearItems: () => {
|
|
// Invalidate all library queries instead of clearing store
|
|
queryClient.invalidateQueries({ queryKey: libraryQueryKeys.all });
|
|
},
|
|
};
|
|
}
|
|
|
|
/**
|
|
* FE-STATE-008: Optimized selectors for ChatStore
|
|
*/
|
|
export function useChatConversations(): Conversation[] {
|
|
return useChatStore(useShallow((state: ChatStore) => state.conversations));
|
|
}
|
|
|
|
export function useChatCurrentConversation(): Conversation | null {
|
|
return useChatStore(
|
|
useShallow((state: ChatStore) => state.currentConversation),
|
|
);
|
|
}
|
|
|
|
export function useChatMessages(conversationId: string): ChatMessage[] {
|
|
return useChatStore(
|
|
useShallow((state: ChatStore) => state.messages[conversationId] || []),
|
|
);
|
|
}
|
|
|
|
export function useChatTypingUsers(conversationId: string): string[] {
|
|
return useChatStore(
|
|
useShallow((state: ChatStore) => state.typingUsers[conversationId] || []),
|
|
);
|
|
}
|
|
|
|
export function useChatConnection(): {
|
|
isConnected: boolean;
|
|
isLoading: boolean;
|
|
error: string | null;
|
|
} {
|
|
return useChatStore(
|
|
useShallow((state: ChatStore) => ({
|
|
isConnected: state.isConnected,
|
|
isLoading: state.isLoading,
|
|
error: state.error,
|
|
})),
|
|
);
|
|
}
|
|
|
|
export function useChatActions() {
|
|
return useChatStore(
|
|
useShallow((state: ChatStore) => ({
|
|
setConversations: state.setConversations,
|
|
setCurrentConversation: state.setCurrentConversation,
|
|
addMessage: state.addMessage,
|
|
updateMessage: state.updateMessage,
|
|
removeMessage: state.removeMessage,
|
|
setMessages: state.setMessages,
|
|
setTypingUsers: state.setTypingUsers,
|
|
addTypingUser: state.addTypingUser,
|
|
removeTypingUser: state.removeTypingUser,
|
|
setConnected: state.setConnected,
|
|
setLoading: state.setLoading,
|
|
setError: state.setError,
|
|
connect: state.connect,
|
|
disconnect: state.disconnect,
|
|
joinConversation: state.joinConversation,
|
|
leaveConversation: state.leaveConversation,
|
|
sendMessage: state.sendMessage,
|
|
startTyping: state.startTyping,
|
|
stopTyping: state.stopTyping,
|
|
addReaction: state.addReaction,
|
|
removeReaction: state.removeReaction,
|
|
fetchConversations: state.fetchConversations,
|
|
createConversation: state.createConversation,
|
|
})),
|
|
);
|
|
}
|
|
|
|
/**
|
|
* FE-STATE-008: Helper function to create custom selectors
|
|
*
|
|
* Use this when you need to select multiple values from a store.
|
|
*
|
|
* @example
|
|
* ```typescript
|
|
* // Instead of:
|
|
* const { user, isAuthenticated } = useAuthStore();
|
|
*
|
|
* // Use:
|
|
* const { user, isAuthenticated } = useStoreSelector(useAuthStore, (state) => ({
|
|
* user: state.user,
|
|
* isAuthenticated: state.isAuthenticated,
|
|
* }));
|
|
* ```
|
|
*/
|
|
export function useStoreSelector<T, U>(
|
|
store: (selector: (state: T) => U) => U,
|
|
selector: (state: T) => U,
|
|
): U {
|
|
return store(useShallow(selector));
|
|
}
|