veza/apps/web/src/utils/storeSelectors.ts.backup
senke f0ba7de543 state-ownership: delete unused optimisticStoreUpdates.ts file
- 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
2026-01-15 19:26:53 +01:00

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