Frontend Architecture Patterns
Status: Complete
Generated: 2025-01-20
Overview
This document outlines key frontend architecture patterns and best practices learned from building the Sasha AI knowledge management system.
Component Patterns
Tab-Based UI Components
When implementing tabbed interfaces with shared functionality:
Problematic Pattern: CSS-Hidden Components
// All components are mounted but only one is visible
<div className={`${activeTab === 'chat' ? 'block' : 'hidden'}`}>
<ChatInterface />
</div>
<div className={`${activeTab === 'guides' ? 'block' : 'hidden'}`}>
<ChatInterface />
</div>
<div className={`${activeTab === 'personas' ? 'block' : 'hidden'}`}>
<ChatInterface />
</div>
Issues:
- All components remain mounted and active
- Event listeners fire for all instances
- Global events (WebSocket, notifications) trigger multiple times
- Memory overhead from unused components
Recommended Pattern: Tab-Aware Components
<ChatInterface
activeTab={activeTab}
currentTab="chat"
// Component handles its own active state
/>
<ChatInterface
activeTab={activeTab}
currentTab="guides"
/>
// In ChatInterface component:
const isActive = activeTab === currentTab;
useEffect(() => {
if (!isActive) return; // Skip effects when inactive
// Only process global events when active
if (message.type === 'global-notification' && isActive) {
showNotification(message.data);
}
}, [messages, isActive]);
Alternative Pattern: Conditional Rendering
{activeTab === 'chat' && <ChatInterface />}
{activeTab === 'guides' && <GuideInterface />}
{activeTab === 'personas' && <PersonaInterface />}
Trade-offs:
- No duplicate event handlers
- Component state lost when switching tabs
- Re-mounting performance cost
WebSocket Event Handling
Global Event Patterns
For events that affect the entire application (notifications, document updates):
Single Handler Pattern
// App.jsx - Single WebSocket handler
const handleWebSocketMessage = (message) => {
switch (message.type) {
case 'document-created':
// Only one place handles global notifications
showGlobalToast(message.data);
break;
}
};
// Pass down relevant data to components
<ChatInterface documentUpdates={documentUpdates} />
Component-Scoped Pattern
// Each component handles only relevant events
const ChatInterface = ({ activeTab, currentTab }) => {
useEffect(() => {
const handleMessage = (message) => {
if (message.type === 'document-created' && activeTab === currentTab) {
showToast(message.data);
}
};
ws.addEventListener('message', handleMessage);
return () => ws.removeEventListener('message', handleMessage);
}, [activeTab, currentTab]);
};
Notification Systems
Toast Notification Best Practices
Deduplication Strategies
// 1. Server-side deduplication
const documentOps = new Map();
const operationKey = `${toolName}:${normalizedPath}`;
if (!documentOps.has(operationKey)) {
documentOps.set(operationKey, Date.now());
sendNotification(data);
}
// 2. Client-side component scoping
const showToast = (message) => {
if (activeTab === currentTab) {
toast.show(message);
}
};
// 3. React Context deduplication
const ToastContext = () => {
const [shownToasts, setShownToasts] = useState(new Set());
const showToast = (id, message) => {
if (!shownToasts.has(id)) {
setShownToasts(prev => new Set([...prev, id]));
toast.show(message);
}
};
};
State Management Patterns
Session Protection System
For preventing UI updates during active operations:
// App.jsx - Session Protection
const [activeSessions, setActiveSessions] = useState(new Set());
const markSessionActive = (sessionId) => {
setActiveSessions(prev => new Set([...prev, sessionId]));
};
const markSessionInactive = (sessionId) => {
setActiveSessions(prev => {
const newSet = new Set(prev);
newSet.delete(sessionId);
return newSet;
});
};
// Skip updates during active sessions
useEffect(() => {
if (activeSessions.size > 0) {
// Skip project updates to prevent interrupting active chats
return;
}
updateProjects(latestData);
}, [messages, activeSessions]);
Component Lifecycle Management
Memory and Performance
Resource Cleanup
const ChatInterface = () => {
const timeoutsRef = useRef(new Set());
const intervalsRef = useRef(new Set());
const safeSetTimeout = (callback, delay) => {
const id = setTimeout(callback, delay);
timeoutsRef.current.add(id);
return id;
};
useEffect(() => {
return () => {
// Cleanup all timeouts
timeoutsRef.current.forEach(clearTimeout);
intervalsRef.current.forEach(clearInterval);
};
}, []);
};
Local Storage Management
const safeLocalStorage = {
setItem: (key, value) => {
try {
// Implement size limits for chat history
if (key.startsWith('chat_messages_')) {
const parsed = JSON.parse(value);
if (parsed.length > 50) {
value = JSON.stringify(parsed.slice(-50));
}
}
localStorage.setItem(key, value);
} catch (error) {
if (error.name === 'QuotaExceededError') {
// Clean up old data and retry
cleanupOldData();
localStorage.setItem(key, value);
}
}
}
};
Error Boundaries and Debugging
Component Error Handling
class ErrorBoundary extends React.Component {
constructor(props) {
super(props);
this.state = { hasError: false, error: null };
}
static getDerivedStateFromError(error) {
return { hasError: true, error };
}
componentDidCatch(error, errorInfo) {
console.error('Component error:', error, errorInfo);
// Report to monitoring service
if (this.props.showDetails) {
// Development mode - show detailed error
}
}
render() {
if (this.state.hasError) {
return this.props.fallback || <div>Something went wrong.</div>;
}
return this.props.children;
}
}
// Usage
<ErrorBoundary showDetails={isDevelopment}>
<ChatInterface />
</ErrorBoundary>
Testing Patterns
Component Testing
// Test multiple component instances
describe('Tabbed ChatInterface', () => {
test('only active tab shows notifications', () => {
render(
<>
<ChatInterface activeTab="chat" currentTab="chat" />
<ChatInterface activeTab="chat" currentTab="guides" />
</>
);
// Simulate WebSocket message
fireEvent(mockWebSocket, createDocumentMessage());
// Only one toast should appear
expect(screen.getAllByRole('alert')).toHaveLength(1);
});
});
Key Principles
- Single Responsibility: One component handles one type of global event
- Tab Awareness: Components know when they're active/inactive
- Resource Cleanup: Always clean up timeouts, intervals, and listeners
- Graceful Degradation: Handle localStorage quota and network failures
- Debug Visibility: Log component actions for troubleshooting
Anti-Patterns to Avoid
- Hidden components with active event listeners
- Multiple components handling the same global events
- Unbounded localStorage usage
- Missing cleanup in useEffect hooks
- Complex server-side deduplication for simple client-side issues
Related Documents: