# Custom Hooks Documentation This directory contains custom React hooks for client-side state management in the Ultimate Lists App. ## Hooks Overview ### `useSearch` Manages search state with debouncing functionality. **Features:** - Automatic debouncing of search queries - Immediate clear functionality - Tracks debouncing state **Usage:** ```typescript import { useSearch } from '@/hooks' function MyComponent() { const { query, setQuery, debouncedQuery, isDebouncing, clearSearch } = useSearch({ debounceMs: 300, onSearch: async (query) => { // Perform search with debounced query const results = await searchItems(query) } }) return ( setQuery(e.target.value)} /> ) } ``` ### `useFilters` Manages filter state for sorting and filtering items. **Features:** - Centralized filter state management - Detects active filters - Easy filter clearing - Individual filter updates **Usage:** ```typescript import { useFilters } from '@/hooks' function MyComponent() { const { filters, setFilters, updateFilter, clearFilters, hasActiveFilters } = useFilters({ initialFilters: { sortBy: 'createdAt' }, onFiltersChange: async (filters) => { // Apply filters const results = await getItemsByFilter(listId, filters) } }) return (
{hasActiveFilters && ( )}
) } ``` ### `useOptimisticUpdate` Provides optimistic UI updates for better perceived performance. **Features:** - Immediate UI feedback - Automatic rollback on error - Pending state tracking - Error handling **Usage:** ```typescript import { useOptimisticUpdate } from '@/hooks' function MyComponent() { const { optimisticData, isPending, error, performUpdate } = useOptimisticUpdate({ onUpdate: async (data) => { await updateItem(data.id, data) }, onError: (error) => { console.error('Update failed:', error) } }) const handleToggle = async () => { await performUpdate({ id: '123', completed: true }) } return ( ) } ``` ### `useOptimisticList` Specialized hook for managing lists with optimistic updates. **Usage:** ```typescript import { useOptimisticList } from '@/hooks' function MyComponent({ initialItems }) { const { optimisticList, isPending, addItem, updateItem, removeItem } = useOptimisticList( initialItems, async (updatedList) => { // Sync with server await syncList(updatedList) } ) return (
{optimisticList.map(item => (
{item.title}
))}
) } ``` ### `useScrollPosition` Preserves scroll position across navigation for better UX. **Features:** - Automatic scroll position saving - Restoration on component mount - Debounced save operations - SessionStorage-based persistence **Usage:** ```typescript import { useScrollPosition } from '@/hooks' function MyComponent({ listId }) { // Automatically saves and restores scroll position useScrollPosition({ key: `list-${listId}`, enabled: true, debounceMs: 100 }) return (
{/* Your scrollable content */}
) } ``` ### `useContainerScrollPosition` Similar to `useScrollPosition` but for specific container elements. **Usage:** ```typescript import { useContainerScrollPosition } from '@/hooks' function MyComponent({ listId }) { const { containerRef } = useContainerScrollPosition({ key: `container-${listId}` }) return (
{/* Your scrollable content */}
) } ``` ## Requirements Validation These hooks implement the following requirements from the specification: - **Requirement 6.4**: Filter state management and clearing - **Requirement 7.3**: Visual feedback for user actions (optimistic updates) - **Requirement 7.4**: Scroll position preservation - **Requirement 8.3**: Search clearing and restoration ## Best Practices 1. **Search Debouncing**: Use appropriate debounce times (300ms is a good default) 2. **Filter State**: Keep filter state in sync with URL params for shareable links 3. **Optimistic Updates**: Always provide rollback on error 4. **Scroll Position**: Use unique keys for different views to avoid conflicts 5. **Error Handling**: Always handle errors in async callbacks ## Testing These hooks are designed to be testable with React Testing Library: ```typescript import { renderHook, act } from '@testing-library/react' import { useSearch } from '@/hooks' test('useSearch debounces queries', async () => { const onSearch = jest.fn() const { result } = renderHook(() => useSearch({ onSearch, debounceMs: 100 })) act(() => { result.current.setQuery('test') }) expect(onSearch).not.toHaveBeenCalled() await act(async () => { await new Promise(resolve => setTimeout(resolve, 150)) }) expect(onSearch).toHaveBeenCalledWith('test') }) ```