Files
Todo/hooks
Paul Bailey fc5ad9f7af Improve real-time sync with 1-second polling and visual indicator
- Reduced polling interval from 2s to 1s for near-instant updates
- Added SyncIndicator component to show sync status
- Added console logging to debug sync behavior
- Updates across devices now appear within 1 second
- Visual feedback shows when data is being refreshed
2025-12-03 00:29:24 +00:00
..

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:

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 (
    <input 
      value={query} 
      onChange={(e) => 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:

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 (
    <div>
      <button onClick={() => updateFilter('priority', 'high')}>
        High Priority
      </button>
      {hasActiveFilters && (
        <button onClick={clearFilters}>Clear Filters</button>
      )}
    </div>
  )
}

useOptimisticUpdate

Provides optimistic UI updates for better perceived performance.

Features:

  • Immediate UI feedback
  • Automatic rollback on error
  • Pending state tracking
  • Error handling

Usage:

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 (
    <button onClick={handleToggle} disabled={isPending}>
      {isPending ? 'Updating...' : 'Toggle'}
    </button>
  )
}

useOptimisticList

Specialized hook for managing lists with optimistic updates.

Usage:

import { useOptimisticList } from '@/hooks'

function MyComponent({ initialItems }) {
  const { optimisticList, isPending, addItem, updateItem, removeItem } = useOptimisticList(
    initialItems,
    async (updatedList) => {
      // Sync with server
      await syncList(updatedList)
    }
  )

  return (
    <div>
      {optimisticList.map(item => (
        <div key={item.id}>
          {item.title}
          <button onClick={() => removeItem(item.id)}>Delete</button>
        </div>
      ))}
    </div>
  )
}

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:

import { useScrollPosition } from '@/hooks'

function MyComponent({ listId }) {
  // Automatically saves and restores scroll position
  useScrollPosition({ 
    key: `list-${listId}`,
    enabled: true,
    debounceMs: 100
  })

  return (
    <div>
      {/* Your scrollable content */}
    </div>
  )
}

useContainerScrollPosition

Similar to useScrollPosition but for specific container elements.

Usage:

import { useContainerScrollPosition } from '@/hooks'

function MyComponent({ listId }) {
  const { containerRef } = useContainerScrollPosition({ 
    key: `container-${listId}` 
  })

  return (
    <div ref={containerRef} style={{ height: '400px', overflow: 'auto' }}>
      {/* Your scrollable content */}
    </div>
  )
}

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:

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 (
    <div>
      {!isOnline && <div>Offline - changes will sync when online</div>}
      {isSyncing && <div>Syncing {queueLength} changes...</div>}
      {syncError && <div>Sync error: {syncError.message}</div>}
    </div>
  )
}

Offline-Aware Actions:

For convenience, use the offline-aware action wrappers from lib/offline-actions.ts:

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:

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')
})