Archon/archon-ui-main/docs/socket-memoization-patterns.md

255 lines
6.6 KiB
Markdown

# Socket & Memoization Patterns
## Quick Reference
### DO:
- ✅ Track optimistic updates to prevent double-renders
- ✅ Memoize socket event handlers with useCallback
- ✅ Check if incoming data actually differs from current state
- ✅ Use debouncing for rapid UI updates (drag & drop)
- ✅ Clean up socket listeners in useEffect cleanup
### DON'T:
- ❌ Update state without checking if data changed
- ❌ Create new handler functions on every render
- ❌ Apply server updates that match pending optimistic updates
- ❌ Forget to handle the "modal open" edge case
## Pattern Examples
### Optimistic Update Pattern
```typescript
import { useOptimisticUpdates } from '../../hooks/useOptimisticUpdates';
const MyComponent = () => {
const { addPendingUpdate, isPendingUpdate } = useOptimisticUpdates<Task>();
const handleLocalUpdate = (task: Task) => {
// Track the optimistic update
addPendingUpdate({
id: task.id,
timestamp: Date.now(),
data: task,
operation: 'update'
});
// Update local state immediately
setTasks(prev => prev.map(t => t.id === task.id ? task : t));
// Persist to server
api.updateTask(task);
};
const handleServerUpdate = useCallback((task: Task) => {
// Skip if this is our own update echoing back
if (isPendingUpdate(task.id, task)) {
console.log('Skipping own optimistic update');
return;
}
// Apply server update
setTasks(prev => prev.map(t => t.id === task.id ? task : t));
}, [isPendingUpdate]);
};
```
### Socket Handler Pattern
```typescript
import { useSocketSubscription } from '../../hooks/useSocketSubscription';
const MyComponent = () => {
// Option 1: Using the hook
useSocketSubscription(
socketService,
'data_updated',
(data) => {
console.log('Data updated:', data);
// Handle update
},
[/* dependencies */]
);
// Option 2: Manual memoization
const handleUpdate = useCallback((message: any) => {
const data = message.data || message;
setItems(prev => {
// Check if data actually changed
const existing = prev.find(item => item.id === data.id);
if (existing && JSON.stringify(existing) === JSON.stringify(data)) {
return prev; // No change, prevent re-render
}
return prev.map(item => item.id === data.id ? data : item);
});
}, []);
useEffect(() => {
socketService.addMessageHandler('update', handleUpdate);
return () => {
socketService.removeMessageHandler('update', handleUpdate);
};
}, [handleUpdate]);
};
```
### Debounced Reordering Pattern
```typescript
const useReordering = () => {
const debouncedPersist = useMemo(
() => debounce(async (items: Item[]) => {
try {
await api.updateOrder(items);
} catch (error) {
console.error('Failed to persist order:', error);
// Rollback or retry logic
}
}, 500),
[]
);
const handleReorder = useCallback((dragIndex: number, dropIndex: number) => {
// Update UI immediately
setItems(prev => {
const newItems = [...prev];
const [draggedItem] = newItems.splice(dragIndex, 1);
newItems.splice(dropIndex, 0, draggedItem);
// Update order numbers
return newItems.map((item, index) => ({
...item,
order: index + 1
}));
});
// Persist changes (debounced)
debouncedPersist(items);
}, [items, debouncedPersist]);
};
```
## WebSocket Service Configuration
### Deduplication
The enhanced WebSocketService now includes automatic deduplication:
```typescript
// Configure deduplication window (default: 100ms)
socketService.setDeduplicationWindow(200); // 200ms window
// Duplicate messages within the window are automatically filtered
```
### Connection Management
```typescript
// Always check connection state before critical operations
if (socketService.isConnected()) {
socketService.send({ type: 'update', data: payload });
}
// Monitor connection state
socketService.addStateChangeHandler((state) => {
if (state === WebSocketState.CONNECTED) {
console.log('Connected - refresh data');
}
});
```
## Common Patterns
### 1. State Equality Checks
Always check if incoming data actually differs from current state:
```typescript
// ❌ BAD - Always triggers re-render
setTasks(prev => prev.map(t => t.id === id ? newTask : t));
// ✅ GOOD - Only updates if changed
setTasks(prev => {
const existing = prev.find(t => t.id === id);
if (existing && deepEqual(existing, newTask)) return prev;
return prev.map(t => t.id === id ? newTask : t);
});
```
### 2. Modal State Handling
Be aware of modal state when applying updates:
```typescript
const handleSocketUpdate = useCallback((data) => {
if (isModalOpen && editingItem?.id === data.id) {
console.warn('Update received while editing - consider skipping or merging');
// Option 1: Skip the update
// Option 2: Merge with current edits
// Option 3: Show conflict resolution UI
}
// Normal update flow
}, [isModalOpen, editingItem]);
```
### 3. Cleanup Pattern
Always clean up socket listeners:
```typescript
useEffect(() => {
const handlers = [
{ event: 'create', handler: handleCreate },
{ event: 'update', handler: handleUpdate },
{ event: 'delete', handler: handleDelete }
];
// Add all handlers
handlers.forEach(({ event, handler }) => {
socket.addMessageHandler(event, handler);
});
// Cleanup
return () => {
handlers.forEach(({ event, handler }) => {
socket.removeMessageHandler(event, handler);
});
};
}, [handleCreate, handleUpdate, handleDelete]);
```
## Performance Tips
1. **Measure First**: Use React DevTools Profiler before optimizing
2. **Batch Updates**: Group related state changes
3. **Debounce Rapid Changes**: Especially for drag & drop operations
4. **Use Stable References**: Memoize callbacks passed to child components
5. **Avoid Deep Equality Checks**: Use optimized comparison for large objects
## Debugging
Enable verbose logging for troubleshooting:
```typescript
// In development
if (process.env.NODE_ENV === 'development') {
console.log('[Socket] Message received:', message);
console.log('[Socket] Deduplication result:', isDuplicate);
console.log('[Optimistic] Pending updates:', pendingUpdates);
}
```
## Migration Guide
To migrate existing components:
1. Import `useOptimisticUpdates` hook
2. Wrap socket handlers with `useCallback`
3. Add optimistic update tracking to local changes
4. Check for pending updates in socket handlers
5. Test with React DevTools Profiler
Remember: The goal is to eliminate unnecessary re-renders while maintaining real-time synchronization across all connected clients.