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

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

  1. Single Responsibility: One component handles one type of global event
  2. Tab Awareness: Components know when they're active/inactive
  3. Resource Cleanup: Always clean up timeouts, intervals, and listeners
  4. Graceful Degradation: Handle localStorage quota and network failures
  5. 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: