veza/apps/web/src/utils/storeSelectors.ts
senke 4cc73b0d12 api-contracts: replace ApiError interface with Zod-inferred type
- 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
2026-01-15 17:03:35 +01:00

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));
}