# Streamlined Error and Load State Handling Design ## Overview This document outlines a comprehensive design for standardizing error and loading state handling across all pages in the GridPilot website app using shared, user-friendly components. ## Problem Statement The current implementation has several inconsistencies: - Mixed state management approaches (hook-based vs manual useState) - Inconsistent naming (loading vs isLoading vs pending) - Different error UIs (full-screen, inline, modal, toast) - Various loading UIs (spinners, skeletons, text-only, full-screen) - No shared useDataFetching() hook - Error boundaries not used everywhere ## Solution Architecture ### 1. Unified State Management Pattern All pages will follow this consistent pattern: ```typescript // Standardized state interface interface PageState { data: T | null; isLoading: boolean; error: ApiError | null; retry: () => Promise; } // Standardized hook signature interface UseDataFetchingOptions { queryKey: string[]; queryFn: () => Promise; enabled?: boolean; retryOnMount?: boolean; } // Standardized return type interface UseDataFetchingResult { data: T | null; isLoading: boolean; isFetching: boolean; error: ApiError | null; retry: () => Promise; refetch: () => Promise; } ``` ### 2. Component Architecture ``` apps/website/components/shared/ ├── state/ │ ├── LoadingWrapper.tsx # Main loading component with variants │ ├── ErrorDisplay.tsx # Standardized error display │ ├── EmptyState.tsx # Enhanced empty state │ └── StateContainer.tsx # Combined wrapper for all states ├── hooks/ │ └── useDataFetching.ts # Unified data fetching hook └── types/ └── state.types.ts # TypeScript interfaces ``` ### 3. Shared Component Design #### LoadingWrapper Component **Purpose**: Provides consistent loading states with multiple variants **Variants**: - `spinner` - Traditional loading spinner (default) - `skeleton` - Skeleton screens for better UX - `full-screen` - Centered in viewport - `inline` - Compact inline loading - `card` - Loading card placeholders **Props Interface**: ```typescript interface LoadingWrapperProps { variant?: 'spinner' | 'skeleton' | 'full-screen' | 'inline' | 'card'; message?: string; className?: string; size?: 'sm' | 'md' | 'lg'; /** * For skeleton variant - number of skeleton items to show */ skeletonCount?: number; /** * For card variant - card layout configuration */ cardConfig?: { height?: number; count?: number; className?: string; }; } ``` **Usage Examples**: ```typescript // Default spinner // Full-screen with custom message // Skeleton for list // Inline loading ``` #### ErrorDisplay Component **Purpose**: Standardized error display with consistent behavior **Props Interface**: ```typescript interface ErrorDisplayProps { error: ApiError; onRetry?: () => void; variant?: 'full-screen' | 'inline' | 'card' | 'toast'; /** * Show retry button (auto-detected from error.isRetryable()) */ showRetry?: boolean; /** * Show navigation buttons */ showNavigation?: boolean; /** * Additional actions */ actions?: Array<{ label: string; onClick: () => void; variant?: 'primary' | 'secondary' | 'danger'; }>; className?: string; /** * Hide technical details in development */ hideTechnicalDetails?: boolean; } ``` **Features**: - Auto-detects retryable errors - Shows user-friendly messages - Provides navigation options (back, home) - Displays technical details in development - Supports custom actions - Accessible (ARIA labels, keyboard navigation) **Usage Examples**: ```typescript // Full-screen error // Inline error with custom actions router.push('/support') } ]} /> // Toast-style error ``` #### EmptyState Component **Purpose**: Consistent empty/placeholder states **Props Interface**: ```typescript interface EmptyStateProps { icon: LucideIcon; title: string; description?: string; action?: { label: string; onClick: () => void; icon?: LucideIcon; }; variant?: 'default' | 'minimal' | 'full-page'; className?: string; /** * Show illustration instead of icon */ illustration?: 'racing' | 'league' | 'team' | 'sponsor'; } ``` **Usage Examples**: ```typescript // No races router.push('/leagues') }} /> // No data with illustration ``` ### 4. useDataFetching Hook **Purpose**: Unified data fetching with built-in state management **Signature**: ```typescript function useDataFetching( options: UseDataFetchingOptions ): UseDataFetchingResult ``` **Implementation Features**: - Built-in retry logic - Error classification - Loading state management - Refetch capability - Integration with error boundaries - Automatic retry on mount for recoverable errors **Usage Example**: ```typescript // Dashboard page function DashboardPage() { const { data, isLoading, error, retry, refetch } = useDataFetching({ queryKey: ['dashboardOverview'], queryFn: () => dashboardService.getDashboardOverview(), retryOnMount: true, }); if (isLoading) { return ; } if (error) { return ( ); } if (!data) { return ( ); } return ; } ``` ### 5. StateContainer Component **Purpose**: Combined wrapper that handles all states automatically **Props Interface**: ```typescript interface StateContainerProps { data: T | null; isLoading: boolean; error: ApiError | null; retry: () => Promise; children: (data: T) => React.ReactNode; /** * Configuration for each state */ config?: { loading?: { variant?: LoadingWrapperProps['variant']; message?: string; }; error?: { variant?: ErrorDisplayProps['variant']; actions?: ErrorDisplayProps['actions']; }; empty?: { icon: LucideIcon; title: string; description?: string; action?: EmptyStateProps['action']; }; }; className?: string; } ``` **Usage Example**: ```typescript // Simplified page implementation function LeagueDetailPage() { const { data, isLoading, error, retry } = useDataFetching({ queryKey: ['league', leagueId], queryFn: () => leagueService.getLeague(leagueId), }); return ( router.push('/leagues') } } }} > {(leagueData) => } ); } ``` ## 6. Error Boundary Integration ### EnhancedErrorBoundary Updates The existing `EnhancedErrorBoundary` will be updated to: 1. Support the new standardized error display 2. Provide automatic retry functionality 3. Support custom fallback components 4. Integrate with the notification system ```typescript interface EnhancedErrorBoundaryProps { children: ReactNode; fallback?: ReactNode; onError?: (error: Error, errorInfo: ErrorInfo) => void; onReset?: () => void; enableDevOverlay?: boolean; context?: Record; /** * NEW: Auto-retry configuration */ autoRetry?: { enabled: boolean; maxAttempts?: number; delay?: number; }; /** * NEW: Error display variant */ errorVariant?: ErrorDisplayProps['variant']; } ``` ### ApiErrorBoundary Updates The `ApiErrorBoundary` will be enhanced to: 1. Handle both API errors and regular errors 2. Support retry functionality 3. Provide consistent UI across all API calls ## 7. Migration Strategy ### Phase 1: Foundation (Week 1) 1. Create new shared components in `components/shared/state/` 2. Create `useDataFetching` hook 3. Update TypeScript interfaces 4. Add comprehensive tests ### Phase 2: Core Pages (Week 2-3) Update pages in priority order: **High Priority**: - `app/dashboard/page.tsx` - `app/leagues/[id]/LeagueDetailInteractive.tsx` - `app/races/[id]/RaceDetailInteractive.tsx` **Medium Priority**: - `app/teams/[id]/TeamDetailInteractive.tsx` - `app/leagues/[id]/schedule/page.tsx` - `app/races/[id]/results/RaceResultsInteractive.tsx` - `app/races/[id]/stewarding/RaceStewardingInteractive.tsx` **Lower Priority**: - Sponsor pages - Profile pages - Other interactive components ### Phase 3: Rollout (Week 4) 1. Update remaining pages 2. Add migration codemods for automation 3. Update documentation 4. Team training ### Phase 4: Deprecation (Week 5-6) 1. Remove old components 2. Clean up legacy code 3. Performance optimization ## 8. Migration Examples ### Before (Current State) ```typescript // Dashboard page - inconsistent patterns function DashboardPage() { const { data: dashboardData, isLoading, error } = useDashboardOverview(); if (isLoading) { return (
Loading dashboard...
); } if (error || !dashboardData) { return (
Failed to load dashboard
); } // ... render content } ``` ### After (Standardized) ```typescript // Dashboard page - unified pattern function DashboardPage() { const { data, isLoading, error, retry } = useDataFetching({ queryKey: ['dashboardOverview'], queryFn: () => dashboardService.getDashboardOverview(), }); if (isLoading) { return ; } if (error) { return ; } if (!data) { return ( ); } return ; } ``` ### Before (League Detail - Manual State) ```typescript function LeagueDetailInteractive() { const [viewModel, setViewModel] = useState(null); const [loading, setLoading] = useState(true); const [error, setError] = useState(null); const loadLeagueData = async () => { try { const viewModelData = await leagueService.getLeagueDetailPageData(leagueId); if (!viewModelData) { setError('League not found'); setLoading(false); return; } setViewModel(viewModelData); } catch (err) { setError(err instanceof Error ? err.message : 'Failed to load league'); } finally { setLoading(false); } }; useEffect(() => { loadLeagueData(); }, [leagueId]); if (loading) { return
Loading league...
; } if (error || !viewModel) { return
{error || 'League not found'}
; } // ... render content } ``` ### After (League Detail - Standardized) ```typescript function LeagueDetailInteractive() { const params = useParams(); const leagueId = params.id as string; const { data: viewModel, isLoading, error, retry } = useDataFetching({ queryKey: ['leagueDetailPage', leagueId], queryFn: () => leagueService.getLeagueDetailPageData(leagueId), }); return ( router.push('/leagues') } } }} > {(leagueData) => } ); } ``` ## 9. Testing Strategy ### Component Tests ```typescript // LoadingWrapper tests describe('LoadingWrapper', () => { it('renders spinner by default', () => { /* ... */ }); it('renders skeleton variant', () => { /* ... */ }); it('shows custom message', () => { /* ... */ }); it('is accessible', () => { /* ... */ }); }); // ErrorDisplay tests describe('ErrorDisplay', () => { it('shows retry button for retryable errors', () => { /* ... */ }); it('hides retry for non-retryable errors', () => { /* ... */ }); it('provides navigation options', () => { /* ... */ }); it('shows technical details in dev mode', () => { /* ... */ }); }); // useDataFetching tests describe('useDataFetching', () => { it('manages loading state correctly', () => { /* ... */ }); it('handles errors properly', () => { /* ... */ }); it('provides retry functionality', () => { /* ... */ }); it('integrates with error boundaries', () => { /* ... */ }); }); ``` ### Integration Tests ```typescript describe('Page State Handling', () => { it('dashboard shows loading, then content', async () => { /* ... */ }); it('league page handles not found', async () => { /* ... */ }); it('race page handles network errors', async () => { /* ... */ }); }); ``` ## 10. Accessibility Considerations All components will be fully accessible: - ARIA labels for loading states - Keyboard navigation support - Screen reader announcements - High contrast support - Focus management ## 11. Performance Considerations - Lazy loading for heavy components - Memoization where appropriate - Optimistic updates support - Debounced retry logic - Minimal re-renders ## 12. Documentation and Training ### Developer Documentation - Component API documentation - Migration guides - Best practices - Troubleshooting guide ### Team Training - Workshop on new patterns - Code review guidelines - Migration walkthroughs ## 13. TypeScript Interfaces Reference All TypeScript interfaces are defined in `components/shared/state/types.ts`. Here's a comprehensive reference: ### Core State Interfaces ```typescript // Basic state for any data fetching interface PageState { data: T | null; isLoading: boolean; error: ApiError | null; retry: () => Promise; } // Extended state with metadata interface PageStateWithMeta extends PageState { isFetching: boolean; refetch: () => Promise; lastUpdated: Date | null; isStale: boolean; } ``` ### Hook Interfaces ```typescript // useDataFetching options interface UseDataFetchingOptions { queryKey: string[]; queryFn: () => Promise; enabled?: boolean; retryOnMount?: boolean; cacheTime?: number; staleTime?: number; maxRetries?: number; retryDelay?: number; onSuccess?: (data: T) => void; onError?: (error: ApiError) => void; } // useDataFetching result interface UseDataFetchingResult { data: T | null; isLoading: boolean; isFetching: boolean; error: ApiError | null; retry: () => Promise; refetch: () => Promise; lastUpdated: Date | null; isStale: boolean; } ``` ### Component Props ```typescript // LoadingWrapper interface LoadingWrapperProps { variant?: 'spinner' | 'skeleton' | 'full-screen' | 'inline' | 'card'; message?: string; className?: string; size?: 'sm' | 'md' | 'lg'; skeletonCount?: number; cardConfig?: { height?: number; count?: number; className?: string; }; ariaLabel?: string; } // ErrorDisplay interface ErrorDisplayProps { error: ApiError; onRetry?: () => void; variant?: 'full-screen' | 'inline' | 'card' | 'toast'; showRetry?: boolean; showNavigation?: boolean; actions?: ErrorAction[]; className?: string; hideTechnicalDetails?: boolean; ariaLabel?: string; } interface ErrorAction { label: string; onClick: () => void; variant?: 'primary' | 'secondary' | 'danger' | 'ghost'; icon?: LucideIcon; disabled?: boolean; } // EmptyState interface EmptyStateProps { icon: LucideIcon; title: string; description?: string; action?: { label: string; onClick: () => void; icon?: LucideIcon; variant?: 'primary' | 'secondary'; }; variant?: 'default' | 'minimal' | 'full-page'; className?: string; illustration?: 'racing' | 'league' | 'team' | 'sponsor' | 'driver'; ariaLabel?: string; } // StateContainer interface StateContainerProps { data: T | null; isLoading: boolean; error: ApiError | null; retry: () => Promise; children: (data: T) => ReactNode; config?: StateContainerConfig; className?: string; showEmpty?: boolean; isEmpty?: (data: T) => boolean; } interface StateContainerConfig { loading?: { variant?: LoadingVariant; message?: string; size?: 'sm' | 'md' | 'lg'; }; error?: { variant?: ErrorVariant; actions?: ErrorAction[]; showRetry?: boolean; showNavigation?: boolean; }; empty?: { icon: LucideIcon; title: string; description?: string; action?: { label: string; onClick: () => void; }; }; customRender?: { loading?: () => ReactNode; error?: (error: ApiError) => ReactNode; empty?: () => ReactNode; }; } ``` ### Page Template Interfaces ```typescript // Generic page template interface PageTemplateProps { data: T | null; isLoading: boolean; error: ApiError | null; retry: () => Promise; refetch: () => Promise; title?: string; description?: string; children: (data: T) => ReactNode; config?: StateContainerConfig; } // List page template interface ListPageTemplateProps extends PageTemplateProps { emptyConfig?: { icon: LucideIcon; title: string; description?: string; action?: { label: string; onClick: () => void; }; }; showSkeleton?: boolean; skeletonCount?: number; } // Detail page template interface DetailPageTemplateProps extends PageTemplateProps { onBack?: () => void; onRefresh?: () => void; } ``` ### Configuration Interfaces ```typescript // Retry configuration interface RetryConfig { maxAttempts?: number; baseDelay?: number; backoffMultiplier?: number; retryOnMount?: boolean; } // Notification configuration interface NotificationConfig { showToastOnSuccess?: boolean; showToastOnError?: boolean; successMessage?: string; errorMessage?: string; autoDismissDelay?: number; } // Analytics configuration interface StateAnalytics { onStateChange?: (from: string, to: string, data?: unknown) => void; onError?: (error: ApiError, context: string) => void; onRetry?: (attempt: number, maxAttempts: number) => void; } // Performance metrics interface PerformanceMetrics { timeToFirstRender?: number; timeToDataLoad?: number; retryCount?: number; cacheHit?: boolean; } // Advanced configuration interface AdvancedStateConfig extends StateContainerConfig { retry?: RetryConfig; notifications?: NotificationConfig; analytics?: StateAnalytics; performance?: PerformanceMetrics; } ``` ### Type Aliases ```typescript type LoadingVariant = 'spinner' | 'skeleton' | 'full-screen' | 'inline' | 'card'; type ErrorVariant = 'full-screen' | 'inline' | 'card' | 'toast'; type EmptyVariant = 'default' | 'minimal' | 'full-page'; type EmptyCheck = (data: T | null) => boolean; ``` ### Default Configuration ```typescript const DEFAULT_CONFIG = { loading: { variant: 'spinner', message: 'Loading...', size: 'md', }, error: { variant: 'full-screen', showRetry: true, showNavigation: true, }, empty: { title: 'No data available', description: 'There is nothing to display here', }, retry: { maxAttempts: 3, baseDelay: 1000, backoffMultiplier: 2, retryOnMount: true, }, notifications: { showToastOnSuccess: false, showToastOnError: true, autoDismissDelay: 5000, }, } as const; ``` ## 14. Success Metrics - 100% of pages use standardized components - Reduced error handling code by 60% - Consistent user experience across all pages - Improved error recovery rates - Faster development velocity ## 15. Rollback Plan If issues arise: 1. Feature flags to toggle new components 2. Gradual rollout with monitoring 3. Quick revert capability 4. Comprehensive logging for debugging --- This design provides a complete, user-friendly solution for error and load state handling that will improve both developer experience and user experience across the entire GridPilot website application.