Files
Todo/hooks/README.md
Paul Bailey 9726f3a9e1 feat: implement offline synchronization with mutation queue and background sync
- Created useOfflineSync hook for connectivity detection and sync management
- Implemented localStorage-based mutation queue for offline operations
- Added background sync support in service worker
- Created OfflineIndicator component to show connection status
- Added offline-aware action wrappers for seamless offline/online transitions
- Implemented conflict handling with retry logic (up to 3 retries)
- Auto-sync when connection is restored
- Validates Requirements 4.5
2025-12-01 20:34:21 +00:00

296 lines
6.8 KiB
Markdown

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