- 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
296 lines
6.8 KiB
Markdown
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')
|
|
})
|
|
```
|