veza/apps/web/src/utils/storeSelectors.ts

240 lines
7.5 KiB
TypeScript
Raw Normal View History

/**
* 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.
*/
import { useShallow } from 'zustand/react/shallow';
import { useQueryClient } from '@tanstack/react-query';
import { useAuthStore } from '@/features/auth/store/authStore';
2026-01-07 18:39:21 +00:00
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';
2026-01-07 18:39:21 +00:00
import type { User, LibraryItem, ChatMessage, Conversation } from '@/types';
import type { ApiError } from '@/types/api';
/**
* FE-STATE-008: Optimized selectors for AuthStore
*
* These hooks only re-render when the selected values actually change.
*/
2026-01-07 18:39:21 +00:00
export function useAuthUser(): User | null {
return useAuthStore(useShallow((state) => state.user));
}
2026-01-07 18:39:21 +00:00
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
*/
2026-01-07 18:39:21 +00:00
export function useUITheme(): 'light' | 'dark' | 'system' {
return useUIStore(useShallow((state: UIStore) => state.theme));
}
2026-01-07 18:39:21 +00:00
export function useUILanguage(): 'en' | 'fr' {
return useUIStore(useShallow((state: UIStore) => state.language));
}
export function useUISidebar() {
2026-01-07 18:39:21 +00:00
return useUIStore(useShallow((state: UIStore) => ({
sidebarOpen: state.sidebarOpen,
setSidebarOpen: state.setSidebarOpen,
})));
}
export function useUINotifications() {
2026-01-07 18:39:21 +00:00
return useUIStore(useShallow((state: UIStore) => ({
notifications: state.notifications,
addNotification: state.addNotification,
removeNotification: state.removeNotification,
markNotificationAsRead: state.markNotificationAsRead,
clearNotifications: state.clearNotifications,
})));
}
export function useUIActions() {
2026-01-07 18:39:21 +00:00
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 ?? [];
}
2026-01-07 18:39:21 +00:00
export function useLibraryFavorites(): LibraryItem[] {
const { data } = useLibraryFavoritesQuery();
return data ?? [];
}
/**
* FE-STATE-009: Get normalized state directly (for advanced use cases)
*/
export function useLibraryItemsNormalized() {
2026-01-07 18:39:21 +00:00
return useLibraryStore(useShallow((state: LibraryStore) => state.items));
}
export function useLibraryFavoritesNormalized() {
2026-01-07 18:39:21 +00:00
return useLibraryStore(useShallow((state: LibraryStore) => state.favorites));
}
export function useLibraryFilters() {
2026-01-07 18:39:21 +00:00
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,
has_next: data?.has_next ?? false,
has_prev: data?.has_prev ?? false,
};
}
2026-01-07 18:39:21 +00:00
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();
const libraryStore = useLibraryStore();
return {
fetchItems: async (params?: LibraryItemsParams) => {
await queryClient.refetchQueries({
queryKey: libraryQueryKeys.items(params),
});
},
fetchFavorites: async () => {
await queryClient.refetchQueries({
queryKey: libraryQueryKeys.favorites(),
});
},
uploadFile: libraryStore.uploadFile,
toggleFavorite: libraryStore.toggleFavorite,
deleteItem: libraryStore.deleteItem,
clearItems: libraryStore.clearItems,
};
}
/**
* FE-STATE-008: Optimized selectors for ChatStore
*/
2026-01-07 18:39:21 +00:00
export function useChatConversations(): Conversation[] {
return useChatStore(useShallow((state: ChatStore) => state.conversations));
}
2026-01-07 18:39:21 +00:00
export function useChatCurrentConversation(): Conversation | null {
return useChatStore(useShallow((state: ChatStore) => state.currentConversation));
}
2026-01-07 18:39:21 +00:00
export function useChatMessages(conversationId: string): ChatMessage[] {
return useChatStore(useShallow((state: ChatStore) => state.messages[conversationId] || []));
}
2026-01-07 18:39:21 +00:00
export function useChatTypingUsers(conversationId: string): string[] {
return useChatStore(useShallow((state: ChatStore) => state.typingUsers[conversationId] || []));
}
2026-01-07 18:39:21 +00:00
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() {
2026-01-07 18:39:21 +00:00
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));
}