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
- No immediate state clearing when switching sessions
- Missing request cancellation for pending API calls
- No WebSocket message validation against current session
- 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.