# 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 */}
) } ``` ### `useOfflineSync` Manages offline synchronization with automatic mutation queuing and background sync. **Features:** - Automatic online/offline detection - Mutation queue for offline operations - Background sync when connection restored - Conflict handling - Retry logic with exponential backoff **Usage:** ```typescript import { useOfflineSync } from '@/hooks' function MyComponent() { const { isOnline, isSyncing, syncError, queueLength, queueMutation, triggerSync, clearQueue } = useOfflineSync() // Queue a mutation when offline const handleCreateItem = async () => { if (!isOnline) { queueMutation({ type: 'create', entity: 'item', data: { listId: '123', title: 'New Item' } }) } else { await createItem('123', 'New Item') } } return (
{!isOnline &&
Offline - changes will sync when online
} {isSyncing &&
Syncing {queueLength} changes...
} {syncError &&
Sync error: {syncError.message}
}
) } ``` **Offline-Aware Actions:** For convenience, use the offline-aware action wrappers from `lib/offline-actions.ts`: ```typescript import { createItemOffline, updateItemOffline, deleteItemOffline } from '@/lib/offline-actions' // These automatically queue mutations when offline await createItemOffline(listId, title, description, metadata) await updateItemOffline(itemId, updates) await deleteItemOffline(itemId) ``` ## Requirements Validation These hooks implement the following requirements from the specification: - **Requirement 4.5**: Offline synchronization when connectivity restored - **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') }) ```