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

6.6 KiB

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

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

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

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:

// Configure deduplication window (default: 100ms)
socketService.setDeduplicationWindow(200); // 200ms window

// Duplicate messages within the window are automatically filtered

Connection Management

// 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:

// ❌ 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:

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:

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:

// 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.