- Replace manual ApiError interface with Zod-inferred type from apiSchemas - Update all imports (15+ files) to use ApiError from @/schemas/apiSchemas - Remove ApiError interface from types/api.ts - Update ApiResponse to import ApiError from schemas - All TypeScript checks pass for ApiError-related code
349 lines
11 KiB
TypeScript
349 lines
11 KiB
TypeScript
/**
|
|
* 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.
|
|
*/
|
|
|
|
// Avoid zustand/react/shallow to prevent React.Children init issues
|
|
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 '@/schemas/apiSchemas';
|
|
|
|
|
|
|
|
/**
|
|
* 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;
|
|
} {
|
|
// TEMPORARY FIX: Use direct store access instead of useShallow to isolate initialization error
|
|
// TODO: Re-enable useShallow once initialization issue is resolved
|
|
const store = useAuthStore();
|
|
return {
|
|
isAuthenticated: store.isAuthenticated,
|
|
isLoading: store.isLoading,
|
|
error: store.error,
|
|
};
|
|
// ORIGINAL CODE (commented for debugging):
|
|
// return useAuthStore(
|
|
// useShallow((state) => ({
|
|
// isAuthenticated: state.isAuthenticated,
|
|
// isLoading: state.isLoading,
|
|
// error: state.error,
|
|
// })),
|
|
// );
|
|
}
|
|
|
|
export function useAuthActions() {
|
|
// TEMPORARY FIX: Use direct store access instead of useShallow to isolate initialization error
|
|
const store = useAuthStore();
|
|
return {
|
|
login: store.login,
|
|
register: store.register,
|
|
logout: store.logout,
|
|
refreshUser: store.refreshUser,
|
|
checkAuthStatus: store.checkAuthStatus,
|
|
clearError: store.clearError,
|
|
setLoading: store.setLoading,
|
|
};
|
|
// ORIGINAL CODE (commented for debugging):
|
|
// 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' {
|
|
// TEMPORARY FIX: Direct store access
|
|
return useUIStore((state: UIStore) => state.theme);
|
|
}
|
|
|
|
export function useUILanguage(): 'en' | 'fr' {
|
|
// TEMPORARY FIX: Direct store access
|
|
return useUIStore((state: UIStore) => state.language);
|
|
}
|
|
|
|
export function useUISidebar() {
|
|
const sidebarOpen = useUIStore((state: UIStore) => state.sidebarOpen);
|
|
const setSidebarOpen = useUIStore((state: UIStore) => state.setSidebarOpen);
|
|
return { sidebarOpen, setSidebarOpen };
|
|
}
|
|
|
|
export function useUINotifications() {
|
|
const notifications = useUIStore((state: UIStore) => state.notifications);
|
|
const addNotification = useUIStore((state: UIStore) => state.addNotification);
|
|
const removeNotification = useUIStore(
|
|
(state: UIStore) => state.removeNotification,
|
|
);
|
|
const markNotificationAsRead = useUIStore(
|
|
(state: UIStore) => state.markNotificationAsRead,
|
|
);
|
|
const clearNotifications = useUIStore(
|
|
(state: UIStore) => state.clearNotifications,
|
|
);
|
|
return {
|
|
notifications,
|
|
addNotification,
|
|
removeNotification,
|
|
markNotificationAsRead,
|
|
clearNotifications,
|
|
};
|
|
}
|
|
|
|
export function useUIActions() {
|
|
const setTheme = useUIStore((state: UIStore) => state.setTheme);
|
|
const setLanguage = useUIStore((state: UIStore) => state.setLanguage);
|
|
const setSidebarOpen = useUIStore((state: UIStore) => state.setSidebarOpen);
|
|
return { setTheme, setLanguage, 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() {
|
|
const filters = useLibraryStore((state: LibraryStore) => state.filters);
|
|
const setFilters = useLibraryStore((state: LibraryStore) => state.setFilters);
|
|
return { filters, 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((state: ChatStore) => state.conversations);
|
|
}
|
|
|
|
export function useChatCurrentConversation(): Conversation | null {
|
|
return useChatStore((state: ChatStore) => state.currentConversation);
|
|
}
|
|
|
|
export function useChatMessages(conversationId: string): ChatMessage[] {
|
|
return useChatStore(
|
|
(state: ChatStore) => state.messages[conversationId] || [],
|
|
);
|
|
}
|
|
|
|
export function useChatTypingUsers(conversationId: string): string[] {
|
|
return useChatStore(
|
|
(state: ChatStore) => state.typingUsers[conversationId] || [],
|
|
);
|
|
}
|
|
|
|
export function useChatConnection(): {
|
|
isConnected: boolean;
|
|
isLoading: boolean;
|
|
error: string | null;
|
|
} {
|
|
const isConnected = useChatStore((state: ChatStore) => state.isConnected);
|
|
const isLoading = useChatStore((state: ChatStore) => state.isLoading);
|
|
const error = useChatStore((state: ChatStore) => state.error);
|
|
return { isConnected, isLoading, error };
|
|
}
|
|
|
|
export function useChatActions() {
|
|
const store = useChatStore();
|
|
return {
|
|
setConversations: store.setConversations,
|
|
setCurrentConversation: store.setCurrentConversation,
|
|
addMessage: store.addMessage,
|
|
updateMessage: store.updateMessage,
|
|
removeMessage: store.removeMessage,
|
|
setMessages: store.setMessages,
|
|
setTypingUsers: store.setTypingUsers,
|
|
addTypingUser: store.addTypingUser,
|
|
removeTypingUser: store.removeTypingUser,
|
|
setConnected: store.setConnected,
|
|
setLoading: store.setLoading,
|
|
setError: store.setError,
|
|
connect: store.connect,
|
|
disconnect: store.disconnect,
|
|
joinConversation: store.joinConversation,
|
|
leaveConversation: store.leaveConversation,
|
|
sendMessage: store.sendMessage,
|
|
startTyping: store.startTyping,
|
|
stopTyping: store.stopTyping,
|
|
addReaction: store.addReaction,
|
|
removeReaction: store.removeReaction,
|
|
fetchConversations: store.fetchConversations,
|
|
createConversation: store.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 {
|
|
// TEMPORARY FIX: Direct store access instead of useShallow to avoid React.Children error
|
|
return store(selector);
|
|
// ORIGINAL CODE (commented for debugging):
|
|
// return store(useShallow(selector));
|
|
}
|