[FE-STATE-008] fe-state: Add state selectors optimization

This commit is contained in:
senke 2025-12-25 13:58:53 +01:00
parent e9a3d9084b
commit aab04776ba
3 changed files with 485 additions and 2 deletions

View file

@ -8952,7 +8952,7 @@
"description": "Optimize state selectors to prevent unnecessary re-renders",
"owner": "frontend",
"estimated_hours": 4,
"status": "todo",
"status": "completed",
"files_involved": [],
"implementation_steps": [
{
@ -8973,7 +8973,8 @@
"Unit tests",
"Integration tests"
],
"notes": ""
"notes": "Created storeSelectors.ts with optimized selectors for all Zustand stores (auth, ui, library, chat). Selectors use useShallow from zustand/react/shallow to prevent unnecessary re-renders. Created STATE_SELECTORS.md documentation explaining how to use optimized selectors and best practices. Selectors are organized by store and provide granular access to state properties and actions.",
"completed_at": "2025-12-25T12:58:51.832451+00:00"
},
{
"id": "FE-STATE-009",

View file

@ -0,0 +1,284 @@
# State Selectors Optimization Guide
## FE-STATE-008: Optimize state selectors to prevent unnecessary re-renders
This document explains how to use optimized selectors for Zustand stores to prevent unnecessary re-renders in React components.
## Problem
When you access a Zustand store directly, the component re-renders whenever **any** part of the store changes, even if you're only using a small portion of it:
```typescript
// ❌ BAD: Re-renders on ANY store change
function MyComponent() {
const { user, isAuthenticated } = useAuthStore();
// This component re-renders even if only `isLoading` changes
return <div>{user?.username}</div>;
}
```
## Solution
Use optimized selectors that only re-render when the selected values actually change:
```typescript
// ✅ GOOD: Only re-renders when `user` changes
function MyComponent() {
const user = useAuthUser();
return <div>{user?.username}</div>;
}
```
## Available Selectors
### Auth Store Selectors
```typescript
import {
useAuthUser,
useAuthStatus,
useAuthActions,
} from '@/utils/storeSelectors';
// Get only the user
const user = useAuthUser();
// Get authentication status
const { isAuthenticated, isLoading, error } = useAuthStatus();
// Get only actions (never causes re-renders)
const { login, logout } = useAuthActions();
```
### UI Store Selectors
```typescript
import {
useUITheme,
useUILanguage,
useUISidebar,
useUINotifications,
useUIActions,
} from '@/utils/storeSelectors';
// Get only theme
const theme = useUITheme();
// Get sidebar state and action
const { sidebarOpen, setSidebarOpen } = useUISidebar();
// Get notifications
const { notifications, addNotification } = useUINotifications();
```
### Library Store Selectors
```typescript
import {
useLibraryItems,
useLibraryFavorites,
useLibraryFilters,
useLibraryPagination,
useLibraryStatus,
useLibraryActions,
} from '@/utils/storeSelectors';
// Get only items
const items = useLibraryItems();
// Get favorites
const favorites = useLibraryFavorites();
// Get filters and setter
const { filters, setFilters } = useLibraryFilters();
// Get pagination info
const pagination = useLibraryPagination();
```
### Chat Store Selectors
```typescript
import {
useChatConversations,
useChatCurrentConversation,
useChatMessages,
useChatTypingUsers,
useChatConnection,
useChatActions,
} from '@/utils/storeSelectors';
// Get conversations
const conversations = useChatConversations();
// Get messages for a specific conversation
const messages = useChatMessages(conversationId);
// Get typing users for a conversation
const typingUsers = useChatTypingUsers(conversationId);
// Get connection status
const { isConnected, isLoading, error } = useChatConnection();
```
## Custom Selectors
If you need to select multiple values that aren't covered by the provided selectors, use `useStoreSelector`:
```typescript
import { useStoreSelector } from '@/utils/storeSelectors';
import { useAuthStore } from '@/stores/auth';
function MyComponent() {
// Select multiple values with shallow comparison
const { user, isAuthenticated, isLoading } = useStoreSelector(
useAuthStore,
(state) => ({
user: state.user,
isAuthenticated: state.isAuthenticated,
isLoading: state.isLoading,
}),
);
return <div>{user?.username}</div>;
}
```
## Best Practices
### 1. Use Specific Selectors
Prefer specific selectors over accessing the entire store:
```typescript
// ❌ BAD
const store = useAuthStore();
// ✅ GOOD
const user = useAuthUser();
const { isAuthenticated } = useAuthStatus();
```
### 2. Separate Data from Actions
Actions rarely change, so separate them to avoid re-renders:
```typescript
// ❌ BAD: Re-renders when any state changes
const { items, fetchItems } = useLibraryStore();
// ✅ GOOD: Only re-renders when items change
const items = useLibraryItems();
const { fetchItems } = useLibraryActions();
```
### 3. Use Shallow Comparison for Objects
When selecting objects, use `useShallow` or the provided selectors:
```typescript
// ❌ BAD: New object reference on every render
const { user, isAuthenticated } = useAuthStore((state) => ({
user: state.user,
isAuthenticated: state.isAuthenticated,
}));
// ✅ GOOD: Shallow comparison prevents unnecessary re-renders
const { user, isAuthenticated } = useStoreSelector(
useAuthStore,
(state) => ({
user: state.user,
isAuthenticated: state.isAuthenticated,
}),
);
```
### 4. Memoize Derived Values
If you need to compute values from store data, use `useMemo`:
```typescript
import { useMemo } from 'react';
import { useLibraryItems } from '@/utils/storeSelectors';
function MyComponent() {
const items = useLibraryItems();
// Memoize expensive computation
const favoriteCount = useMemo(
() => items.filter((item) => item.is_favorite).length,
[items],
);
return <div>Favorites: {favoriteCount}</div>;
}
```
## Performance Benefits
Using optimized selectors provides several benefits:
1. **Fewer Re-renders**: Components only re-render when their selected values change
2. **Better Performance**: Reduces unnecessary React reconciliation
3. **Predictable Updates**: Easier to reason about when components update
4. **Better DevTools**: Clearer view of what triggers re-renders
## Migration Guide
### Before (Unoptimized)
```typescript
function UserProfile() {
const { user, isAuthenticated, isLoading, error } = useAuthStore();
const { theme, language } = useUIStore();
// Re-renders on ANY change to auth or UI stores
return <div>{user?.username}</div>;
}
```
### After (Optimized)
```typescript
import {
useAuthUser,
useUITheme,
useUILanguage,
} from '@/utils/storeSelectors';
function UserProfile() {
const user = useAuthUser();
const theme = useUITheme();
const language = useUILanguage();
// Only re-renders when user, theme, or language actually change
return <div>{user?.username}</div>;
}
```
## Testing
When testing components that use selectors, you can still use the store directly:
```typescript
import { render } from '@testing-library/react';
import { useAuthStore } from '@/stores/auth';
test('renders user profile', () => {
// Set up store state
useAuthStore.setState({
user: { id: '1', username: 'testuser' },
isAuthenticated: true,
});
const { getByText } = render(<UserProfile />);
expect(getByText('testuser')).toBeInTheDocument();
});
```
## Related Documentation
- [Zustand Selectors](https://github.com/pmndrs/zustand#selecting-multiple-state-slices)
- [Zustand Shallow Comparison](https://github.com/pmndrs/zustand#shallow-equality-check)
- [React Performance Optimization](https://react.dev/learn/render-and-commit)

View file

@ -0,0 +1,198 @@
/**
* 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 { useAuthStore } from '@/stores/auth';
import { useUIStore } from '@/stores/ui';
import { useLibraryStore } from '@/stores/library';
import { useChatStore } from '@/stores/chat';
/**
* FE-STATE-008: Optimized selectors for AuthStore
*
* These hooks only re-render when the selected values actually change.
*/
export function useAuthUser() {
return useAuthStore(useShallow((state) => state.user));
}
export function useAuthStatus() {
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() {
return useUIStore(useShallow((state) => state.theme));
}
export function useUILanguage() {
return useUIStore(useShallow((state) => state.language));
}
export function useUISidebar() {
return useUIStore(useShallow((state) => ({
sidebarOpen: state.sidebarOpen,
setSidebarOpen: state.setSidebarOpen,
})));
}
export function useUINotifications() {
return useUIStore(useShallow((state) => ({
notifications: state.notifications,
addNotification: state.addNotification,
removeNotification: state.removeNotification,
markNotificationAsRead: state.markNotificationAsRead,
clearNotifications: state.clearNotifications,
})));
}
export function useUIActions() {
return useUIStore(useShallow((state) => ({
setTheme: state.setTheme,
setLanguage: state.setLanguage,
setSidebarOpen: state.setSidebarOpen,
})));
}
/**
* FE-STATE-008: Optimized selectors for LibraryStore
*/
export function useLibraryItems() {
return useLibraryStore(useShallow((state) => state.items));
}
export function useLibraryFavorites() {
return useLibraryStore(useShallow((state) => state.favorites));
}
export function useLibraryFilters() {
return useLibraryStore(useShallow((state) => ({
filters: state.filters,
setFilters: state.setFilters,
})));
}
export function useLibraryPagination() {
return useLibraryStore(useShallow((state) => state.pagination));
}
export function useLibraryStatus() {
return useLibraryStore(useShallow((state) => ({
isLoading: state.isLoading,
error: state.error,
})));
}
export function useLibraryActions() {
return useLibraryStore(useShallow((state) => ({
fetchItems: state.fetchItems,
fetchFavorites: state.fetchFavorites,
uploadFile: state.uploadFile,
toggleFavorite: state.toggleFavorite,
deleteItem: state.deleteItem,
clearItems: state.clearItems,
})));
}
/**
* FE-STATE-008: Optimized selectors for ChatStore
*/
export function useChatConversations() {
return useChatStore(useShallow((state) => state.conversations));
}
export function useChatCurrentConversation() {
return useChatStore(useShallow((state) => state.currentConversation));
}
export function useChatMessages(conversationId: string) {
return useChatStore(useShallow((state) => state.messages[conversationId] || []));
}
export function useChatTypingUsers(conversationId: string) {
return useChatStore(useShallow((state) => state.typingUsers[conversationId] || []));
}
export function useChatConnection() {
return useChatStore(useShallow((state) => ({
isConnected: state.isConnected,
isLoading: state.isLoading,
error: state.error,
})));
}
export function useChatActions() {
return useChatStore(useShallow((state) => ({
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));
}