6.6 KiB
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:
// ❌ 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:
// ✅ GOOD: Only re-renders when `user` changes
function MyComponent() {
const user = useAuthUser();
return <div>{user?.username}</div>;
}
Available Selectors
Auth Store Selectors
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
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
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
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:
import { useStoreSelector } from '@/utils/storeSelectors';
import { useAuthStore } from '@/features/auth/store/authStore';
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:
// ❌ 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:
// ❌ 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:
// ❌ 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:
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:
- Fewer Re-renders: Components only re-render when their selected values change
- Better Performance: Reduces unnecessary React reconciliation
- Predictable Updates: Easier to reason about when components update
- Better DevTools: Clearer view of what triggers re-renders
Migration Guide
Before (Unoptimized)
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)
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:
import { render } from '@testing-library/react';
import { useAuthStore } from '@/features/auth/store/authStore';
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();
});