# Architecture Improvements for Booking Detail Page ## Current Issues 1. **State Explosion**: 14+ useState hooks make state management complex 2. **Mixed Concerns**: UI state, domain state, and loading states all at same level 3. **No Type Safety for Errors**: String-based errors lose context 4. **Inline Handlers**: Complex async logic in JSX callbacks 5. **No State Machine**: Can end up in invalid states ## Recommended Design Patterns ### 1. **Result Pattern** (Rust-like) Instead of: ```typescript const [error, setError] = useState(''); const [success, setSuccess] = useState(''); const [loading, setLoading] = useState(false); ``` Use: ```typescript type Result = | { status: 'idle' } | { status: 'loading' } | { status: 'success'; data: T } | { status: 'error'; error: E }; const [bookingResult, setBookingResult] = useState>({ status: 'idle' }); ``` **Benefits**: - Can't have loading=true and error set simultaneously - TypeScript enforces checking status before accessing data - Clear state transitions ### 2. **Custom Hooks for State Logic** Extract state management into custom hooks: ```typescript // hooks/useBookingData.ts export function useBookingData(slotId: string) { const [result, setResult] = useState>({ status: 'loading' }); const refetch = useCallback(async () => { setResult({ status: 'loading' }); try { const data = await fetchBooking(slotId); setResult({ status: 'success', data }); } catch (error) { setResult({ status: 'error', error: error as Error }); } }, [slotId]); useEffect(() => { refetch(); }, [refetch]); return { result, refetch }; } // hooks/useSwapActions.ts export function useSwapActions(slotId: string, onSuccess: () => void) { const [actionResult, setActionResult] = useState>({ status: 'idle' }); const approveSwap = useCallback(async (approvalId: number) => { setActionResult({ status: 'loading' }); try { await api.approveSwap(slotId, approvalId); setActionResult({ status: 'success', data: undefined }); onSuccess(); } catch (error) { setActionResult({ status: 'error', error: error as Error }); } }, [slotId, onSuccess]); const cancelSwap = useCallback(async (swapGroupId: string) => { // Similar pattern }, [slotId, onSuccess]); return { actionResult, approveSwap, cancelSwap }; } ``` **Benefits**: - Testable in isolation - Reusable across components - Single responsibility - Can use React Query or SWR patterns ### 3. **useReducer for Complex State** For UI state with related updates: ```typescript type UIState = { modals: { picker: boolean; login: boolean; matchType: boolean; punctuality: boolean; }; selection: { joinPosition: number | null; reportSlot: any | null; }; swaps: { recorded: [number, number][]; isProcessing: boolean; forceSwap: boolean; }; }; type UIAction = | { type: 'OPEN_MODAL'; modal: keyof UIState['modals'] } | { type: 'CLOSE_MODAL'; modal: keyof UIState['modals'] } | { type: 'SET_JOIN_POSITION'; position: number | null } | { type: 'START_SWAP'; from: number; to: number } | { type: 'COMPLETE_SWAP' } | { type: 'RESET_SWAPS' }; function uiReducer(state: UIState, action: UIAction): UIState { switch (action.type) { case 'OPEN_MODAL': return { ...state, modals: { ...state.modals, [action.modal]: true } }; case 'CLOSE_MODAL': return { ...state, modals: { ...state.modals, [action.modal]: false } }; case 'START_SWAP': return { ...state, swaps: { ...state.swaps, isProcessing: true, recorded: [...state.swaps.recorded, [action.from, action.to]] } }; case 'COMPLETE_SWAP': return { ...state, swaps: { ...state.swaps, isProcessing: false } }; // ... other cases default: return state; } } const [uiState, dispatch] = useReducer(uiReducer, initialUIState); ``` **Benefits**: - Related state updates happen atomically - Clear action types (self-documenting) - Easy to add middleware (logging, analytics) - Time-travel debugging possible ### 4. **Command Pattern for Actions** Create command objects for async operations: ```typescript // commands/SwapCommands.ts interface Command { execute(): Promise; undo?(): Promise; } class ApproveSwapCommand implements Command { constructor( private slotId: string, private approvalId: number, private api: BookingAPI ) {} async execute(): Promise { return this.api.approveSwap(this.slotId, this.approvalId); } } class CancelSwapCommand implements Command { constructor( private slotId: string, private swapGroupId: string, private api: BookingAPI ) {} async execute(): Promise { return this.api.cancelSwap(this.slotId, this.swapGroupId); } } // Usage const command = new ApproveSwapCommand(slotId, approvalId, api); await executeCommand(command, { onSuccess: () => setSuccess('Approved!'), onError: (error) => setError(error.message) }); ``` **Benefits**: - Testable business logic - Can add undo/redo - Easy to add logging, retry logic, etc. - Separation of concerns ### 5. **Error Boundary with Typed Errors** ```typescript // types/errors.ts export class BookingError extends Error { constructor( message: string, public code: string, public severity: 'fatal' | 'recoverable' ) { super(message); this.name = 'BookingError'; } } export class NetworkError extends BookingError { constructor(message: string) { super(message, 'NETWORK_ERROR', 'recoverable'); } } export class BookingNotFoundError extends BookingError { constructor(slotId: string) { super(`Booking ${slotId} not found`, 'NOT_FOUND', 'fatal'); } } // Component function handleError(error: unknown) { if (error instanceof BookingError) { if (error.severity === 'fatal') { setFatalError(error); } else { setError(error); } } else { setError(new BookingError('Unknown error', 'UNKNOWN', 'recoverable')); } } ``` **Benefits**: - Type-safe error handling - Clear error severity - Can add retry strategies per error type - Better error reporting/logging ### 6. **Service Layer** Extract API calls into a service: ```typescript // services/BookingService.ts export class BookingService { constructor(private api: ApiClient) {} async getBooking(slotId: string): Promise { const response = await this.api.fetch(`/booking/${slotId}`); if (!response.ok) { if (response.status === 404) { throw new BookingNotFoundError(slotId); } throw new NetworkError('Failed to load booking'); } return response.json(); } async approveSwap(slotId: string, approvalId: number): Promise { const response = await this.api.fetch( `/booking/${slotId}/swap-approval/${approvalId}`, { method: 'PATCH' } ); if (!response.ok) { const data = await response.json(); throw new BookingError(data.message, 'APPROVE_FAILED', 'recoverable'); } } async cancelSwap(slotId: string, swapGroupId: string): Promise { const response = await this.api.fetch( `/booking/${slotId}/swap-group/${swapGroupId}`, { method: 'DELETE' } ); if (!response.ok) { const data = await response.json(); throw new BookingError(data.message, 'CANCEL_FAILED', 'recoverable'); } } } // Usage in component const bookingService = useMemo(() => new BookingService(apiFetch), []); const { result, refetch } = useBookingData(slotId, bookingService); ``` **Benefits**: - Testable without React - Can mock easily - Reusable across components - Clear API contract ## Recommended Implementation Plan ### Phase 1: Error Types (Low Risk, High Value) 1. Create typed error classes 2. Update error handling to use typed errors 3. Keep current state structure ### Phase 2: Extract Custom Hooks (Medium Risk, High Value) 1. Create `useBookingData` hook 2. Create `useSwapActions` hook 3. Create `usePositionSwap` hook 4. Move complex logic out of component ### Phase 3: Result Pattern (Medium Risk, Medium Value) 1. Replace loading/error/data trio with Result type 2. Update components to use Result pattern 3. Add type guards for safe access ### Phase 4: UI State Reducer (Low Risk, Medium Value) 1. Move modal states to reducer 2. Move swap states to reducer 3. Keep domain data separate ### Phase 5: Service Layer (Low Risk, High Value) 1. Create BookingService 2. Move all API calls to service 3. Add retry logic, caching, etc. in one place ## Example: Refactored Component Structure ```typescript export default function BookingDetailPage() { // Domain data const bookingService = useBookingService(); const { result: bookingResult, refetch } = useBookingData(slot_id, bookingService); const { actionResult, approveSwap, cancelSwap } = useSwapActions(slot_id, refetch); // UI state const [uiState, dispatch] = useReducer(uiReducer, initialUIState); // Derived state const booking = bookingResult.status === 'success' ? bookingResult.data : null; const error = actionResult.status === 'error' ? actionResult.error : null; const isLoading = bookingResult.status === 'loading' || actionResult.status === 'loading'; // Render logic if (bookingResult.status === 'loading') return ; if (bookingResult.status === 'error' && bookingResult.error.severity === 'fatal') { return ; } if (!booking) return ; return ( <> dispatch({ type: 'CLEAR_ACTION_ERROR' })} error={error} /> ); } ``` ## Tools to Consider 1. **TanStack Query (React Query)**: Handles data fetching, caching, refetching automatically 2. **Zustand**: Lightweight state management if you need global state 3. **XState**: Full state machine implementation for complex flows 4. **Zod**: Runtime type validation for API responses ## Metrics for Success - **Reduced useState calls**: From 14+ to ~3-5 - **Improved testability**: Can test hooks in isolation - **Better type safety**: TypeScript catches more errors - **Clearer separation**: UI state vs domain state vs loading state - **Easier debugging**: Result types make state explicit - **Better error handling**: Typed errors with severity levels