Last updated: Sep 1, 2025, 01:10 PM UTC

Session Management Lessons Learned

Status: Complete
Date: 2025-08-23
Issue: Session switching message cross-contamination
Resolution Commit: 71c4dff

Problem Overview

Session switching in React applications with WebSocket connections and asynchronous API calls can lead to complex race conditions and state management issues. This document captures lessons learned from resolving a critical session switching bug in Sasha Studio.

The Issue

Symptoms

  • Messages from different sessions appearing mixed together
  • Duplicate API calls (6+ identical requests)
  • Stale data persisting when switching sessions
  • Race conditions during rapid session switches

Root Causes

  1. No immediate state clearing when switching sessions
  2. Missing request cancellation for pending API calls
  3. No WebSocket message validation against current session
  4. Poor useEffect dependency management causing function recreation

The Solution Framework

1. Immediate State Clearing Pattern

useEffect(() => {
  // CRITICAL: Clear state IMMEDIATELY on dependency change
  setChatMessages([]);
  setSessionMessages([]);
  
  // Then proceed with loading new data
  const loadData = async () => {
    // ... load logic
  };
  loadData();
}, [sessionId, projectId]);

Why this works:

  • Prevents any stale data from lingering
  • Gives immediate visual feedback to user
  • Eliminates cross-contamination at the source

2. Abort Controller Pattern

const loadingControllerRef = useRef(null);

const loadData = useCallback(async (id) => {
  // Cancel any pending request
  if (loadingControllerRef.current) {
    loadingControllerRef.current.abort();
  }
  
  // Create new controller
  const controller = new AbortController();
  loadingControllerRef.current = controller;
  
  try {
    const response = await fetch(url, {
      signal: controller.signal
    });
    // ... handle response
  } catch (error) {
    if (error.name === 'AbortError') {
      console.log('Request cancelled');
      return [];
    }
    throw error;
  } finally {
    loadingControllerRef.current = null;
  }
}, []);

Why this works:

  • Prevents duplicate API calls during rapid changes
  • Eliminates race conditions from overlapping requests
  • Graceful handling of cancelled requests

3. WebSocket Message Validation Pattern

case 'message-type':
  // Validate message belongs to current context
  const messageSessionId = message.session_id;
  if (messageSessionId && currentSessionId && messageSessionId !== currentSessionId) {
    console.log('Ignoring message from different session');
    return; // Don't process
  }
  
  // Process message for current session
  processMessage(message);
  break;

Why this works:

  • Prevents cross-contamination from real-time messages
  • Maintains session isolation in multi-session apps
  • Simple but effective validation

4. Optimized useEffect Dependencies Pattern

// Use primitive values, not objects
useEffect(() => {
  let mounted = true;
  
  const loadData = async () => {
    if (!mounted) return;
    // ... load logic
  };
  
  loadData();
  
  return () => {
    mounted = false;
    // Cancel any ongoing operations
    if (controllerRef.current) {
      controllerRef.current.abort();
    }
  };
}, [session?.id, project?.name]); // Primitive values only

Why this works:

  • Prevents unnecessary re-renders and function recreation
  • Proper cleanup prevents memory leaks
  • Mounted flag prevents state updates on unmounted components

Implementation Checklist

State Management

  • Clear all relevant state immediately on context change
  • Use primitive values in useEffect dependencies
  • Implement proper cleanup functions
  • Add mounted flags to prevent stale updates

Request Management

  • Implement abort controllers for all async requests
  • Handle AbortError gracefully
  • Cancel pending requests on context change
  • Prevent duplicate requests during rapid changes

Real-time Message Handling

  • Validate message context before processing
  • Log ignored messages for debugging
  • Maintain session/context isolation
  • Handle edge cases (missing IDs, malformed messages)

Testing Strategy

  • Test rapid context switching
  • Monitor network tab for duplicate requests
  • Verify state isolation between contexts
  • Test WebSocket message filtering

Performance Metrics

Before Fix

  • API Calls: 6+ duplicate requests per session switch
  • Memory: Accumulated stale messages across sessions
  • UX: Visible cross-contamination and flickering
  • Reliability: Race conditions causing unpredictable behavior

After Fix

  • API Calls: Single request per session switch
  • Memory: Immediate cleanup, no accumulation
  • UX: Instant state clearing, smooth transitions
  • Reliability: Deterministic behavior with proper isolation

Common Pitfalls to Avoid

1. Object Dependencies in useEffect

Wrong:

}, [selectedSession, selectedProject]);

Correct:

}, [selectedSession?.id, selectedProject?.name]);

2. No Request Cancellation

Wrong:

const loadData = async () => {
  const response = await fetch(url);
  // No way to cancel this request
};

Correct:

const loadData = async () => {
  if (controllerRef.current) {
    controllerRef.current.abort();
  }
  const controller = new AbortController();
  const response = await fetch(url, { signal: controller.signal });
};

3. No Message Validation

Wrong:

case 'message':
  processMessage(message); // Processes all messages
  break;

Correct:

case 'message':
  if (message.contextId !== currentContextId) {
    return; // Ignore messages from other contexts
  }
  processMessage(message);
  break;

Debugging Techniques

1. Comprehensive Logging

console.log('πŸ”₯ [SESSION-EFFECT] useEffect triggered:', {
  selectedSession: selectedSession?.id,
  selectedProject: selectedProject?.name,
  trigger: 'dependency change',
  timestamp: new Date().toISOString()
});

2. Request Tracking

console.log('πŸš€ [API-CALL] Starting request:', {
  endpoint,
  params,
  timestamp: new Date().toISOString()
});

3. Message Filtering Logs

console.log('🚫 [SESSION-VALIDATION] Ignoring message:', {
  currentSession,
  messageSession,
  reason: 'session-mismatch'
});

Architectural Recommendations

1. State Isolation

  • Each session/context should have completely isolated state
  • Shared state should be minimal and carefully managed
  • Use context providers for session-specific data

2. Request Management

  • Centralize API call logic with built-in cancellation
  • Implement request deduplication for identical calls
  • Use loading states to prevent user confusion

3. Real-time Integration

  • Always validate real-time messages against current context
  • Implement message queuing for context switches
  • Handle connection state changes gracefully

Future Prevention

Code Review Checklist

  • Are async operations properly cancelled?
  • Is state cleared immediately on context changes?
  • Are real-time messages validated?
  • Are useEffect dependencies primitive values?

Testing Requirements

  • Session/context switching tests
  • Race condition tests
  • Memory leak tests
  • Network request deduplication tests

Related Documentation


This document serves as a reference for implementing robust session management in React applications with real-time features. The patterns described here can be applied to similar state management challenges across the application.