# Sidebar Component Documentation ## Architecture ``` src/ ├── components/Sidebar/ │ ├── Sidebar.tsx # Main container component │ ├── SidebarItem.tsx # Individual menu item │ ├── SidebarSubmenu.tsx # Submenu component │ └── SidebarUser.tsx # User info display ├── machines/ │ └── sidebarMachine.ts # State machine definition └── hooks/machines/ │ └── useSidebarMachine.ts # React hook integration └── contexts/ └── SidebarData.tsx # Navigation data provider ``` ## State Machine Explanation ### Core Concepts **Events** - Things that can happen: - `TOGGLE_ITEM` - User clicks a menu item - `CLOSE_ALL` - Close all submenus - `NAVIGATE` - User navigates to a new route **Context** - Data the machine remembers: ```typescript interface SidebarContext { openItemId: string | null; // Which submenu is open (only one allowed) activePath: string; // Current route for highlighting } ``` **States** - Possible configurations: - `collapsed` - No submenus are open - `expanded` - One submenu is open ### State Transitions ```mermaid stateDiagram-v2 [*] --> collapsed collapsed --> expanded : TOGGLE_ITEM(id) expanded --> collapsed : TOGGLE_ITEM(same_id) expanded --> expanded : TOGGLE_ITEM(different_id) collapsed --> collapsed : CLOSE_ALL expanded --> collapsed : CLOSE_ALL ``` ### Machine Definition ```typescript export const sidebarMachine = setup({ types: { context: {} as SidebarContext, events: {} as SidebarEvent, }, }).createMachine({ id: 'sidebar', initial: 'collapsed', context: { openItemId: null, activePath: '/', }, states: { collapsed: { on: { TOGGLE_ITEM: { target: 'expanded', actions: assign({ openItemId: ({ event }) => event.itemId, }), }, }, }, expanded: { on: { TOGGLE_ITEM: [ { // Same item clicked - close it guard: ({ context, event }) => context.openItemId === event.itemId, target: 'collapsed', actions: assign({ openItemId: null }), }, { // Different item clicked - switch to it target: 'expanded', actions: assign({ openItemId: ({ event }) => event.itemId, }), }, ], }, }, }, }); ``` ## React Hook Integration ### useSidebarMachine Hook The `useSidebarMachine` hook bridges XState with React components: ```typescript export const useSidebarMachine = () => { // 1. Create machine actor const [state, send] = useActor(sidebarMachine); // 2. Sync with React Router const location = useLocation(); useEffect(() => { send({ type: 'NAVIGATE', path: location.pathname }); }, [location.pathname, send]); // 3. Return easy-to-use API return { // State queries hasOpenSubmenu: sidebarState.hasOpenSubmenu, openItemId: sidebarState.openItemId, currentState: state.value, // Actions toggleItem: (itemId: string) => send({ type: 'TOGGLE_ITEM', itemId }), closeAll: () => send({ type: 'CLOSE_ALL' }), isItemOpen: (itemId: string) => sidebarState.isItemOpen(itemId), isItemActive: (itemPath?: string) => location.pathname === itemPath, }; }; ``` ### Hook Benefits 1. **Encapsulation**: State machine logic is hidden from components 2. **Type Safety**: TypeScript ensures correct usage 3. **Automatic Sync**: Route changes update the machine automatically 4. **Simple API**: Components only need simple functions ## Data Loading (SidebarData) ### Structure ```typescript interface SidebarItemType { id: string; name: string; link?: string; icon?: React.ComponentType; submenu?: SidebarItemType[]; } ``` ### Implementation ```typescript const useSidebarData = () => { const { t } = useLanguage(); return useMemo(() => [ { id: '1', name: t('nav.team'), link: '/team-bereich', icon: MdOutlineWorkOutline, }, { id: '2', name: t('nav.dashboard'), link: '/dashboard', icon: LuTicket, submenu: [ // Optional submenu { id: '2-1', name: 'Analytics', link: '/dashboard/analytics' }, { id: '2-2', name: 'Reports', link: '/dashboard/reports' }, ], }, ], [t]); }; ``` ### Key Features - **Internationalization**: Uses language context for translations - **Memoization**: Prevents unnecessary re-renders - **Flexible Structure**: Supports nested submenus - **Icon Support**: React component icons ## Component Interactions ### Sidebar (Main Container) ```typescript const Sidebar: React.FC = ({ data }) => { const sidebarMachine = useSidebarMachine(); return (
{/* Logo and User sections */}
{data.map(item => ( sidebarMachine.toggleItem(item.id)} isActive={sidebarMachine.isItemActive(item.link)} /> ))}
); }; ``` **Responsibilities:** - Initialize state machine - Pass state and actions to children - Handle layout and styling ### SidebarItem (Individual Menu Item) ```typescript const SidebarItem: React.FC = ({ item, isOpen, onToggle, isActive }) => { const hasSubItems = item.submenu && item.submenu.length > 0; const toggleSubmenu = (e: React.MouseEvent) => { if (hasSubItems) { e.preventDefault(); onToggle(); // Call parent's toggle function } }; return (
  • {hasSubItems ? ( {item.name} ) : ( {item.name} )}
  • {hasSubItems && }
    ); }; ``` **Key Changes from Original:** - ❌ **Removed**: `useState` for local state - ✅ **Added**: Props from parent state machine - ✅ **Benefit**: No independent state management ### SidebarSubmenu (Nested Menu) ```typescript const SidebarSubmenu: React.FC = ({ item, isOpen }) => { return ( {isOpen && ( {/* Submenu items */} )} ); }; ``` **Responsibilities:** - Render submenu when `isOpen` is true - Handle animations with Framer Motion - Manage text overflow behavior ## Data Flow ``` 1. User clicks menu item ↓ 2. SidebarItem calls onToggle() ↓ 3. onToggle sends TOGGLE_ITEM event to machine ↓ 4. Machine transitions states and updates context ↓ 5. Hook returns new state ↓ 6. Sidebar re-renders with new isOpen values ↓ 7. SidebarSubmenu shows/hides based on isOpen ``` ## Adding New States ### Example: Adding "Pinned" State 1. **Update Events:** ```typescript export type SidebarEvent = | { type: 'TOGGLE_ITEM'; itemId: string } | { type: 'PIN_SIDEBAR' } // New event | { type: 'UNPIN_SIDEBAR' } // New event | { type: 'CLOSE_ALL' } | { type: 'NAVIGATE'; path: string }; ``` 2. **Update Context:** ```typescript export interface SidebarContext { openItemId: string | null; activePath: string; isPinned: boolean; // New context property } ``` 3. **Add New States:** ```typescript states: { collapsed: { on: { PIN_SIDEBAR: 'pinnedCollapsed', // ... existing transitions }, }, expanded: { on: { PIN_SIDEBAR: 'pinnedExpanded', // ... existing transitions }, }, pinnedCollapsed: { // New state on: { UNPIN_SIDEBAR: 'collapsed', TOGGLE_ITEM: 'pinnedExpanded', }, }, pinnedExpanded: { // New state on: { UNPIN_SIDEBAR: 'expanded', // ... similar to expanded }, }, }, ``` 4. **Update Hook:** ```typescript return { // ... existing returns isPinned: state.matches('pinnedCollapsed') || state.matches('pinnedExpanded'), pinSidebar: () => send({ type: 'PIN_SIDEBAR' }), unpinSidebar: () => send({ type: 'UNPIN_SIDEBAR' }), }; ``` ## Best Practices ### State Machine Design 1. **Keep States Simple**: Each state should represent a distinct UI mode 2. **Use Guards Wisely**: For conditional transitions based on context 3. **Minimize Context**: Only store data that affects behavior 4. **Clear Event Names**: Use descriptive, action-oriented names ### Component Architecture 1. **Single Source of Truth**: State machine holds all navigation state 2. **Props Down**: Pass state and actions as props 3. **Events Up**: Send events to the machine, not direct state updates 4. **Separation of Concerns**: Components handle UI, machine handles logic ### Performance 1. **Memoize Data**: Use `useMemo` for sidebar data 2. **Avoid Deep Objects**: Keep context flat when possible 3. **Selective Subscriptions**: Only subscribe to needed state slices ## Debugging ### Development Tools 1. **Debug Props**: Hook returns `_debugState` and `_debugSend` 2. **State Logging**: Log state changes in development 3. **XState DevTools**: Use `@xstate/inspect` for visual debugging ### Common Issues 1. **Multiple Items Open**: Check guard conditions in TOGGLE_ITEM 2. **State Not Updating**: Ensure events are sent correctly 3. **Route Sync Issues**: Verify NAVIGATE event is sent on route change ## Testing ### Unit Testing State Machine ```typescript import { sidebarMachine } from '../machines/sidebarMachine'; test('should open submenu when item toggled', () => { const state = sidebarMachine.transition('collapsed', { type: 'TOGGLE_ITEM', itemId: 'menu-1', }); expect(state.value).toBe('expanded'); expect(state.context.openItemId).toBe('menu-1'); }); ``` ### Integration Testing ```typescript import { render, fireEvent } from '@testing-library/react'; import { SidebarWithData } from '../components/Sidebar'; test('should close submenu when same item clicked twice', () => { const { getByText } = render(); const menuItem = getByText('Dashboard'); fireEvent.click(menuItem); // Open fireEvent.click(menuItem); // Close expect(/* submenu is closed */).toBeTruthy(); }); ``` ## Migration Guide ### From useState to State Machine **Before:** ```typescript const [isOpen, setIsOpen] = useState(false); const toggle = () => setIsOpen(!isOpen); ``` **After:** ```typescript const { isItemOpen, toggleItem } = useSidebarMachine(); // Use: isItemOpen(itemId) and toggleItem(itemId) ``` ### Benefits of Migration 1. **Predictable State**: No more impossible state combinations 2. **Centralized Logic**: All sidebar behavior in one place 3. **Better Testing**: State machine logic is easily testable 4. **Type Safety**: TypeScript prevents invalid transitions 5. **Debugging**: Clear state visualization and logging